famedlysdk/lib/src/event.dart

470 lines
14 KiB
Dart
Raw Normal View History

2019-06-09 11:57:33 +00:00
/*
* Copyright (c) 2019 Zender & Kurtz GbR.
*
* Authors:
* Christian Pauly <krille@famedly.com>
* Marcel Radzio <mtrnord@famedly.com>
*
* This file is part of famedlysdk.
*
* famedlysdk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* famedlysdk is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with famedlysdk. If not, see <http://www.gnu.org/licenses/>.
2019-06-09 11:57:33 +00:00
*/
2020-01-02 14:09:49 +00:00
import 'dart:convert';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/src/utils/receipt.dart';
import './room.dart';
2020-01-04 09:31:27 +00:00
/// All data exchanged over Matrix is expressed as an "event". Typically each client action (e.g. sending a message) correlates with exactly one event.
2020-01-02 14:09:49 +00:00
class Event {
/// The Matrix ID for this event in the format '$localpart:server.abc'. Please not
/// that account data, presence and other events may not have an eventId.
final String eventId;
/// The json payload of the content. The content highly depends on the type.
Map<String, dynamic> content;
/// The type String of this event. For example 'm.room.message'.
final String typeKey;
/// The ID of the room this event belongs to.
final String roomId;
/// The user who has sent this event if it is not a global account data event.
final String senderId;
2020-02-14 14:06:46 +00:00
User get sender => room.getUserByMXIDSync(senderId ?? "@unknown");
2020-01-02 14:09:49 +00:00
/// The time this event has received at the server. May be null for events like
/// account data.
final DateTime time;
/// Optional additional content for this event.
Map<String, dynamic> unsigned;
/// The room this event belongs to. May be null.
final Room room;
/// Optional. The previous content for this state.
/// This will be present only for state events appearing in the timeline.
/// If this is not a state event, or there is no previous content, this key will be null.
Map<String, dynamic> prevContent;
/// Optional. This key will only be present for state events. A unique key which defines
/// the overwriting semantics for this piece of room state.
final String stateKey;
2019-06-11 09:23:57 +00:00
/// The status of this event.
/// -1=ERROR
/// 0=SENDING
/// 1=SENT
2020-01-02 14:09:49 +00:00
/// 2=TIMELINE
/// 3=ROOM_STATE
2019-06-11 09:23:57 +00:00
int status;
2019-08-29 07:12:55 +00:00
static const int defaultStatus = 2;
2020-01-02 14:09:49 +00:00
static const Map<String, int> STATUS_TYPE = {
"ERROR": -1,
"SENDING": 0,
"SENT": 1,
"TIMELINE": 2,
"ROOM_STATE": 3,
};
/// Optional. The event that redacted this event, if any. Otherwise null.
Event get redactedBecause =>
unsigned != null && unsigned.containsKey("redacted_because")
? Event.fromJson(unsigned["redacted_because"], room)
: null;
bool get redacted => redactedBecause != null;
User get stateKeyUser => room.getUserByMXIDSync(stateKey);
2019-08-29 07:12:55 +00:00
Event(
2019-08-29 07:12:55 +00:00
{this.status = defaultStatus,
2020-01-02 14:09:49 +00:00
this.content,
this.typeKey,
this.eventId,
this.roomId,
this.senderId,
this.time,
this.unsigned,
this.prevContent,
this.stateKey,
this.room});
static Map<String, dynamic> getMapFromPayload(dynamic payload) {
2020-01-02 14:33:26 +00:00
if (payload is String) {
2020-01-02 14:09:49 +00:00
try {
return json.decode(payload);
} catch (e) {
return {};
}
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
if (payload is Map<String, dynamic>) return payload;
return {};
}
2019-08-07 07:52:36 +00:00
/// Get a State event from a table row or from the event stream.
2019-08-07 08:46:59 +00:00
factory Event.fromJson(Map<String, dynamic> jsonPayload, Room room) {
2019-08-07 07:52:36 +00:00
final Map<String, dynamic> content =
2020-01-02 14:09:49 +00:00
Event.getMapFromPayload(jsonPayload['content']);
2019-08-07 07:52:36 +00:00
final Map<String, dynamic> unsigned =
2020-01-02 14:09:49 +00:00
Event.getMapFromPayload(jsonPayload['unsigned']);
2019-08-08 10:29:09 +00:00
final Map<String, dynamic> prevContent =
2020-01-02 14:09:49 +00:00
Event.getMapFromPayload(jsonPayload['prev_content']);
2019-08-07 07:52:36 +00:00
return Event(
2020-01-02 14:09:49 +00:00
status: jsonPayload['status'] ?? defaultStatus,
stateKey: jsonPayload['state_key'],
prevContent: prevContent,
content: content,
typeKey: jsonPayload['type'],
eventId: jsonPayload['event_id'],
roomId: jsonPayload['room_id'],
senderId: jsonPayload['sender'],
time: jsonPayload.containsKey('origin_server_ts')
? DateTime.fromMillisecondsSinceEpoch(jsonPayload['origin_server_ts'])
: DateTime.now(),
unsigned: unsigned,
room: room,
);
}
Map<String, dynamic> toJson() {
2020-01-02 14:33:26 +00:00
final Map<String, dynamic> data = Map<String, dynamic>();
2020-01-02 14:09:49 +00:00
if (this.stateKey != null) data['state_key'] = this.stateKey;
2020-01-02 14:33:26 +00:00
if (this.prevContent != null && this.prevContent.isNotEmpty) {
2020-01-02 14:09:49 +00:00
data['prev_content'] = this.prevContent;
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
data['content'] = this.content;
data['type'] = this.typeKey;
data['event_id'] = this.eventId;
data['room_id'] = this.roomId;
data['sender'] = this.senderId;
data['origin_server_ts'] = this.time.millisecondsSinceEpoch;
2020-01-02 14:33:26 +00:00
if (this.unsigned != null && this.unsigned.isNotEmpty) {
2020-01-02 14:09:49 +00:00
data['unsigned'] = this.unsigned;
2020-01-02 14:33:26 +00:00
}
2020-01-02 14:09:49 +00:00
return data;
}
/// The unique key of this event. For events with a [stateKey], it will be the
/// stateKey. Otherwise it will be the [type] as a string.
@deprecated
String get key => stateKey == null || stateKey.isEmpty ? typeKey : stateKey;
User get asUser => User.fromState(
stateKey: stateKey,
prevContent: prevContent,
content: content,
typeKey: typeKey,
eventId: eventId,
roomId: roomId,
senderId: senderId,
time: time,
unsigned: unsigned,
room: room);
/// Get the real type.
EventTypes get type {
switch (typeKey) {
case "m.room.avatar":
return EventTypes.RoomAvatar;
case "m.room.name":
return EventTypes.RoomName;
case "m.room.topic":
return EventTypes.RoomTopic;
2020-01-29 12:11:21 +00:00
case "m.room.aliases":
2020-01-02 14:09:49 +00:00
return EventTypes.RoomAliases;
case "m.room.canonical_alias":
return EventTypes.RoomCanonicalAlias;
case "m.room.create":
return EventTypes.RoomCreate;
case "m.room.redaction":
return EventTypes.Redaction;
case "m.room.join_rules":
return EventTypes.RoomJoinRules;
case "m.room.member":
return EventTypes.RoomMember;
case "m.room.power_levels":
return EventTypes.RoomPowerLevels;
case "m.room.guest_access":
return EventTypes.GuestAccess;
case "m.room.history_visibility":
return EventTypes.HistoryVisibility;
2020-01-04 09:31:27 +00:00
case "m.sticker":
return EventTypes.Sticker;
2020-01-02 14:09:49 +00:00
case "m.room.message":
2020-01-04 09:31:27 +00:00
return EventTypes.Message;
2020-02-04 13:41:13 +00:00
case "m.room.encrypted":
2020-01-04 18:36:17 +00:00
return EventTypes.Encrypted;
2020-02-04 13:41:13 +00:00
case "m.room.encryption":
2020-01-04 18:36:17 +00:00
return EventTypes.Encryption;
case "m.call.invite":
return EventTypes.CallInvite;
case "m.call.answer":
return EventTypes.CallAnswer;
case "m.call.candidates":
return EventTypes.CallCandidates;
case "m.call.hangup":
return EventTypes.CallHangup;
2020-01-02 14:09:49 +00:00
}
return EventTypes.Unknown;
}
2020-01-04 09:31:27 +00:00
///
MessageTypes get messageType {
switch (content["msgtype"] ?? "m.text") {
case "m.text":
if (content.containsKey("m.relates_to")) {
return MessageTypes.Reply;
}
return MessageTypes.Text;
case "m.notice":
return MessageTypes.Notice;
case "m.emote":
return MessageTypes.Emote;
case "m.image":
return MessageTypes.Image;
case "m.video":
return MessageTypes.Video;
case "m.audio":
return MessageTypes.Audio;
case "m.file":
return MessageTypes.File;
case "m.sticker":
return MessageTypes.Sticker;
case "m.location":
return MessageTypes.Location;
2020-02-18 07:02:17 +00:00
case "m.bad.encrypted":
return MessageTypes.BadEncrypted;
2020-01-04 09:31:27 +00:00
default:
if (type == EventTypes.Message) {
return MessageTypes.Text;
}
return MessageTypes.None;
}
}
2020-01-02 14:09:49 +00:00
void setRedactionEvent(Event redactedBecause) {
unsigned = {
"redacted_because": redactedBecause.toJson(),
};
prevContent = null;
List<String> contentKeyWhiteList = [];
switch (type) {
case EventTypes.RoomMember:
contentKeyWhiteList.add("membership");
break;
case EventTypes.RoomCreate:
contentKeyWhiteList.add("creator");
break;
case EventTypes.RoomJoinRules:
contentKeyWhiteList.add("join_rule");
break;
case EventTypes.RoomPowerLevels:
contentKeyWhiteList.add("ban");
contentKeyWhiteList.add("events");
contentKeyWhiteList.add("events_default");
contentKeyWhiteList.add("kick");
contentKeyWhiteList.add("redact");
contentKeyWhiteList.add("state_default");
contentKeyWhiteList.add("users");
contentKeyWhiteList.add("users_default");
break;
case EventTypes.RoomAliases:
contentKeyWhiteList.add("aliases");
break;
case EventTypes.HistoryVisibility:
contentKeyWhiteList.add("history_visibility");
break;
default:
break;
}
List<String> toRemoveList = [];
for (var entry in content.entries) {
2020-01-02 14:33:26 +00:00
if (!contentKeyWhiteList.contains(entry.key)) {
2020-01-02 14:09:49 +00:00
toRemoveList.add(entry.key);
}
}
toRemoveList.forEach((s) => content.remove(s));
2019-08-07 07:52:36 +00:00
}
2019-06-09 10:16:48 +00:00
2019-06-11 09:23:57 +00:00
/// Returns the body of this event if it has a body.
String get text => content["body"] ?? "";
/// Returns the formatted boy of this event if it has a formatted body.
String get formattedText => content["formatted_body"] ?? "";
2020-01-14 11:27:26 +00:00
@Deprecated("Use [body] instead.")
String getBody() => body;
2019-06-11 09:23:57 +00:00
/// Use this to get the body.
2020-01-14 11:27:26 +00:00
String get body {
2019-12-12 12:19:18 +00:00
if (redacted) return "Redacted";
2019-06-12 06:22:30 +00:00
if (text != "") return text;
2019-06-12 07:07:07 +00:00
if (formattedText != "") return formattedText;
2019-08-07 07:52:36 +00:00
return "$type";
2019-06-09 10:16:48 +00:00
}
2019-10-20 09:44:14 +00:00
/// Returns a list of [Receipt] instances for this event.
List<Receipt> get receipts {
2019-10-25 08:02:56 +00:00
if (!(room.roomAccountData.containsKey("m.receipt"))) return [];
2019-10-20 09:44:14 +00:00
List<Receipt> receiptsList = [];
2019-10-25 08:02:56 +00:00
for (var entry in room.roomAccountData["m.receipt"].content.entries) {
2020-01-02 14:33:26 +00:00
if (entry.value["event_id"] == eventId) {
2020-01-02 14:09:49 +00:00
receiptsList.add(Receipt(room.getUserByMXIDSync(entry.key),
DateTime.fromMillisecondsSinceEpoch(entry.value["ts"])));
2020-01-02 14:33:26 +00:00
}
2019-10-20 09:44:14 +00:00
}
return receiptsList;
}
/// Removes this event if the status is < 1. This event will just be removed
2019-07-24 08:13:02 +00:00
/// from the database and the timelines. Returns false if not removed.
2019-07-24 08:48:13 +00:00
Future<bool> remove() async {
if (status < 1) {
2020-01-02 14:33:26 +00:00
if (room.client.store != null) {
2019-10-02 11:33:01 +00:00
await room.client.store.removeEvent(eventId);
2020-01-02 14:33:26 +00:00
}
2019-06-27 08:33:43 +00:00
2020-01-02 14:09:49 +00:00
room.client.onEvent.add(EventUpdate(
roomID: room.id,
type: "timeline",
2019-08-07 07:52:36 +00:00
eventType: typeKey,
2019-06-27 08:20:47 +00:00
content: {
2019-08-07 07:52:36 +00:00
"event_id": eventId,
2019-06-27 08:20:47 +00:00
"status": -2,
"content": {"body": "Removed..."}
}));
2019-07-24 08:13:02 +00:00
return true;
}
2019-07-24 08:13:02 +00:00
return false;
}
/// Try to send this event again. Only works with events of status -1.
Future<String> sendAgain({String txid}) async {
if (status != -1) return null;
2020-01-02 14:33:26 +00:00
await remove();
final String eventID = await room.sendTextEvent(text, txid: txid);
return eventID;
}
2019-11-26 06:38:44 +00:00
/// Whether the client is allowed to redact this event.
bool get canRedact => senderId == room.client.userID || room.canRedact;
2019-12-12 12:19:18 +00:00
/// Redacts this event. Returns [ErrorResponse] on error.
Future<dynamic> redact({String reason, String txid}) =>
room.redactEvent(eventId, reason: reason, txid: txid);
2020-02-11 11:06:54 +00:00
/// Whether this event is in reply to another event.
bool get isReply =>
content['m.relates_to'] is Map<String, dynamic> &&
content['m.relates_to']['m.in_reply_to'] is Map<String, dynamic> &&
content['m.relates_to']['m.in_reply_to']['event_id'] is String &&
(content['m.relates_to']['m.in_reply_to']['event_id'] as String)
2020-02-11 11:28:26 +00:00
.isNotEmpty;
2020-02-11 11:06:54 +00:00
/// Searches for the reply event in the given timeline.
Future<Event> getReplyEvent(Timeline timeline) async {
if (!isReply) return null;
2020-02-18 07:02:17 +00:00
final String replyEventId =
content['m.relates_to']['m.in_reply_to']['event_id'];
2020-02-11 11:06:54 +00:00
return await timeline.getEventById(replyEventId);
}
2020-02-21 08:44:05 +00:00
/// Trys to decrypt this event. Returns a m.bad.encrypted event
/// if it fails and does nothing if the event was not encrypted.
Event get decrypted => room.decryptGroupMessage(this);
2020-02-21 15:05:19 +00:00
/// If this event is encrypted and the decryption was not successful because
/// the session is unknown, this requests the session key from other devices
/// in the room. If the event is not encrypted or the decryption failed because
/// of a different error, this throws an exception.
Future<void> requestKey() async {
if (this.type != EventTypes.Encrypted ||
this.messageType != MessageTypes.BadEncrypted ||
this.content["body"] != DecryptError.UNKNOWN_SESSION) {
throw ("Session key not unknown");
}
final List<User> users = await room.requestParticipants();
2020-02-21 15:05:19 +00:00
await room.client.sendToDevice(
[],
"m.room_key_request",
{
"action": "request_cancellation",
"request_id": base64.encode(utf8.encode(content["session_id"])),
"requesting_device_id": room.client.deviceID,
},
toUsers: users);
2020-02-21 15:05:19 +00:00
await room.client.sendToDevice(
[],
"m.room_key_request",
{
"action": "request",
"body": {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": roomId,
"sender_key": content["sender_key"],
"session_id": content["session_id"],
},
"request_id": base64.encode(utf8.encode(content["session_id"])),
"requesting_device_id": room.client.deviceID,
},
encrypted: false,
toUsers: users);
2020-02-21 15:05:19 +00:00
return;
}
2019-06-09 10:16:48 +00:00
}
2020-01-02 14:09:49 +00:00
2020-01-04 09:31:27 +00:00
enum MessageTypes {
2020-01-02 14:09:49 +00:00
Text,
Emote,
Notice,
Image,
Video,
Audio,
File,
Location,
Reply,
2020-01-04 09:31:27 +00:00
Sticker,
2020-02-18 07:02:17 +00:00
BadEncrypted,
2020-01-04 09:31:27 +00:00
None,
}
enum EventTypes {
Message,
Sticker,
Redaction,
2020-01-02 14:09:49 +00:00
RoomAliases,
RoomCanonicalAlias,
RoomCreate,
RoomJoinRules,
RoomMember,
RoomPowerLevels,
RoomName,
RoomTopic,
RoomAvatar,
GuestAccess,
HistoryVisibility,
2020-01-04 18:36:17 +00:00
Encryption,
Encrypted,
CallInvite,
CallAnswer,
CallCandidates,
CallHangup,
2020-01-02 14:09:49 +00:00
Unknown,
}