diff --git a/lib/filter-component.js b/lib/filter-component.js new file mode 100644 index 000000000..3930806f6 --- /dev/null +++ b/lib/filter-component.js @@ -0,0 +1,119 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +"use strict"; +/** + * @module filter-component + */ + +function _matches_wildcard(actual_value, filter_value) { + if (filter_value.endsWith("*")) { + type_prefix = filter_value.slice(0, -1); + return actual_value.substr(0, type_prefix.length) === type_prefix; + } + else { + return actual_value === filter_value; + } +} + +/** + * A FilterComponent is a section of a Filter definition which defines the + * types, rooms, senders filters etc to be applied to a particular type of resource. + * + * This is all ported from synapse's Filter object. + */ +FilterComponent = function(filter_json) { + this.filter_json = filter_json; + + this.types = filter_json.types || null; + this.not_types = filter_json.not_types || []; + + self.rooms = filter_json.rooms || null; + self.not_rooms = filter_json.not_rooms || []; + + self.senders = filter_json.senders || null; + self.not_senders = filter_json.not_senders || []; + + self.contains_url = filter_json.contains_url || null; +}; + +/** + * Checks with the filter component matches the given event + */ +FilterComponent.prototype.check = function(event) { + var sender = event.sender; + if (!sender) { + // Presence events have their 'sender' in content.user_id + if (event.content) { + sender = event.content.user_id; + } + } + + return this.checkFields( + event.room_id, + sender, + event.type, + event.content ? event.content.url !== undefined : false, + ); +}; + +/** + * Checks whether the filter matches the given event fields. + */ +FilterComponent.prototype.checkFields = + function(room_id, sender, event_type, contains_url) { + var literal_keys = { + "rooms": function(v) { return room_id === v; }, + "senders": function(v) { return sender === v; }, + "types": function(v) { return _matches_wildcard(event_type, v); }, + }; + + Object.keys(literal_keys).forEach(function(name) { + var match_func = literal_keys[name]; + var not_name = "not_" + name; + var disallowed_values = this[not_name]; + if (disallowed_values.map(match_func)) { + return false; + } + + var allowed_values = this[name]; + if (allowed_values) { + if (!allowed_values.map(match_func)) { + return false; + } + } + }); + + contains_url_filter = this.filter_json.contains_url; + if (contains_url_filter !== undefined) { + if (contains_url_filter !== contains_url) { + return false; + } + } + + return true; + } +}; + +FilterComponent.prototype.filter = function(events) { + return events.filter(this.check); +}; + +FilterComponent.prototype.limit = function() { + return this.filter_json.limit !== undefined ? this.filter_json.limit : 10; +}; + +/** The FilterComponent class */ +module.exports = FilterComponent; diff --git a/lib/filter.js b/lib/filter.js index e533ae937..ff81d2fcd 100644 --- a/lib/filter.js +++ b/lib/filter.js @@ -18,6 +18,8 @@ limitations under the License. * @module filter */ +var FilterComponent = require("./filter-component"); + /** * @param {Object} obj * @param {string} keyNesting @@ -63,6 +65,70 @@ Filter.prototype.getDefinition = function() { */ Filter.prototype.setDefinition = function(definition) { this.definition = definition; + + // This is all ported from synapse's FilterCollection() + + // definitions look something like: + // { + // "room": { + // "rooms": ["!abcde:example.com"], + // "not_rooms": ["!123456:example.com"], + // "state": { + // "types": ["m.room.*"], + // "not_rooms": ["!726s6s6q:example.com"] + // }, + // "timeline": { + // "limit": 10, + // "types": ["m.room.message"], + // "not_rooms": ["!726s6s6q:example.com"], + // "not_senders": ["@spam:example.com"] + // }, + // "ephemeral": { + // "types": ["m.receipt", "m.typing"], + // "not_rooms": ["!726s6s6q:example.com"], + // "not_senders": ["@spam:example.com"] + // } + // }, + // "presence": { + // "types": ["m.presence"], + // "not_senders": ["@alice:example.com"] + // }, + // "event_format": "client", + // "event_fields": ["type", "content", "sender"] + // } + + var room_filter_json = definition.room; + + // consider the top level rooms/not_rooms filter + var room_filter_fields = {}; + if (room_filter_json) { + if (room_filter_json.rooms) { + room_filter_fields.rooms = room_filter_json.rooms; + } + if (room_filter_json.rooms) { + room_filter_fields.not_rooms = room_filter_json.not_rooms; + } + + this._include_leave = room_filter_json.include_leave || false; + } + + this._room_filter = new FilterComponent(room_filter_fields); + this._room_timeline_filter = new FilterComponent(room_filter_json.timeline || {}); + + // don't bother porting this from synapse yet: + // this._room_state_filter = new FilterComponent(room_filter_json.state || {}); + // this._room_ephemeral_filter = new FilterComponent(room_filter_json.ephemeral || {}); + // this._room_account_data_filter = new FilterComponent(room_filter_json.account_data || {}); + // this._presence_filter = new FilterComponent(definition.presence || {}); + // this._account_data_filter = new FilterComponent(definition.account_data || {}); +}; + +/** + * Filter the list of events based on whether they are allowed in a timeline + * based on this filter + */ +Filter.prototype.filterRoomTimeline = function(events) { + return this._room_timeline_filter.filter(this._room_filter.filter(events)); }; /** diff --git a/lib/models/event-timeline.js b/lib/models/event-timeline.js index 4123849ac..f68bbe4d3 100644 --- a/lib/models/event-timeline.js +++ b/lib/models/event-timeline.js @@ -36,6 +36,7 @@ function EventTimeline(roomId) { this._startState.paginationToken = null; this._endState = new RoomState(roomId); this._endState.paginationToken = null; + this._filter = null; this._prevTimeline = null; this._nextTimeline = null; @@ -58,6 +59,21 @@ EventTimeline.BACKWARDS = "b"; */ EventTimeline.FORWARDS = "f"; +/** + * Get the filter object this timeline is filtered on + */ +EventTimeline.prototype.getFilter = function() { + return this._filter; +} + +/** + * Set the filter object this timeline is filtered on + * (passed to the server when paginating via /messages). + */ +EventTimeline.prototype.setFilter = function(filter) { + this._filter = filter; +} + /** * Initialise the start and end state with the given events * @@ -217,6 +233,14 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) EventTimeline.prototype.addEvent = function(event, atStart) { var stateContext = atStart ? this._startState : this._endState; + // manually filter the event if we have a filter, as currently we insert + // events incrementally only from the main /sync rather than a filtered + // /sync to avoid running multiple redundant /syncs. + if (this._filter) { + var events = this._filter.filterRoomTimeline([event]); + if (!events) return; + } + setEventMetadata(event, stateContext, atStart); // modify state