diff --git a/lib/models/event.js b/lib/models/event.js index 14e6c0f14..e9feb6445 100644 --- a/lib/models/event.js +++ b/lib/models/event.js @@ -1,3 +1,5 @@ +var PushProcessor = require('../pushprocessor'); + "use strict"; /** * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for @@ -142,5 +144,13 @@ module.exports.MatrixEvent.prototype = { */ isState: function() { return this.event.state_key !== undefined; + }, + + getPushActions(client) { + if (this.pushActions === undefined) { + var pushProcessor = new PushProcessor(client); + this.pushActions = pushProcessor.actionsForEvent(this.event); + } + return this.pushActions; } }; diff --git a/lib/pushprocessor.js b/lib/pushprocessor.js new file mode 100644 index 000000000..1abfc9e16 --- /dev/null +++ b/lib/pushprocessor.js @@ -0,0 +1,216 @@ +var micromatch = require("micromatch"); + +module.exports = function(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); + 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; } + + // Supportting ! in globs would mean figuring out when we don't want to use things as a regex, like room IDs + var pat = cond.pattern.replace("!", "\\!"); + + if (cond.key == 'content.body') { + return micromatch.contains(val, pat); + } else { + return micromatch.isMatch(val, 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) { 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); + }; +} + diff --git a/package.json b/package.json index a1b239391..bedfe0ece 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "browser-request": "^0.3.3", "browserify": "^10.2.3", + "micromatch": "^2.1.6", "q": "^1.4.1", "request": "^2.53.0" },