You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-19 16:42:09 +03:00
260 lines
8.7 KiB
JavaScript
260 lines
8.7 KiB
JavaScript
/**
|
|
* @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;
|