UNPKG

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