1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import {escapeRegExp, globToRegexp} from "./utils";
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | function PushProcessor(client) {
|
32 | const cachedGlobToRegex = {
|
33 |
|
34 | };
|
35 |
|
36 | const matchingRuleFromKindSet = (ev, kindset, device) => {
|
37 | for (let ruleKindIndex = 0;
|
38 | ruleKindIndex < RULEKINDS_IN_ORDER.length;
|
39 | ++ruleKindIndex) {
|
40 | const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
|
41 | const ruleset = kindset[kind];
|
42 |
|
43 | for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
|
44 | const rule = ruleset[ruleIndex];
|
45 | if (!rule.enabled) {
|
46 | continue;
|
47 | }
|
48 |
|
49 | const rawrule = templateRuleToRaw(kind, rule, device);
|
50 | if (!rawrule) {
|
51 | continue;
|
52 | }
|
53 |
|
54 | if (this.ruleMatchesEvent(rawrule, ev)) {
|
55 | rule.kind = kind;
|
56 | return rule;
|
57 | }
|
58 | }
|
59 | }
|
60 | return null;
|
61 | };
|
62 |
|
63 | const templateRuleToRaw = function(kind, tprule, device) {
|
64 | const rawrule = {
|
65 | 'rule_id': tprule.rule_id,
|
66 | 'actions': tprule.actions,
|
67 | 'conditions': [],
|
68 | };
|
69 | switch (kind) {
|
70 | case 'underride':
|
71 | case 'override':
|
72 | rawrule.conditions = tprule.conditions;
|
73 | break;
|
74 | case 'room':
|
75 | if (!tprule.rule_id) {
|
76 | return null;
|
77 | }
|
78 | rawrule.conditions.push({
|
79 | 'kind': 'event_match',
|
80 | 'key': 'room_id',
|
81 | 'value': tprule.rule_id,
|
82 | });
|
83 | break;
|
84 | case 'sender':
|
85 | if (!tprule.rule_id) {
|
86 | return null;
|
87 | }
|
88 | rawrule.conditions.push({
|
89 | 'kind': 'event_match',
|
90 | 'key': 'user_id',
|
91 | 'value': tprule.rule_id,
|
92 | });
|
93 | break;
|
94 | case 'content':
|
95 | if (!tprule.pattern) {
|
96 | return null;
|
97 | }
|
98 | rawrule.conditions.push({
|
99 | 'kind': 'event_match',
|
100 | 'key': 'content.body',
|
101 | 'pattern': tprule.pattern,
|
102 | });
|
103 | break;
|
104 | }
|
105 | if (device) {
|
106 | rawrule.conditions.push({
|
107 | 'kind': 'device',
|
108 | 'profile_tag': device,
|
109 | });
|
110 | }
|
111 | return rawrule;
|
112 | };
|
113 |
|
114 | const eventFulfillsCondition = function(cond, ev) {
|
115 | const condition_functions = {
|
116 | "event_match": eventFulfillsEventMatchCondition,
|
117 | "device": eventFulfillsDeviceCondition,
|
118 | "contains_display_name": eventFulfillsDisplayNameCondition,
|
119 | "room_member_count": eventFulfillsRoomMemberCountCondition,
|
120 | "sender_notification_permission": eventFulfillsSenderNotifPermCondition,
|
121 | };
|
122 | if (condition_functions[cond.kind]) {
|
123 | return condition_functions[cond.kind](cond, ev);
|
124 | }
|
125 |
|
126 |
|
127 |
|
128 | return false;
|
129 | };
|
130 |
|
131 | const eventFulfillsSenderNotifPermCondition = function(cond, ev) {
|
132 | const notifLevelKey = cond['key'];
|
133 | if (!notifLevelKey) {
|
134 | return false;
|
135 | }
|
136 |
|
137 | const room = client.getRoom(ev.getRoomId());
|
138 | if (!room || !room.currentState) {
|
139 | return false;
|
140 | }
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender());
|
146 | };
|
147 |
|
148 | const eventFulfillsRoomMemberCountCondition = function(cond, ev) {
|
149 | if (!cond.is) {
|
150 | return false;
|
151 | }
|
152 |
|
153 | const room = client.getRoom(ev.getRoomId());
|
154 | if (!room || !room.currentState || !room.currentState.members) {
|
155 | return false;
|
156 | }
|
157 |
|
158 | const memberCount = room.currentState.getJoinedMemberCount();
|
159 |
|
160 | const m = cond.is.match(/^([=<>]*)([0-9]*)$/);
|
161 | if (!m) {
|
162 | return false;
|
163 | }
|
164 | const ineq = m[1];
|
165 | const rhs = parseInt(m[2]);
|
166 | if (isNaN(rhs)) {
|
167 | return false;
|
168 | }
|
169 | switch (ineq) {
|
170 | case '':
|
171 | case '==':
|
172 | return memberCount == rhs;
|
173 | case '<':
|
174 | return memberCount < rhs;
|
175 | case '>':
|
176 | return memberCount > rhs;
|
177 | case '<=':
|
178 | return memberCount <= rhs;
|
179 | case '>=':
|
180 | return memberCount >= rhs;
|
181 | default:
|
182 | return false;
|
183 | }
|
184 | };
|
185 |
|
186 | const eventFulfillsDisplayNameCondition = function(cond, ev) {
|
187 | const content = ev.getContent();
|
188 | if (!content || !content.body || typeof content.body != 'string') {
|
189 | return false;
|
190 | }
|
191 |
|
192 | const room = client.getRoom(ev.getRoomId());
|
193 | if (!room || !room.currentState || !room.currentState.members ||
|
194 | !room.currentState.getMember(client.credentials.userId)) {
|
195 | return false;
|
196 | }
|
197 |
|
198 | const displayName = room.currentState.getMember(client.credentials.userId).name;
|
199 |
|
200 |
|
201 |
|
202 | const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
|
203 | return content.body.search(pat) > -1;
|
204 | };
|
205 |
|
206 | const eventFulfillsDeviceCondition = function(cond, ev) {
|
207 | return false;
|
208 | };
|
209 |
|
210 | const eventFulfillsEventMatchCondition = function(cond, ev) {
|
211 | if (!cond.key) {
|
212 | return false;
|
213 | }
|
214 |
|
215 | const val = valueForDottedKey(cond.key, ev);
|
216 | if (!val || typeof val != 'string') {
|
217 | return false;
|
218 | }
|
219 |
|
220 | if (cond.value) {
|
221 | return cond.value === val;
|
222 | }
|
223 |
|
224 | let regex;
|
225 |
|
226 | if (cond.key == 'content.body') {
|
227 | regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
|
228 | } else {
|
229 | regex = createCachedRegex('^', cond.pattern, '$');
|
230 | }
|
231 |
|
232 | return !!val.match(regex);
|
233 | };
|
234 |
|
235 | const createCachedRegex = function(prefix, glob, suffix) {
|
236 | if (cachedGlobToRegex[glob]) {
|
237 | return cachedGlobToRegex[glob];
|
238 | }
|
239 | cachedGlobToRegex[glob] = new RegExp(
|
240 | prefix + globToRegexp(glob) + suffix,
|
241 | 'i',
|
242 | );
|
243 | return cachedGlobToRegex[glob];
|
244 | };
|
245 |
|
246 | const valueForDottedKey = function(key, ev) {
|
247 | const parts = key.split('.');
|
248 | let val;
|
249 |
|
250 |
|
251 | const firstPart = parts[0];
|
252 | if (firstPart == 'content') {
|
253 | val = ev.getContent();
|
254 | parts.shift();
|
255 | } else if (firstPart == 'type') {
|
256 | val = ev.getType();
|
257 | parts.shift();
|
258 | } else {
|
259 |
|
260 | val = ev.event;
|
261 | }
|
262 |
|
263 | while (parts.length > 0) {
|
264 | const thispart = parts.shift();
|
265 | if (!val[thispart]) {
|
266 | return null;
|
267 | }
|
268 | val = val[thispart];
|
269 | }
|
270 | return val;
|
271 | };
|
272 |
|
273 | const matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
274 | if (!rulesets || !rulesets.device) {
|
275 | return null;
|
276 | }
|
277 | if (ev.getSender() == client.credentials.userId) {
|
278 | return null;
|
279 | }
|
280 |
|
281 | const allDevNames = Object.keys(rulesets.device);
|
282 | for (let i = 0; i < allDevNames.length; ++i) {
|
283 | const devname = allDevNames[i];
|
284 | const devrules = rulesets.device[devname];
|
285 |
|
286 | const matchingRule = matchingRuleFromKindSet(devrules, devname);
|
287 | if (matchingRule) {
|
288 | return matchingRule;
|
289 | }
|
290 | }
|
291 | return matchingRuleFromKindSet(ev, rulesets.global);
|
292 | };
|
293 |
|
294 | const pushActionsForEventAndRulesets = function(ev, rulesets) {
|
295 | const rule = matchingRuleForEventWithRulesets(ev, rulesets);
|
296 | if (!rule) {
|
297 | return {};
|
298 | }
|
299 |
|
300 | const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
|
301 |
|
302 |
|
303 | if (actionObj.tweaks.highlight === undefined) {
|
304 |
|
305 |
|
306 | actionObj.tweaks.highlight = (rule.kind == 'content');
|
307 | }
|
308 |
|
309 | return actionObj;
|
310 | };
|
311 |
|
312 | this.ruleMatchesEvent = function(rule, ev) {
|
313 | let ret = true;
|
314 | for (let i = 0; i < rule.conditions.length; ++i) {
|
315 | const cond = rule.conditions[i];
|
316 | ret &= eventFulfillsCondition(cond, ev);
|
317 | }
|
318 |
|
319 | return ret;
|
320 | };
|
321 |
|
322 |
|
323 | |
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 | this.actionsForEvent = function(ev) {
|
331 | return pushActionsForEventAndRulesets(ev, client.pushRules);
|
332 | };
|
333 |
|
334 | |
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 | this.getPushRuleById = function(ruleId) {
|
341 | for (const scope of ['device', 'global']) {
|
342 | if (client.pushRules[scope] === undefined) continue;
|
343 |
|
344 | for (const kind of RULEKINDS_IN_ORDER) {
|
345 | if (client.pushRules[scope][kind] === undefined) continue;
|
346 |
|
347 | for (const rule of client.pushRules[scope][kind]) {
|
348 | if (rule.rule_id === ruleId) return rule;
|
349 | }
|
350 | }
|
351 | }
|
352 | return null;
|
353 | };
|
354 | }
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 | PushProcessor.actionListToActionsObject = function(actionlist) {
|
365 | const actionobj = { 'notify': false, 'tweaks': {} };
|
366 | for (let i = 0; i < actionlist.length; ++i) {
|
367 | const action = actionlist[i];
|
368 | if (action === 'notify') {
|
369 | actionobj.notify = true;
|
370 | } else if (typeof action === 'object') {
|
371 | if (action.value === undefined) {
|
372 | action.value = true;
|
373 | }
|
374 | actionobj.tweaks[action.set_tweak] = action.value;
|
375 | }
|
376 | }
|
377 | return actionobj;
|
378 | };
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 | module.exports = PushProcessor;
|
393 |
|