From 5eff2784544b8829a102fddd19998380a0f8b901 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 16 Feb 2016 12:05:13 +0000 Subject: [PATCH 1/5] Keep redacted events in the timeline Everything gets confused when we remove events from timelines, so keep redacted events in there, and mark them as redacted instead. --- lib/models/event.js | 25 +++++++++++++++- lib/models/room.js | 69 +++++++++++++++++++++++++++++---------------- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/lib/models/event.js b/lib/models/event.js index d333a6261..2e794705c 100644 --- a/lib/models/event.js +++ b/lib/models/event.js @@ -189,5 +189,28 @@ module.exports.MatrixEvent.prototype = { getUnsigned: function() { return this.event.unsigned || {}; - } + }, + + /** + * Update the content of an event in the same way it would be by the server + * if it were redacted before it was sent to us + * + * @param {Object} the raw event causing the redaction + */ + makeRedacted: function(redaction_event) { + if (!this.event.unsigned) { + this.event.unsigned = {}; + } + this.event.unsigned.redacted_because = redaction_event; + this.event.content = {}; + }, + + /** + * Check if this event has been redacted + * + * @return {boolean} True if this event has been redacted + */ + isRedacted: function() { + return Boolean(this.getUnsigned().redacted_because); + }, }; diff --git a/lib/models/room.js b/lib/models/room.js index 5b14e2e16..8efb472b1 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -118,7 +118,6 @@ function Room(roomId, opts) { this.summary = null; this.storageToken = opts.storageToken; this._opts = opts; - this._redactions = []; this._txnToEvent = {}; // Pending in-flight requests { string: MatrixEvent } // receipts should clobber based on receipt_type and user_id pairs hence // the form of this structure. This is sub-optimal for the exposed APIs @@ -216,6 +215,22 @@ Room.prototype.getTimelineForEvent = function(eventId) { return (res === undefined) ? null : res; }; +/** + * Get an event which is stored in our timelines + * + * @param {string} eventId event ID to look for + * @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown + */ +Room.prototype.findEventById = function(eventId) { + var tl = this.getTimelineForEvent(eventId); + if (!tl) { + return undefined; + } + return utils.findElement(tl.getEvents(), + function(ev) { return ev.getId() == eventId; }); +}; + + /** * Get one of the notification counts for this room * @param {String} type The type of notification count to get. default: 'total' @@ -523,29 +538,8 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, Room.prototype._addEventToTimeline = function(event, timeline, toStartOfTimeline, spliceBeforeLocalEcho) { var eventId = event.getId(); - - if (this._redactions.indexOf(eventId) >= 0) { - return; // do not add the redacted event. - } - - if (event.getType() === "m.room.redaction") { - var redactId = event.event.redacts; - - // try to remove the element - var removed = this.removeEvent(redactId); - if (!removed) { - // redactions will trickle in BEFORE the event redacted so make - // a note of the redacted event; we'll check it later. - this._redactions.push(event.event.redacts); - } - // NB: We continue to add the redaction event to the timeline so clients - // can say "so and so redacted an event" if they wish to. - } - - if (this._redactions.indexOf(eventId) < 0) { - timeline.addEvent(event, toStartOfTimeline, spliceBeforeLocalEcho); - this._eventIdToTimeline[eventId] = timeline; - } + timeline.addEvent(event, toStartOfTimeline, spliceBeforeLocalEcho); + this._eventIdToTimeline[eventId] = timeline; var data = { timeline: timeline, @@ -599,6 +593,22 @@ Room.prototype._addLiveEvents = function(events) { } } + + if (events[i].getType() === "m.room.redaction") { + var redactId = events[i].event.redacts; + + // if we know about this event, redact its contents now. + var redactedEvent = this.findEventById(redactId); + if (redactedEvent) { + redactedEvent.makeRedacted(events[i]); + this.emit("Room.redaction", events[i], this); + } + + // NB: We continue to add the redaction event to the timeline so + // clients can say "so and so redacted an event" if they wish to. Also + // this may be needed to trigger an update. + } + var spliceBeforeLocalEcho = !isLocalEcho && addLocalEchoToEnd; if (!this._eventIdToTimeline[events[i].getId()]) { @@ -1147,6 +1157,17 @@ module.exports = Room; * }); */ +/** + * Fires when an event we had previously received is redacted. + * + * (Note this is *not* fired when the redaction happens before we receive the + * event). + * + * @event module:client~MatrixClient#"Room.redaction" + * @param {MatrixEvent} event The matrix event which was redacted + * @param {Room} room The room containing the redacted event + */ + /** * Fires whenever the name of a room is updated. * @event module:client~MatrixClient#"Room.name" From 4c6d0a512893fe868f5b38d5f98ae2257323ffce Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 16 Feb 2016 16:03:18 +0000 Subject: [PATCH 2/5] Redactions: only remove the keys that are specced for removal --- lib/models/event.js | 52 ++++++++++++++++++++++++++++++++++++++++++++- lib/models/room.js | 6 ++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/models/event.js b/lib/models/event.js index 2e794705c..68e2a8618 100644 --- a/lib/models/event.js +++ b/lib/models/event.js @@ -21,6 +21,7 @@ limitations under the License. * @module models/event */ +var utils = require("../utils"); /** * Enum for event statuses. @@ -202,7 +203,19 @@ module.exports.MatrixEvent.prototype = { this.event.unsigned = {}; } this.event.unsigned.redacted_because = redaction_event; - this.event.content = {}; + + for (var key in utils.keys(this.event)) { + if (!_REDACT_KEEP_KEY_MAP[key]) { + delete this.event[key]; + } + } + + var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {}; + for (var key in utils.keys(this.event.content)) { + if (!keeps[key]) { + delete this.event.content[key]; + } + } }, /** @@ -214,3 +227,40 @@ module.exports.MatrixEvent.prototype = { return Boolean(this.getUnsigned().redacted_because); }, }; + + +/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says: + * + * the server should strip off any keys not in the following list: + * event_id + * type + * room_id + * user_id + * state_key + * prev_state + * content + * [we keep 'unsigned' as well, since that is created by the local server] + * + * The content object should also be stripped of all keys, unless it is one of one of the following event types: + * m.room.member allows key membership + * m.room.create allows key creator + * m.room.join_rules allows key join_rule + * m.room.power_levels allows keys ban, events, events_default, kick, redact, state_default, users, users_default. + * m.room.aliases allows key aliases + */ +// a map giving the keys we keep when an event is redacted +var _REDACT_KEEP_KEY_MAP = [ + 'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state', 'content', 'unsigned', +].reduce(function(ret, val) { ret[val] = 1; return ret}, {}); + +// a map from event type to the .content keys we keep when an event is redacted +var _REDACT_KEEP_CONTENT_MAP = { + 'm.room.member': {'membership': 1}, + 'm.room.create': {'creator': 1}, + 'm.room.join_rules': {'join_rule': 1}, + 'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1, + 'kick': 1, 'redact': 1, 'state_default': 1, + 'users': 1, 'users_default': 1, + }, + 'm.room.aliases': {'aliases': 1}, +}; diff --git a/lib/models/room.js b/lib/models/room.js index 8efb472b1..0f3fc3943 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -602,6 +602,12 @@ Room.prototype._addLiveEvents = function(events) { if (redactedEvent) { redactedEvent.makeRedacted(events[i]); this.emit("Room.redaction", events[i], this); + + // TODO: we stash user displaynames (among other things) in + // RoomMember objects which are then attached to other events + // (in the sender and target fields). We should get those + // RoomMember objects to update themselves when the events that + // they are based on are changed. } // NB: We continue to add the redaction event to the timeline so From 6f3bdcfbb61268eea9bbc9f0f53bf5438df624ee Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 16 Feb 2016 16:03:40 +0000 Subject: [PATCH 3/5] Remember to propagate Room.redaction --- lib/sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/sync.js b/lib/sync.js index acd80ef6e..2742a2815 100644 --- a/lib/sync.js +++ b/lib/sync.js @@ -81,7 +81,8 @@ SyncApi.prototype.createRoom = function(roomId) { pendingEventOrdering: this.opts.pendingEventOrdering, timelineSupport: client.timelineSupport, }); - reEmit(client, room, ["Room.name", "Room.timeline", "Room.receipt", "Room.tags"]); + reEmit(client, room, ["Room.name", "Room.timeline", "Room.redaction", + "Room.receipt", "Room.tags"]); this._registerStateListeners(room); return room; }; From 6a6db360883d597d03b153373b5030284efe00dd Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 16 Feb 2016 16:14:45 +0000 Subject: [PATCH 4/5] delintificate --- lib/models/event.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/models/event.js b/lib/models/event.js index 68e2a8618..c2c1d4d54 100644 --- a/lib/models/event.js +++ b/lib/models/event.js @@ -204,14 +204,17 @@ module.exports.MatrixEvent.prototype = { } this.event.unsigned.redacted_because = redaction_event; - for (var key in utils.keys(this.event)) { + var key; + for (key in this.event) { + if (!this.event.hasOwnProperty(key)) { continue; } if (!_REDACT_KEEP_KEY_MAP[key]) { delete this.event[key]; } } var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {}; - for (var key in utils.keys(this.event.content)) { + for (key in this.event.content) { + if (!this.event.content.hasOwnProperty(key)) { continue; } if (!keeps[key]) { delete this.event.content[key]; } @@ -241,17 +244,20 @@ module.exports.MatrixEvent.prototype = { * content * [we keep 'unsigned' as well, since that is created by the local server] * - * The content object should also be stripped of all keys, unless it is one of one of the following event types: + * The content object should also be stripped of all keys, unless it is one of + * one of the following event types: * m.room.member allows key membership * m.room.create allows key creator * m.room.join_rules allows key join_rule - * m.room.power_levels allows keys ban, events, events_default, kick, redact, state_default, users, users_default. + * m.room.power_levels allows keys ban, events, events_default, kick, + * redact, state_default, users, users_default. * m.room.aliases allows key aliases */ // a map giving the keys we keep when an event is redacted var _REDACT_KEEP_KEY_MAP = [ - 'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state', 'content', 'unsigned', -].reduce(function(ret, val) { ret[val] = 1; return ret}, {}); + 'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state', + 'content', 'unsigned', +].reduce(function(ret, val) { ret[val] = 1; return ret; }, {}); // a map from event type to the .content keys we keep when an event is redacted var _REDACT_KEEP_CONTENT_MAP = { From ab0a06eea760b5484165d84ec01622e50ccbf0d8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 16 Feb 2016 16:19:32 +0000 Subject: [PATCH 5/5] More delinting --- lib/models/event.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/models/event.js b/lib/models/event.js index c2c1d4d54..bcdb47644 100644 --- a/lib/models/event.js +++ b/lib/models/event.js @@ -21,8 +21,6 @@ limitations under the License. * @module models/event */ -var utils = require("../utils"); - /** * Enum for event statuses. * @readonly