Merge branch 'event-enhance-redactions' into 'master'
[Event] Add support for redactions Closes #26 See merge request famedly/famedlysdk!135
This commit is contained in:
commit
ef09f099e0
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -180,10 +180,23 @@ class RoomList {
|
|||
eventUpdate.type == "state" ||
|
||||
eventUpdate.type == "invite_state") {
|
||||
RoomState stateEvent = RoomState.fromJson(eventUpdate.content, rooms[j]);
|
||||
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]);
|
||||
|
|
|
@ -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(
|
||||
|
@ -107,7 +115,24 @@ class RoomState {
|
|||
senderId: jsonPayload['sender'],
|
||||
time: ChatTime(jsonPayload['origin_server_ts']),
|
||||
unsigned: unsigned,
|
||||
room: room);
|
||||
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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue