UNPKG

15.9 kBJavaScriptView Raw
1'use strict';
2
3var _typeof2 = require('babel-runtime/helpers/typeof');
4
5var _typeof3 = _interopRequireDefault(_typeof2);
6
7var _getIterator2 = require('babel-runtime/core-js/get-iterator');
8
9var _getIterator3 = _interopRequireDefault(_getIterator2);
10
11var _keys = require('babel-runtime/core-js/object/keys');
12
13var _keys2 = _interopRequireDefault(_keys);
14
15function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
17/*
18Copyright 2015, 2016 OpenMarket Ltd
19Copyright 2017 New Vector Ltd
20
21Licensed under the Apache License, Version 2.0 (the "License");
22you may not use this file except in compliance with the License.
23You may obtain a copy of the License at
24
25 http://www.apache.org/licenses/LICENSE-2.0
26
27Unless required by applicable law or agreed to in writing, software
28distributed under the License is distributed on an "AS IS" BASIS,
29WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30See the License for the specific language governing permissions and
31limitations under the License.
32*/
33/**
34 * @module pushprocessor
35 */
36
37var RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
38
39/**
40 * Construct a Push Processor.
41 * @constructor
42 * @param {Object} client The Matrix client object to use
43 */
44function PushProcessor(client) {
45 var _this = this;
46
47 var escapeRegExp = function escapeRegExp(string) {
48 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
49 };
50
51 var cachedGlobToRegex = {
52 // $glob: RegExp,
53 };
54
55 var matchingRuleFromKindSet = function matchingRuleFromKindSet(ev, kindset, device) {
56 for (var ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) {
57 var kind = RULEKINDS_IN_ORDER[ruleKindIndex];
58 var ruleset = kindset[kind];
59
60 for (var ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
61 var rule = ruleset[ruleIndex];
62 if (!rule.enabled) {
63 continue;
64 }
65
66 var rawrule = templateRuleToRaw(kind, rule, device);
67 if (!rawrule) {
68 continue;
69 }
70
71 if (_this.ruleMatchesEvent(rawrule, ev)) {
72 rule.kind = kind;
73 return rule;
74 }
75 }
76 }
77 return null;
78 };
79
80 var templateRuleToRaw = function templateRuleToRaw(kind, tprule, device) {
81 var rawrule = {
82 'rule_id': tprule.rule_id,
83 'actions': tprule.actions,
84 'conditions': []
85 };
86 switch (kind) {
87 case 'underride':
88 case 'override':
89 rawrule.conditions = tprule.conditions;
90 break;
91 case 'room':
92 if (!tprule.rule_id) {
93 return null;
94 }
95 rawrule.conditions.push({
96 'kind': 'event_match',
97 'key': 'room_id',
98 'value': tprule.rule_id
99 });
100 break;
101 case 'sender':
102 if (!tprule.rule_id) {
103 return null;
104 }
105 rawrule.conditions.push({
106 'kind': 'event_match',
107 'key': 'user_id',
108 'value': tprule.rule_id
109 });
110 break;
111 case 'content':
112 if (!tprule.pattern) {
113 return null;
114 }
115 rawrule.conditions.push({
116 'kind': 'event_match',
117 'key': 'content.body',
118 'pattern': tprule.pattern
119 });
120 break;
121 }
122 if (device) {
123 rawrule.conditions.push({
124 'kind': 'device',
125 'profile_tag': device
126 });
127 }
128 return rawrule;
129 };
130
131 var eventFulfillsCondition = function eventFulfillsCondition(cond, ev) {
132 var condition_functions = {
133 "event_match": eventFulfillsEventMatchCondition,
134 "device": eventFulfillsDeviceCondition,
135 "contains_display_name": eventFulfillsDisplayNameCondition,
136 "room_member_count": eventFulfillsRoomMemberCountCondition,
137 "sender_notification_permission": eventFulfillsSenderNotifPermCondition
138 };
139 if (condition_functions[cond.kind]) {
140 return condition_functions[cond.kind](cond, ev);
141 }
142 // unknown conditions: we previously matched all unknown conditions,
143 // but given that rules can be added to the base rules on a server,
144 // it's probably better to not match unknown conditions.
145 return false;
146 };
147
148 var eventFulfillsSenderNotifPermCondition = function eventFulfillsSenderNotifPermCondition(cond, ev) {
149 var notifLevelKey = cond['key'];
150 if (!notifLevelKey) {
151 return false;
152 }
153
154 var room = client.getRoom(ev.getRoomId());
155 if (!room || !room.currentState) {
156 return false;
157 }
158
159 // Note that this should not be the current state of the room but the state at
160 // the point the event is in the DAG. Unfortunately the js-sdk does not store
161 // this.
162 return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender());
163 };
164
165 var eventFulfillsRoomMemberCountCondition = function eventFulfillsRoomMemberCountCondition(cond, ev) {
166 if (!cond.is) {
167 return false;
168 }
169
170 var room = client.getRoom(ev.getRoomId());
171 if (!room || !room.currentState || !room.currentState.members) {
172 return false;
173 }
174
175 var memberCount = room.currentState.getJoinedMemberCount();
176
177 var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
178 if (!m) {
179 return false;
180 }
181 var ineq = m[1];
182 var rhs = parseInt(m[2]);
183 if (isNaN(rhs)) {
184 return false;
185 }
186 switch (ineq) {
187 case '':
188 case '==':
189 return memberCount == rhs;
190 case '<':
191 return memberCount < rhs;
192 case '>':
193 return memberCount > rhs;
194 case '<=':
195 return memberCount <= rhs;
196 case '>=':
197 return memberCount >= rhs;
198 default:
199 return false;
200 }
201 };
202
203 var eventFulfillsDisplayNameCondition = function eventFulfillsDisplayNameCondition(cond, ev) {
204 var content = ev.getContent();
205 if (!content || !content.body || typeof content.body != 'string') {
206 return false;
207 }
208
209 var room = client.getRoom(ev.getRoomId());
210 if (!room || !room.currentState || !room.currentState.members || !room.currentState.getMember(client.credentials.userId)) {
211 return false;
212 }
213
214 var displayName = room.currentState.getMember(client.credentials.userId).name;
215
216 // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
217 // as shorthand for [^0-9A-Za-z_].
218 var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
219 return content.body.search(pat) > -1;
220 };
221
222 var eventFulfillsDeviceCondition = function eventFulfillsDeviceCondition(cond, ev) {
223 return false; // XXX: Allow a profile tag to be set for the web client instance
224 };
225
226 var eventFulfillsEventMatchCondition = function eventFulfillsEventMatchCondition(cond, ev) {
227 if (!cond.key) {
228 return false;
229 }
230
231 var val = valueForDottedKey(cond.key, ev);
232 if (!val || typeof val != 'string') {
233 return false;
234 }
235
236 if (cond.value) {
237 return cond.value === val;
238 }
239
240 var regex = void 0;
241
242 if (cond.key == 'content.body') {
243 regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
244 } else {
245 regex = createCachedRegex('^', cond.pattern, '$');
246 }
247
248 return !!val.match(regex);
249 };
250
251 var createCachedRegex = function createCachedRegex(prefix, glob, suffix) {
252 if (cachedGlobToRegex[glob]) {
253 return cachedGlobToRegex[glob];
254 }
255 cachedGlobToRegex[glob] = new RegExp(prefix + globToRegexp(glob) + suffix, 'i');
256 return cachedGlobToRegex[glob];
257 };
258
259 var globToRegexp = function globToRegexp(glob) {
260 // From
261 // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
262 // Because micromatch is about 130KB with dependencies,
263 // and minimatch is not much better.
264 var pat = escapeRegExp(glob);
265 pat = pat.replace(/\\\*/g, '.*');
266 pat = pat.replace(/\?/g, '.');
267 pat = pat.replace(/\\\[(!|)(.*)\\]/g, function (match, p1, p2, offset, string) {
268 var first = p1 && '^' || '';
269 var second = p2.replace(/\\\-/, '-');
270 return '[' + first + second + ']';
271 });
272 return pat;
273 };
274
275 var valueForDottedKey = function valueForDottedKey(key, ev) {
276 var parts = key.split('.');
277 var val = void 0;
278
279 // special-case the first component to deal with encrypted messages
280 var firstPart = parts[0];
281 if (firstPart == 'content') {
282 val = ev.getContent();
283 parts.shift();
284 } else if (firstPart == 'type') {
285 val = ev.getType();
286 parts.shift();
287 } else {
288 // use the raw event for any other fields
289 val = ev.event;
290 }
291
292 while (parts.length > 0) {
293 var thispart = parts.shift();
294 if (!val[thispart]) {
295 return null;
296 }
297 val = val[thispart];
298 }
299 return val;
300 };
301
302 var matchingRuleForEventWithRulesets = function matchingRuleForEventWithRulesets(ev, rulesets) {
303 if (!rulesets || !rulesets.device) {
304 return null;
305 }
306 if (ev.getSender() == client.credentials.userId) {
307 return null;
308 }
309
310 var allDevNames = (0, _keys2.default)(rulesets.device);
311 for (var i = 0; i < allDevNames.length; ++i) {
312 var devname = allDevNames[i];
313 var devrules = rulesets.device[devname];
314
315 var matchingRule = matchingRuleFromKindSet(devrules, devname);
316 if (matchingRule) {
317 return matchingRule;
318 }
319 }
320 return matchingRuleFromKindSet(ev, rulesets.global);
321 };
322
323 var pushActionsForEventAndRulesets = function pushActionsForEventAndRulesets(ev, rulesets) {
324 var rule = matchingRuleForEventWithRulesets(ev, rulesets);
325 if (!rule) {
326 return {};
327 }
328
329 var actionObj = PushProcessor.actionListToActionsObject(rule.actions);
330
331 // Some actions are implicit in some situations: we add those here
332 if (actionObj.tweaks.highlight === undefined) {
333 // if it isn't specified, highlight if it's a content
334 // rule but otherwise not
335 actionObj.tweaks.highlight = rule.kind == 'content';
336 }
337
338 return actionObj;
339 };
340
341 this.ruleMatchesEvent = function (rule, ev) {
342 var ret = true;
343 for (var i = 0; i < rule.conditions.length; ++i) {
344 var cond = rule.conditions[i];
345 ret &= eventFulfillsCondition(cond, ev);
346 }
347 //console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
348 return ret;
349 };
350
351 /**
352 * Get the user's push actions for the given event
353 *
354 * @param {module:models/event.MatrixEvent} ev
355 *
356 * @return {PushAction}
357 */
358 this.actionsForEvent = function (ev) {
359 return pushActionsForEventAndRulesets(ev, client.pushRules);
360 };
361
362 /**
363 * Get one of the users push rules by its ID
364 *
365 * @param {string} ruleId The ID of the rule to search for
366 * @return {object} The push rule, or null if no such rule was found
367 */
368 this.getPushRuleById = function (ruleId) {
369 var _arr = ['device', 'global'];
370
371 for (var _i = 0; _i < _arr.length; _i++) {
372 var scope = _arr[_i];
373 if (client.pushRules[scope] === undefined) continue;
374
375 var _iteratorNormalCompletion = true;
376 var _didIteratorError = false;
377 var _iteratorError = undefined;
378
379 try {
380 for (var _iterator = (0, _getIterator3.default)(RULEKINDS_IN_ORDER), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
381 var kind = _step.value;
382
383 if (client.pushRules[scope][kind] === undefined) continue;
384
385 var _iteratorNormalCompletion2 = true;
386 var _didIteratorError2 = false;
387 var _iteratorError2 = undefined;
388
389 try {
390 for (var _iterator2 = (0, _getIterator3.default)(client.pushRules[scope][kind]), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
391 var rule = _step2.value;
392
393 if (rule.rule_id === ruleId) return rule;
394 }
395 } catch (err) {
396 _didIteratorError2 = true;
397 _iteratorError2 = err;
398 } finally {
399 try {
400 if (!_iteratorNormalCompletion2 && _iterator2.return) {
401 _iterator2.return();
402 }
403 } finally {
404 if (_didIteratorError2) {
405 throw _iteratorError2;
406 }
407 }
408 }
409 }
410 } catch (err) {
411 _didIteratorError = true;
412 _iteratorError = err;
413 } finally {
414 try {
415 if (!_iteratorNormalCompletion && _iterator.return) {
416 _iterator.return();
417 }
418 } finally {
419 if (_didIteratorError) {
420 throw _iteratorError;
421 }
422 }
423 }
424 }
425 return null;
426 };
427}
428
429/**
430 * Convert a list of actions into a object with the actions as keys and their values
431 * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
432 * becomes { notify: true, tweaks: { sound: 'default' } }
433 * @param {array} actionlist The actions list
434 *
435 * @return {object} A object with key 'notify' (true or false) and an object of actions
436 */
437PushProcessor.actionListToActionsObject = function (actionlist) {
438 var actionobj = { 'notify': false, 'tweaks': {} };
439 for (var i = 0; i < actionlist.length; ++i) {
440 var action = actionlist[i];
441 if (action === 'notify') {
442 actionobj.notify = true;
443 } else if ((typeof action === 'undefined' ? 'undefined' : (0, _typeof3.default)(action)) === 'object') {
444 if (action.value === undefined) {
445 action.value = true;
446 }
447 actionobj.tweaks[action.set_tweak] = action.value;
448 }
449 }
450 return actionobj;
451};
452
453/**
454 * @typedef {Object} PushAction
455 * @type {Object}
456 * @property {boolean} notify Whether this event should notify the user or not.
457 * @property {Object} tweaks How this event should be notified.
458 * @property {boolean} tweaks.highlight Whether this event should be highlighted
459 * on the UI.
460 * @property {boolean} tweaks.sound Whether this notification should produce a
461 * noise.
462 */
463
464/** The PushProcessor class. */
465module.exports = PushProcessor;
466//# sourceMappingURL=pushprocessor.js.map
\No newline at end of file