[Event] Add support for redactions

This commit is contained in:
Christian Pauly 2019-12-12 12:19:18 +00:00
parent 3321c9b16d
commit bff394fbb5
8 changed files with 228 additions and 28 deletions

View File

@ -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<String, dynamic> 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<dynamic> redact({String reason, String txid}) =>
room.redactEvent(eventId, reason: reason, txid: txid);
}

View File

@ -955,6 +955,25 @@ class Room {
}
return resp;
}
/// Redacts this event. Returns [ErrorResponse] on error.
Future<dynamic> 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<String, dynamic> 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 }

View File

@ -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<String, RoomState> 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]);

View File

@ -32,7 +32,7 @@ class RoomState {
final String eventId;
/// The json payload of the content. The content highly depends on the type.
final Map<String, dynamic> content;
Map<String, dynamic> 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<String, dynamic> unsigned;
Map<String, dynamic> 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<String, dynamic> prevContent;
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;
/// 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<String, dynamic> 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<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
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<String> 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<String> 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,

View File

@ -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);
}

View File

@ -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<String, dynamic> 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)));

View File

@ -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 {

View File

@ -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");