From bff394fbb550c1deda5c5b936459d1d2f9b844cd Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Thu, 12 Dec 2019 12:19:18 +0000 Subject: [PATCH] =?UTF-8?q?[Event]=C2=A0Add=20support=20for=20redactions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/Event.dart | 14 +++++- lib/src/Room.dart | 19 ++++++++ lib/src/RoomList.dart | 21 +++++++-- lib/src/RoomState.dart | 101 ++++++++++++++++++++++++++++++++++------ lib/src/Timeline.dart | 10 +++- test/Event_test.dart | 28 +++++++++++ test/RoomList_test.dart | 30 +++++++++++- test/Timeline_test.dart | 33 ++++++++++--- 8 files changed, 228 insertions(+), 28 deletions(-) diff --git a/lib/src/Event.dart b/lib/src/Event.dart index f5251ab..ea12301 100644 --- a/lib/src/Event.dart +++ b/lib/src/Event.dart @@ -50,7 +50,8 @@ class Event extends RoomState { dynamic unsigned, dynamic prevContent, String stateKey, - Room room}) + Room room, + Event redactedBecause}) : super( content: content, typeKey: typeKey, @@ -71,6 +72,9 @@ class Event extends RoomState { RoomState.getMapFromPayload(jsonPayload['unsigned']); final Map prevContent = RoomState.getMapFromPayload(jsonPayload['prev_content']); + Event redactedBecause = null; + if (unsigned.containsKey("redacted_because")) + redactedBecause = Event.fromJson(unsigned["redacted_because"], room); return Event( status: jsonPayload['status'] ?? defaultStatus, content: content, @@ -82,7 +86,8 @@ class Event extends RoomState { unsigned: unsigned, prevContent: prevContent, stateKey: jsonPayload['state_key'], - room: room); + room: room, + redactedBecause: redactedBecause); } /// Returns the body of this event if it has a body. @@ -93,6 +98,7 @@ class Event extends RoomState { /// Use this to get the body. String getBody() { + if (redacted) return "Redacted"; if (text != "") return text; if (formattedText != "") return formattedText; return "$type"; @@ -141,4 +147,8 @@ class Event extends RoomState { /// Whether the client is allowed to redact this event. bool get canRedact => senderId == room.client.userID || room.canRedact; + + /// Redacts this event. Returns [ErrorResponse] on error. + Future redact({String reason, String txid}) => + room.redactEvent(eventId, reason: reason, txid: txid); } diff --git a/lib/src/Room.dart b/lib/src/Room.dart index d7eeba6..cacdd89 100644 --- a/lib/src/Room.dart +++ b/lib/src/Room.dart @@ -955,6 +955,25 @@ class Room { } return resp; } + + /// Redacts this event. Returns [ErrorResponse] on error. + Future redactEvent(String eventId, + {String reason, String txid}) async { + // Create new transaction id + String messageID; + final int now = DateTime.now().millisecondsSinceEpoch; + if (txid == null) { + messageID = "msg$now"; + } else + messageID = txid; + Map data = {}; + if (reason != null) data["reason"] = reason; + final dynamic resp = await client.connection.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/rooms/$id/redact/$eventId/$messageID", + data: data); + return resp; + } } enum PushRuleState { notify, mentions_only, dont_notify } diff --git a/lib/src/RoomList.dart b/lib/src/RoomList.dart index 2882e30..d3090c2 100644 --- a/lib/src/RoomList.dart +++ b/lib/src/RoomList.dart @@ -180,10 +180,23 @@ class RoomList { eventUpdate.type == "state" || eventUpdate.type == "invite_state") { RoomState stateEvent = RoomState.fromJson(eventUpdate.content, rooms[j]); - RoomState prevState = - rooms[j].getState(stateEvent.typeKey, stateEvent.stateKey); - if (prevState != null && prevState.time > stateEvent.time) return; - rooms[j].setState(stateEvent); + if (stateEvent.type == EventTypes.Redaction) { + final String redacts = eventUpdate.content["redacts"]; + rooms[j].states.states.forEach( + (String key, Map states) => states.forEach( + (String key, RoomState state) { + if (state.eventId == redacts) { + state.setRedactionEvent(stateEvent); + } + }, + ), + ); + } else { + RoomState prevState = + rooms[j].getState(stateEvent.typeKey, stateEvent.stateKey); + if (prevState != null && prevState.time > stateEvent.time) return; + rooms[j].setState(stateEvent); + } } else if (eventUpdate.type == "account_data") { rooms[j].roomAccountData[eventUpdate.eventType] = RoomAccountData.fromJson(eventUpdate.content, rooms[j]); diff --git a/lib/src/RoomState.dart b/lib/src/RoomState.dart index 50791df..2b58422 100644 --- a/lib/src/RoomState.dart +++ b/lib/src/RoomState.dart @@ -32,7 +32,7 @@ class RoomState { final String eventId; /// The json payload of the content. The content highly depends on the type. - final Map content; + Map content; /// The type String of this event. For example 'm.room.message'. final String typeKey; @@ -50,7 +50,7 @@ class RoomState { final ChatTime time; /// Optional additional content for this event. - final Map unsigned; + Map unsigned; /// The room this event belongs to. May be null. final Room room; @@ -58,12 +58,20 @@ class RoomState { /// 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. - final Map prevContent; + Map 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; + /// Optional. The event that redacted this event, if any. Otherwise null. + RoomState get redactedBecause => + unsigned != null && unsigned.containsKey("redacted_because") + ? RoomState.fromJson(unsigned["redacted_because"], room) + : null; + + bool get redacted => redactedBecause != null; + User get stateKeyUser => room.getUserByMXIDSync(stateKey); RoomState( @@ -98,16 +106,33 @@ class RoomState { final Map prevContent = RoomState.getMapFromPayload(jsonPayload['prev_content']); return RoomState( - stateKey: jsonPayload['state_key'], - prevContent: prevContent, - content: content, - typeKey: jsonPayload['type'], - eventId: jsonPayload['event_id'], - roomId: jsonPayload['room_id'], - senderId: jsonPayload['sender'], - time: ChatTime(jsonPayload['origin_server_ts']), - unsigned: unsigned, - room: room); + stateKey: jsonPayload['state_key'], + prevContent: prevContent, + content: content, + typeKey: jsonPayload['type'], + eventId: jsonPayload['event_id'], + roomId: jsonPayload['room_id'], + senderId: jsonPayload['sender'], + time: ChatTime(jsonPayload['origin_server_ts']), + unsigned: unsigned, + room: room, + ); + } + + Map toJson() { + final Map data = new Map(); + if (this.stateKey != null) data['state_key'] = this.stateKey; + if (this.prevContent != null && this.prevContent.isNotEmpty) + data['prev_content'] = this.prevContent; + 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.toTimeStamp(); + if (this.unsigned != null && this.unsigned.isNotEmpty) + data['unsigned'] = this.unsigned; + return data; } Event get timelineEvent => Event( @@ -154,6 +179,8 @@ class RoomState { 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": @@ -189,6 +216,53 @@ class RoomState { } return EventTypes.Unknown; } + + void setRedactionEvent(RoomState redactedBecause) { + unsigned = { + "redacted_because": redactedBecause.toJson(), + }; + prevContent = null; + List contentKeyWhiteList = []; + switch (type) { + case EventTypes.RoomMember: + contentKeyWhiteList.add("membership"); + break; + 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 toRemoveList = []; + for (var entry in content.entries) { + if (contentKeyWhiteList.indexOf(entry.key) == -1) { + toRemoveList.add(entry.key); + } + } + toRemoveList.forEach((s) => content.remove(s)); + } } enum EventTypes { @@ -198,6 +272,7 @@ enum EventTypes { Image, Video, Audio, + Redaction, File, Location, Reply, diff --git a/lib/src/Timeline.dart b/lib/src/Timeline.dart index cf11323..2a9f92f 100644 --- a/lib/src/Timeline.dart +++ b/lib/src/Timeline.dart @@ -91,7 +91,15 @@ class Timeline { try { if (eventUpdate.roomID != room.id) return; if (eventUpdate.type == "timeline" || eventUpdate.type == "history") { - if (eventUpdate.content["status"] == -2) { + // Redaction events are handled as modification for existing events. + if (eventUpdate.eventType == "m.room.redaction") { + final int eventId = + _findEvent(event_id: eventUpdate.content["redacts"]); + if (eventId != null) { + events[eventId] + .setRedactionEvent(Event.fromJson(eventUpdate.content, room)); + } + } else if (eventUpdate.content["status"] == -2) { int i = _findEvent(event_id: eventUpdate.content["event_id"]); if (i < events.length) events.removeAt(i); } diff --git a/test/Event_test.dart b/test/Event_test.dart index 6eeee16..139132f 100644 --- a/test/Event_test.dart +++ b/test/Event_test.dart @@ -48,12 +48,17 @@ void main() { "sender": senderID, "origin_server_ts": timestamp, "type": type, + "room_id": "1234", "status": 2, "content": contentJson, }; test("Create from json", () async { Event event = Event.fromJson(jsonObj, null); + jsonObj.remove("status"); + jsonObj["content"] = json.decode(contentJson); + expect(event.toJson(), jsonObj); + jsonObj["content"] = contentJson; expect(event.eventId, id); expect(event.senderId, senderID); @@ -156,6 +161,29 @@ void main() { expect(event.type, EventTypes.Reply); }); + test("redact", () async { + final Room room = + Room(id: "1234", client: Client("testclient", debug: true)); + final Map redactionEventJson = { + "content": {"reason": "Spamming"}, + "event_id": "143273582443PhrSn:example.org", + "origin_server_ts": 1432735824653, + "redacts": id, + "room_id": "1234", + "sender": "@example:example.org", + "type": "m.room.redaction", + "unsigned": {"age": 1234} + }; + RoomState redactedBecause = RoomState.fromJson(redactionEventJson, room); + Event event = Event.fromJson(jsonObj, room); + event.setRedactionEvent(redactedBecause); + expect(event.redacted, true); + expect(event.redactedBecause.toJson(), redactedBecause.toJson()); + expect(event.content.isEmpty, true); + redactionEventJson.remove("redacts"); + expect(event.unsigned["redacted_because"], redactionEventJson); + }); + test("remove", () async { Event event = Event.fromJson( jsonObj, Room(id: "1234", client: Client("testclient", debug: true))); diff --git a/test/RoomList_test.dart b/test/RoomList_test.dart index 0017c7b..baa9695 100644 --- a/test/RoomList_test.dart +++ b/test/RoomList_test.dart @@ -163,8 +163,9 @@ void main() { "type": "m.room.message", "content": {"msgtype": "m.text", "body": "Testcase"}, "sender": "@alice:example.com", + "room_id": "1", "status": 2, - "id": "1", + "event_id": "1", "origin_server_ts": now.toTimeStamp() - 1000 })); @@ -176,8 +177,9 @@ void main() { "type": "m.room.message", "content": {"msgtype": "m.text", "body": "Testcase 2"}, "sender": "@alice:example.com", + "room_id": "1", "status": 2, - "id": "2", + "event_id": "2", "origin_server_ts": now.toTimeStamp() })); @@ -195,6 +197,30 @@ void main() { expect(roomList.rooms[1].id, "1"); expect(roomList.rooms[0].lastMessage, "Testcase 2"); expect(roomList.rooms[0].timeCreated, now); + + client.connection.onEvent.add(EventUpdate( + type: "timeline", + roomID: "1", + eventType: "m.room.redaction", + content: { + "content": {"reason": "Spamming"}, + "event_id": "143273582443PhrSn:example.org", + "origin_server_ts": 1432735824653, + "redacts": "1", + "room_id": "1", + "sender": "@example:example.org", + "type": "m.room.redaction", + "unsigned": {"age": 1234} + })); + + await new Future.delayed(new Duration(milliseconds: 50)); + + expect(updateCount, 6); + expect(insertList, [0, 1]); + expect(removeList, []); + expect(roomList.rooms.length, 2); + expect(roomList.rooms[1].getState("m.room.message").eventId, "1"); + expect(roomList.rooms[1].getState("m.room.message").redacted, true); }); test("onlyLeft", () async { diff --git a/test/Timeline_test.dart b/test/Timeline_test.dart index 29c4b53..bb17e28 100644 --- a/test/Timeline_test.dart +++ b/test/Timeline_test.dart @@ -111,6 +111,27 @@ void main() { expect(timeline.events[0].receipts.length, 1); expect(timeline.events[0].receipts[0].user.id, "@alice:example.com"); + + client.connection.onEvent.add(EventUpdate( + type: "timeline", + roomID: roomID, + eventType: "m.room.redaction", + content: { + "type": "m.room.redaction", + "content": {"reason": "spamming"}, + "sender": "@alice:example.com", + "redacts": "2", + "event_id": "3", + "origin_server_ts": testTimeStamp + 1000 + })); + + await new Future.delayed(new Duration(milliseconds: 50)); + + expect(updateCount, 3); + expect(insertList, [0, 0]); + expect(insertList.length, timeline.events.length); + expect(timeline.events.length, 2); + expect(timeline.events[1].redacted, true); }); test("Send message", () async { @@ -118,7 +139,7 @@ void main() { await new Future.delayed(new Duration(milliseconds: 50)); - expect(updateCount, 4); + expect(updateCount, 5); expect(insertList, [0, 0, 0]); expect(insertList.length, timeline.events.length); expect(timeline.events[0].eventId, "42"); @@ -140,7 +161,7 @@ void main() { await new Future.delayed(new Duration(milliseconds: 50)); - expect(updateCount, 5); + expect(updateCount, 6); expect(insertList, [0, 0, 0]); expect(insertList.length, timeline.events.length); expect(timeline.events[0].eventId, "42"); @@ -168,7 +189,7 @@ void main() { room.sendTextEvent("test", txid: "errortxid3"); await new Future.delayed(new Duration(milliseconds: 50)); - expect(updateCount, 12); + expect(updateCount, 13); expect(insertList, [0, 0, 0, 0, 0, 0, 0]); expect(insertList.length, timeline.events.length); expect(timeline.events[0].status, -1); @@ -181,7 +202,7 @@ void main() { await new Future.delayed(new Duration(milliseconds: 50)); - expect(updateCount, 13); + expect(updateCount, 14); expect(insertList, [0, 0, 0, 0, 0, 0, 0]); expect(timeline.events.length, 6); @@ -193,7 +214,7 @@ void main() { await new Future.delayed(new Duration(milliseconds: 50)); - expect(updateCount, 16); + expect(updateCount, 17); expect(insertList, [0, 0, 0, 0, 0, 0, 0, 0]); expect(timeline.events.length, 6); @@ -205,7 +226,7 @@ void main() { await new Future.delayed(new Duration(milliseconds: 50)); - expect(updateCount, 19); + expect(updateCount, 20); expect(timeline.events.length, 9); expect(timeline.events[6].eventId, "1143273582443PhrSn:example.org"); expect(timeline.events[7].eventId, "2143273582443PhrSn:example.org");