Merge pull request #96 from matrix-org/pushrules2

Evolution of push rules
This commit is contained in:
Erik Johnston 2015-03-06 14:20:04 +00:00
commit b102a87348
5 changed files with 226 additions and 17 deletions

View File

@ -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" %

View File

@ -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',
]
},
]

View File

@ -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,11 +354,24 @@ 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
if the_rule is None:
raise NotFoundError 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):
if spec['template'] not in PRIORITY_CLASS_MAP.keys(): if spec['template'] not in PRIORITY_CLASS_MAP.keys():
@ -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)

View File

@ -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"
]

View File

@ -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);