/** * @module pushprocessor */ /** * Construct a Push Processor. * @constructor * @param {Object} client The Matrix client object to use */ function PushProcessor(client) { var escapeRegExp = function(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }; var matchingRuleFromKindSet = function(ev, kindset, device) { var rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride']; for (var ruleKindIndex = 0; ruleKindIndex < rulekinds_in_order.length; ++ruleKindIndex) { var kind = rulekinds_in_order[ruleKindIndex]; var ruleset = kindset[kind]; for (var ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) { var rule = ruleset[ruleIndex]; if (!rule.enabled) { continue; } var rawrule = templateRuleToRaw(kind, rule, device); if (!rawrule) { continue; } if (ruleMatchesEvent(rawrule, ev)) { rule.kind = kind; return rule; } } } return null; }; var templateRuleToRaw = function(kind, tprule, device) { var rawrule = { 'rule_id': tprule.rule_id, 'actions': tprule.actions, 'conditions': [] }; switch (kind) { case 'underride': case 'override': rawrule.conditions = tprule.conditions; break; case 'room': if (!tprule.rule_id) { return null; } rawrule.conditions.push({ 'kind': 'event_match', 'key': 'room_id', 'pattern': tprule.rule_id }); break; case 'sender': if (!tprule.rule_id) { return null; } rawrule.conditions.push({ 'kind': 'event_match', 'key': 'user_id', 'pattern': tprule.rule_id }); break; case 'content': if (!tprule.pattern) { return null; } rawrule.conditions.push({ 'kind': 'event_match', 'key': 'content.body', 'pattern': tprule.pattern }); break; } if (device) { rawrule.conditions.push({ 'kind': 'device', 'profile_tag': device }); } return rawrule; }; var ruleMatchesEvent = function(rule, ev) { var ret = true; for (var i = 0; i < rule.conditions.length; ++i) { var cond = rule.conditions[i]; ret &= eventFulfillsCondition(cond, ev); } //console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match")); return ret; }; var eventFulfillsCondition = function(cond, ev) { var condition_functions = { "event_match": eventFulfillsEventMatchCondition, "device": eventFulfillsDeviceCondition, "contains_display_name": eventFulfillsDisplayNameCondition, "room_member_count": eventFulfillsRoomMemberCountCondition }; if (condition_functions[cond.kind]) { return condition_functions[cond.kind](cond, ev); } return true; }; var eventFulfillsRoomMemberCountCondition = function(cond, ev) { if (!cond.is) { return false; } var room = client.getRoom(ev.room_id); if (!room || !room.currentState || !room.currentState.members) { return false; } var memberCount = Object.keys(room.currentState.members).length; var m = cond.is.match(/^([=<>]*)([0-9]*)$/); if (!m) { return false; } var ineq = m[1]; var rhs = parseInt(m[2]); if (isNaN(rhs)) { return false; } switch (ineq) { case '': case '==': return memberCount == rhs; case '<': return memberCount < rhs; case '>': return memberCount > rhs; case '<=': return memberCount <= rhs; case '>=': return memberCount >= rhs; default: return false; } }; var eventFulfillsDisplayNameCondition = function(cond, ev) { if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') { return false; } var room = client.getRoom(ev.room_id); if (!room || !room.currentState || !room.currentState.members || !room.currentState.getMember(client.credentials.userId)) { return false; } var displayName = room.currentState.getMember(client.credentials.userId).name; var pat = new RegExp("\\b" + escapeRegExp(displayName) + "\\b", 'i'); return ev.content.body.search(pat) > -1; }; var eventFulfillsDeviceCondition = function(cond, ev) { return false; // XXX: Allow a profile tag to be set for the web client instance }; var eventFulfillsEventMatchCondition = function(cond, ev) { var val = valueForDottedKey(cond.key, ev); if (!val || typeof val != 'string') { return false; } var pat; if (cond.key == 'content.body') { pat = '\\b' + globToRegexp(cond.pattern) + '\\b'; } else { pat = '^' + globToRegexp(cond.pattern) + '$'; } var regex = new RegExp(pat, 'i'); return !!val.match(regex); }; var globToRegexp = function(glob) { // From // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 // Because micromatch is about 130KB with dependencies, // and minimatch is not much better. var pat = escapeRegExp(glob); pat = pat.replace(/\\\*/, '.*'); pat = pat.replace(/\?/, '.'); pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) { var first = p1 && '^' || ''; var second = p2.replace(/\\\-/, '-'); return '[' + first + second + ']'; }); return pat; }; var valueForDottedKey = function(key, ev) { var parts = key.split('.'); var val = ev; while (parts.length > 0) { var thispart = parts.shift(); if (!val[thispart]) { return null; } val = val[thispart]; } return val; }; var matchingRuleForEventWithRulesets = function(ev, rulesets) { if (!rulesets || !rulesets.device) { return null; } if (ev.user_id == client.credentials.userId) { return null; } var allDevNames = Object.keys(rulesets.device); for (var i = 0; i < allDevNames.length; ++i) { var devname = allDevNames[i]; var devrules = rulesets.device[devname]; var matchingRule = matchingRuleFromKindSet(devrules, devname); if (matchingRule) { return matchingRule; } } return matchingRuleFromKindSet(ev, rulesets.global); }; var actionListToActionsObject = function(actionlist) { var actionobj = { 'notify': false, 'tweaks': {} }; for (var i = 0; i < actionlist.length; ++i) { var action = actionlist[i]; if (action === 'notify') { actionobj.notify = true; } else if (typeof action === 'object') { if (action.value === undefined) { action.value = true; } actionobj.tweaks[action.set_tweak] = action.value; } } return actionobj; }; var pushActionsForEventAndRulesets = function(ev, rulesets) { var rule = matchingRuleForEventWithRulesets(ev, rulesets); if (!rule) { return {}; } var actionObj = actionListToActionsObject(rule.actions); // Some actions are implicit in some situations: we add those here if (actionObj.tweaks.highlight === undefined) { // if it isn't specified, highlight if it's a content // rule but otherwise not actionObj.tweaks.highlight = (rule.kind == 'content'); } return actionObj; }; this.actionsForEvent = function(ev) { return pushActionsForEventAndRulesets(ev, client.pushRules); }; } /** * @typedef {Object} PushAction * @type {Object} * @property {boolean} notify Whether this event should notify the user or not. * @property {Object} tweaks How this event should be notified. * @property {boolean} tweaks.highlight Whether this event should be highlighted * on the UI. * @property {boolean} tweaks.sound Whether this notification should produce a * noise. */ /** The PushProcessor class. */ module.exports = PushProcessor;