Merge pull request #96 from matrix-org/pushrules2
Evolution of push rules
This commit is contained in:
commit
b102a87348
|
@ -32,7 +32,7 @@ class Pusher(object):
|
||||||
INITIAL_BACKOFF = 1000
|
INITIAL_BACKOFF = 1000
|
||||||
MAX_BACKOFF = 60 * 60 * 1000
|
MAX_BACKOFF = 60 * 60 * 1000
|
||||||
GIVE_UP_AFTER = 24 * 60 * 60 * 1000
|
GIVE_UP_AFTER = 24 * 60 * 60 * 1000
|
||||||
DEFAULT_ACTIONS = ['notify']
|
DEFAULT_ACTIONS = ['dont-notify']
|
||||||
|
|
||||||
INEQUALITY_EXPR = re.compile("^([=<>]*)([0-9]*)$")
|
INEQUALITY_EXPR = re.compile("^([=<>]*)([0-9]*)$")
|
||||||
|
|
||||||
|
@ -72,16 +72,14 @@ class Pusher(object):
|
||||||
# let's assume you probably know about messages you sent yourself
|
# let's assume you probably know about messages you sent yourself
|
||||||
defer.returnValue(['dont_notify'])
|
defer.returnValue(['dont_notify'])
|
||||||
|
|
||||||
if ev['type'] == 'm.room.member':
|
rawrules = yield self.store.get_push_rules_for_user(self.user_name)
|
||||||
if ev['state_key'] != self.user_name:
|
|
||||||
defer.returnValue(['dont_notify'])
|
|
||||||
|
|
||||||
rawrules = yield self.store.get_push_rules_for_user_name(self.user_name)
|
|
||||||
|
|
||||||
for r in rawrules:
|
for r in rawrules:
|
||||||
r['conditions'] = json.loads(r['conditions'])
|
r['conditions'] = json.loads(r['conditions'])
|
||||||
r['actions'] = json.loads(r['actions'])
|
r['actions'] = json.loads(r['actions'])
|
||||||
|
|
||||||
|
enabled_map = yield self.store.get_push_rules_enabled_for_user(self.user_name)
|
||||||
|
|
||||||
user = UserID.from_string(self.user_name)
|
user = UserID.from_string(self.user_name)
|
||||||
|
|
||||||
rules = baserules.list_with_base_rules(rawrules, user)
|
rules = baserules.list_with_base_rules(rawrules, user)
|
||||||
|
@ -107,6 +105,8 @@ class Pusher(object):
|
||||||
room_member_count += 1
|
room_member_count += 1
|
||||||
|
|
||||||
for r in rules:
|
for r in rules:
|
||||||
|
if r['rule_id'] in enabled_map and not enabled_map[r['rule_id']]:
|
||||||
|
continue
|
||||||
matches = True
|
matches = True
|
||||||
|
|
||||||
conditions = r['conditions']
|
conditions = r['conditions']
|
||||||
|
@ -117,7 +117,11 @@ class Pusher(object):
|
||||||
ev, c, display_name=my_display_name,
|
ev, c, display_name=my_display_name,
|
||||||
room_member_count=room_member_count
|
room_member_count=room_member_count
|
||||||
)
|
)
|
||||||
# ignore rules with no actions (we have an explict 'dont_notify'
|
logger.debug(
|
||||||
|
"Rule %s %s",
|
||||||
|
r['rule_id'], "matches" if matches else "doesn't match"
|
||||||
|
)
|
||||||
|
# ignore rules with no actions (we have an explict 'dont_notify')
|
||||||
if len(actions) == 0:
|
if len(actions) == 0:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Ignoring rule id %s with no actions for user %s" %
|
"Ignoring rule id %s with no actions for user %s" %
|
||||||
|
|
|
@ -32,12 +32,14 @@ def make_base_rules(user, kind):
|
||||||
|
|
||||||
if kind == 'override':
|
if kind == 'override':
|
||||||
rules = make_base_override_rules()
|
rules = make_base_override_rules()
|
||||||
|
elif kind == 'underride':
|
||||||
|
rules = make_base_underride_rules(user)
|
||||||
elif kind == 'content':
|
elif kind == 'content':
|
||||||
rules = make_base_content_rules(user)
|
rules = make_base_content_rules(user)
|
||||||
|
|
||||||
for r in rules:
|
for r in rules:
|
||||||
r['priority_class'] = PRIORITY_CLASS_MAP[kind]
|
r['priority_class'] = PRIORITY_CLASS_MAP[kind]
|
||||||
r['default'] = True
|
r['default'] = True # Deprecated, left for backwards compat
|
||||||
|
|
||||||
return rules
|
return rules
|
||||||
|
|
||||||
|
@ -45,6 +47,7 @@ def make_base_rules(user, kind):
|
||||||
def make_base_content_rules(user):
|
def make_base_content_rules(user):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
'rule_id': 'global/content/.m.rule.contains_user_name',
|
||||||
'conditions': [
|
'conditions': [
|
||||||
{
|
{
|
||||||
'kind': 'event_match',
|
'kind': 'event_match',
|
||||||
|
@ -57,6 +60,8 @@ def make_base_content_rules(user):
|
||||||
{
|
{
|
||||||
'set_tweak': 'sound',
|
'set_tweak': 'sound',
|
||||||
'value': 'default',
|
'value': 'default',
|
||||||
|
}, {
|
||||||
|
'set_tweak': 'highlight'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -66,6 +71,20 @@ def make_base_content_rules(user):
|
||||||
def make_base_override_rules():
|
def make_base_override_rules():
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
'rule_id': 'global/underride/.m.rule.suppress_notices',
|
||||||
|
'conditions': [
|
||||||
|
{
|
||||||
|
'kind': 'event_match',
|
||||||
|
'key': 'content.msgtype',
|
||||||
|
'pattern': 'm.notice',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'actions': [
|
||||||
|
'dont-notify',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'rule_id': 'global/override/.m.rule.contains_display_name',
|
||||||
'conditions': [
|
'conditions': [
|
||||||
{
|
{
|
||||||
'kind': 'contains_display_name'
|
'kind': 'contains_display_name'
|
||||||
|
@ -76,10 +95,13 @@ def make_base_override_rules():
|
||||||
{
|
{
|
||||||
'set_tweak': 'sound',
|
'set_tweak': 'sound',
|
||||||
'value': 'default'
|
'value': 'default'
|
||||||
|
}, {
|
||||||
|
'set_tweak': 'highlight'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
'rule_id': 'global/override/.m.rule.room_one_to_one',
|
||||||
'conditions': [
|
'conditions': [
|
||||||
{
|
{
|
||||||
'kind': 'room_member_count',
|
'kind': 'room_member_count',
|
||||||
|
@ -95,3 +117,86 @@ def make_base_override_rules():
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_base_underride_rules(user):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'rule_id': 'global/underride/.m.rule.invite_for_me',
|
||||||
|
'conditions': [
|
||||||
|
{
|
||||||
|
'kind': 'event_match',
|
||||||
|
'key': 'type',
|
||||||
|
'pattern': 'm.room.member',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'kind': 'event_match',
|
||||||
|
'key': 'content.membership',
|
||||||
|
'pattern': 'invite',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'kind': 'event_match',
|
||||||
|
'key': 'state_key',
|
||||||
|
'pattern': user.to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'actions': [
|
||||||
|
'notify',
|
||||||
|
{
|
||||||
|
'set_tweak': 'sound',
|
||||||
|
'value': 'default'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'rule_id': 'global/underride/.m.rule.member_event',
|
||||||
|
'conditions': [
|
||||||
|
{
|
||||||
|
'kind': 'event_match',
|
||||||
|
'key': 'type',
|
||||||
|
'pattern': 'm.room.member',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'actions': [
|
||||||
|
'notify',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'rule_id': 'global/underride/.m.rule.message',
|
||||||
|
'conditions': [
|
||||||
|
{
|
||||||
|
'kind': 'event_match',
|
||||||
|
'key': 'type',
|
||||||
|
'pattern': 'm.room.message',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'actions': [
|
||||||
|
'notify',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'rule_id': 'global/underride/.m.rule.call',
|
||||||
|
'conditions': [
|
||||||
|
{
|
||||||
|
'kind': 'event_match',
|
||||||
|
'key': 'type',
|
||||||
|
'pattern': 'm.call.invite',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'actions': [
|
||||||
|
'notify',
|
||||||
|
{
|
||||||
|
'set_tweak': 'sound',
|
||||||
|
'value': 'ring'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'rule_id': 'global/underride/.m.rule.fallback',
|
||||||
|
'conditions': [
|
||||||
|
],
|
||||||
|
'actions': [
|
||||||
|
'notify',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
|
@ -50,6 +50,10 @@ class PushRuleRestServlet(ClientV1RestServlet):
|
||||||
|
|
||||||
content = _parse_json(request)
|
content = _parse_json(request)
|
||||||
|
|
||||||
|
if 'attr' in spec:
|
||||||
|
self.set_rule_attr(user.to_string(), spec, content)
|
||||||
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(conditions, actions) = _rule_tuple_from_request_object(
|
(conditions, actions) = _rule_tuple_from_request_object(
|
||||||
spec['template'],
|
spec['template'],
|
||||||
|
@ -110,7 +114,7 @@ class PushRuleRestServlet(ClientV1RestServlet):
|
||||||
# we build up the full structure and then decide which bits of it
|
# we build up the full structure and then decide which bits of it
|
||||||
# to send which means doing unnecessary work sometimes but is
|
# to send which means doing unnecessary work sometimes but is
|
||||||
# is probably not going to make a whole lot of difference
|
# is probably not going to make a whole lot of difference
|
||||||
rawrules = yield self.hs.get_datastore().get_push_rules_for_user_name(
|
rawrules = yield self.hs.get_datastore().get_push_rules_for_user(
|
||||||
user.to_string()
|
user.to_string()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -124,6 +128,9 @@ class PushRuleRestServlet(ClientV1RestServlet):
|
||||||
|
|
||||||
rules['global'] = _add_empty_priority_class_arrays(rules['global'])
|
rules['global'] = _add_empty_priority_class_arrays(rules['global'])
|
||||||
|
|
||||||
|
enabled_map = yield self.hs.get_datastore().\
|
||||||
|
get_push_rules_enabled_for_user(user.to_string())
|
||||||
|
|
||||||
for r in ruleslist:
|
for r in ruleslist:
|
||||||
rulearray = None
|
rulearray = None
|
||||||
|
|
||||||
|
@ -149,6 +156,9 @@ class PushRuleRestServlet(ClientV1RestServlet):
|
||||||
|
|
||||||
template_rule = _rule_to_template(r)
|
template_rule = _rule_to_template(r)
|
||||||
if template_rule:
|
if template_rule:
|
||||||
|
template_rule['enabled'] = True
|
||||||
|
if r['rule_id'] in enabled_map:
|
||||||
|
template_rule['enabled'] = enabled_map[r['rule_id']]
|
||||||
rulearray.append(template_rule)
|
rulearray.append(template_rule)
|
||||||
|
|
||||||
path = request.postpath[1:]
|
path = request.postpath[1:]
|
||||||
|
@ -189,6 +199,25 @@ class PushRuleRestServlet(ClientV1RestServlet):
|
||||||
def on_OPTIONS(self, _):
|
def on_OPTIONS(self, _):
|
||||||
return 200, {}
|
return 200, {}
|
||||||
|
|
||||||
|
def set_rule_attr(self, user_name, spec, val):
|
||||||
|
if spec['attr'] == 'enabled':
|
||||||
|
if not isinstance(val, bool):
|
||||||
|
raise SynapseError(400, "Value for 'enabled' must be boolean")
|
||||||
|
namespaced_rule_id = _namespaced_rule_id_from_spec(spec)
|
||||||
|
self.hs.get_datastore().set_push_rule_enabled(
|
||||||
|
user_name, namespaced_rule_id, val
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise UnrecognizedRequestError()
|
||||||
|
|
||||||
|
def get_rule_attr(self, user_name, namespaced_rule_id, attr):
|
||||||
|
if attr == 'enabled':
|
||||||
|
return self.hs.get_datastore().get_push_rule_enabled_by_user_rule_id(
|
||||||
|
user_name, namespaced_rule_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise UnrecognizedRequestError()
|
||||||
|
|
||||||
|
|
||||||
def _rule_spec_from_path(path):
|
def _rule_spec_from_path(path):
|
||||||
if len(path) < 2:
|
if len(path) < 2:
|
||||||
|
@ -226,6 +255,12 @@ def _rule_spec_from_path(path):
|
||||||
}
|
}
|
||||||
if device:
|
if device:
|
||||||
spec['profile_tag'] = device
|
spec['profile_tag'] = device
|
||||||
|
|
||||||
|
path = path[1:]
|
||||||
|
|
||||||
|
if len(path) > 0 and len(path[0]) > 0:
|
||||||
|
spec['attr'] = path[0]
|
||||||
|
|
||||||
return spec
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
@ -275,7 +310,7 @@ def _rule_tuple_from_request_object(rule_template, rule_id, req_obj, device=None
|
||||||
for a in actions:
|
for a in actions:
|
||||||
if a in ['notify', 'dont_notify', 'coalesce']:
|
if a in ['notify', 'dont_notify', 'coalesce']:
|
||||||
pass
|
pass
|
||||||
elif isinstance(a, dict) and 'set_sound' in a:
|
elif isinstance(a, dict) and 'set_tweak' in a:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise InvalidRuleException("Unrecognised action")
|
raise InvalidRuleException("Unrecognised action")
|
||||||
|
@ -319,10 +354,23 @@ def _filter_ruleset_with_path(ruleset, path):
|
||||||
if path[0] == '':
|
if path[0] == '':
|
||||||
return ruleset[template_kind]
|
return ruleset[template_kind]
|
||||||
rule_id = path[0]
|
rule_id = path[0]
|
||||||
|
|
||||||
|
the_rule = None
|
||||||
for r in ruleset[template_kind]:
|
for r in ruleset[template_kind]:
|
||||||
if r['rule_id'] == rule_id:
|
if r['rule_id'] == rule_id:
|
||||||
return r
|
the_rule = r
|
||||||
raise NotFoundError
|
if the_rule is None:
|
||||||
|
raise NotFoundError
|
||||||
|
|
||||||
|
path = path[1:]
|
||||||
|
if len(path) == 0:
|
||||||
|
return the_rule
|
||||||
|
|
||||||
|
attr = path[0]
|
||||||
|
if attr in the_rule:
|
||||||
|
return the_rule[attr]
|
||||||
|
else:
|
||||||
|
raise UnrecognizedRequestError()
|
||||||
|
|
||||||
|
|
||||||
def _priority_class_from_spec(spec):
|
def _priority_class_from_spec(spec):
|
||||||
|
@ -339,7 +387,7 @@ def _priority_class_from_spec(spec):
|
||||||
def _priority_class_to_template_name(pc):
|
def _priority_class_to_template_name(pc):
|
||||||
if pc > PRIORITY_CLASS_MAP['override']:
|
if pc > PRIORITY_CLASS_MAP['override']:
|
||||||
# per-device
|
# per-device
|
||||||
prio_class_index = pc - len(PushRuleRestServlet.PRIORITY_CLASS_MAP)
|
prio_class_index = pc - len(PRIORITY_CLASS_MAP)
|
||||||
return PRIORITY_CLASS_INVERSE_MAP[prio_class_index]
|
return PRIORITY_CLASS_INVERSE_MAP[prio_class_index]
|
||||||
else:
|
else:
|
||||||
return PRIORITY_CLASS_INVERSE_MAP[pc]
|
return PRIORITY_CLASS_INVERSE_MAP[pc]
|
||||||
|
@ -399,9 +447,6 @@ class InvalidRuleException(Exception):
|
||||||
def _parse_json(request):
|
def _parse_json(request):
|
||||||
try:
|
try:
|
||||||
content = json.loads(request.content.read())
|
content = json.loads(request.content.read())
|
||||||
if type(content) != dict:
|
|
||||||
raise SynapseError(400, "Content must be a JSON object.",
|
|
||||||
errcode=Codes.NOT_JSON)
|
|
||||||
return content
|
return content
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
|
raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
|
||||||
|
|
|
@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class PushRuleStore(SQLBaseStore):
|
class PushRuleStore(SQLBaseStore):
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_push_rules_for_user_name(self, user_name):
|
def get_push_rules_for_user(self, user_name):
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT "+",".join(PushRuleTable.fields)+" "
|
"SELECT "+",".join(PushRuleTable.fields)+" "
|
||||||
"FROM "+PushRuleTable.table_name+" "
|
"FROM "+PushRuleTable.table_name+" "
|
||||||
|
@ -45,6 +45,28 @@ class PushRuleStore(SQLBaseStore):
|
||||||
|
|
||||||
defer.returnValue(dicts)
|
defer.returnValue(dicts)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_push_rules_enabled_for_user(self, user_name):
|
||||||
|
results = yield self._simple_select_list(
|
||||||
|
PushRuleEnableTable.table_name,
|
||||||
|
{'user_name': user_name},
|
||||||
|
PushRuleEnableTable.fields
|
||||||
|
)
|
||||||
|
defer.returnValue(
|
||||||
|
{r['rule_id']: False if r['enabled'] == 0 else True for r in results}
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_push_rule_enabled_by_user_rule_id(self, user_name, rule_id):
|
||||||
|
results = yield self._simple_select_list(
|
||||||
|
PushRuleEnableTable.table_name,
|
||||||
|
{'user_name': user_name, 'rule_id': rule_id},
|
||||||
|
['enabled']
|
||||||
|
)
|
||||||
|
if not results:
|
||||||
|
defer.returnValue(True)
|
||||||
|
defer.returnValue(results[0])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def add_push_rule(self, before, after, **kwargs):
|
def add_push_rule(self, before, after, **kwargs):
|
||||||
vals = copy.copy(kwargs)
|
vals = copy.copy(kwargs)
|
||||||
|
@ -193,6 +215,20 @@ class PushRuleStore(SQLBaseStore):
|
||||||
{'user_name': user_name, 'rule_id': rule_id}
|
{'user_name': user_name, 'rule_id': rule_id}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def set_push_rule_enabled(self, user_name, rule_id, enabled):
|
||||||
|
if enabled:
|
||||||
|
yield self._simple_delete_one(
|
||||||
|
PushRuleEnableTable.table_name,
|
||||||
|
{'user_name': user_name, 'rule_id': rule_id}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
yield self._simple_upsert(
|
||||||
|
PushRuleEnableTable.table_name,
|
||||||
|
{'user_name': user_name, 'rule_id': rule_id},
|
||||||
|
{'enabled': False}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RuleNotFoundException(Exception):
|
class RuleNotFoundException(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -216,3 +252,13 @@ class PushRuleTable(Table):
|
||||||
]
|
]
|
||||||
|
|
||||||
EntryType = collections.namedtuple("PushRuleEntry", fields)
|
EntryType = collections.namedtuple("PushRuleEntry", fields)
|
||||||
|
|
||||||
|
|
||||||
|
class PushRuleEnableTable(Table):
|
||||||
|
table_name = "push_rules_enable"
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
"user_name",
|
||||||
|
"rule_id",
|
||||||
|
"enabled"
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS push_rules_enable (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_name TEXT NOT NULL,
|
||||||
|
rule_id TEXT NOT NULL,
|
||||||
|
enabled TINYINT,
|
||||||
|
UNIQUE(user_name, rule_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS push_rules_enable_user_name on push_rules_enable (user_name);
|
Loading…
Reference in New Issue