diff --git a/synapse/push/__init__.py b/synapse/push/__init__.py
index 47da31e500..53d3319699 100644
--- a/synapse/push/__init__.py
+++ b/synapse/push/__init__.py
@@ -21,6 +21,8 @@ from synapse.types import StreamToken
import synapse.util.async
import logging
+import fnmatch
+import json
logger = logging.getLogger(__name__)
@@ -29,6 +31,7 @@ class Pusher(object):
INITIAL_BACKOFF = 1000
MAX_BACKOFF = 60 * 60 * 1000
GIVE_UP_AFTER = 24 * 60 * 60 * 1000
+ DEFAULT_ACTIONS = ['notify']
def __init__(self, _hs, instance_handle, user_name, app_id,
app_display_name, device_display_name, pushkey, pushkey_ts,
@@ -37,7 +40,7 @@ class Pusher(object):
self.evStreamHandler = self.hs.get_handlers().event_stream_handler
self.store = self.hs.get_datastore()
self.clock = self.hs.get_clock()
- self.instance_handle = instance_handle,
+ self.instance_handle = instance_handle
self.user_name = user_name
self.app_id = app_id
self.app_display_name = app_display_name
@@ -51,7 +54,8 @@ class Pusher(object):
self.failing_since = failing_since
self.alive = True
- def _should_notify_for_event(self, ev):
+ @defer.inlineCallbacks
+ def _actions_for_event(self, ev):
"""
This should take into account notification settings that the user
has configured both globally and per-room when we have the ability
@@ -59,8 +63,47 @@ class Pusher(object):
"""
if ev['user_id'] == self.user_name:
# let's assume you probably know about messages you sent yourself
+ defer.returnValue(['dont_notify'])
+
+ rules = yield self.store.get_push_rules_for_user_name(self.user_name)
+
+ for r in rules:
+ matches = True
+
+ conditions = json.loads(r['conditions'])
+ actions = json.loads(r['actions'])
+
+ for c in conditions:
+ matches &= self._event_fulfills_condition(ev, c)
+ # ignore rules with no actions (we have an explict 'dont_notify'
+ if len(actions) == 0:
+ logger.warn(
+ "Ignoring rule id %s with no actions for user %s" %
+ (r['rule_id'], r['user_name'])
+ )
+ continue
+ if matches:
+ defer.returnValue(actions)
+
+ defer.returnValue(Pusher.DEFAULT_ACTIONS)
+
+ def _event_fulfills_condition(self, ev, condition):
+ if condition['kind'] == 'event_match':
+ if 'pattern' not in condition:
+ logger.warn("event_match condition with no pattern")
+ return False
+ pat = condition['pattern']
+
+ val = _value_for_dotted_key(condition['key'], ev)
+ if fnmatch.fnmatch(val, pat):
+ return True
return False
- return True
+ elif condition['kind'] == 'device':
+ if 'instance_handle' not in condition:
+ return True
+ return condition['instance_handle'] == self.instance_handle
+ else:
+ return True
@defer.inlineCallbacks
def get_context_for_event(self, ev):
@@ -113,8 +156,23 @@ class Pusher(object):
continue
processed = False
- if self._should_notify_for_event(single_event):
- rejected = yield self.dispatch_push(single_event)
+ actions = yield self._actions_for_event(single_event)
+ tweaks = _tweaks_for_actions(actions)
+
+ if len(actions) == 0:
+ logger.warn("Empty actions! Using default action.")
+ actions = Pusher.DEFAULT_ACTIONS
+ if 'notify' not in actions and 'dont_notify' not in actions:
+ logger.warn("Neither notify nor dont_notify in actions: adding default")
+ actions.extend(Pusher.DEFAULT_ACTIONS)
+ if 'dont_notify' in actions:
+ logger.debug(
+ "%s for %s: dont_notify",
+ single_event['event_id'], self.user_name
+ )
+ processed = True
+ else:
+ rejected = yield self.dispatch_push(single_event, tweaks)
if not rejected is False:
processed = True
for pk in rejected:
@@ -133,8 +191,6 @@ class Pusher(object):
yield self.hs.get_pusherpool().remove_pusher(
self.app_id, pk
)
- else:
- processed = True
if not self.alive:
continue
@@ -202,7 +258,7 @@ class Pusher(object):
def stop(self):
self.alive = False
- def dispatch_push(self, p):
+ def dispatch_push(self, p, tweaks):
"""
Overridden by implementing classes to actually deliver the notification
:param p: The event to notify for as a single event from the event stream
@@ -214,6 +270,25 @@ class Pusher(object):
pass
+def _value_for_dotted_key(dotted_key, event):
+ parts = dotted_key.split(".")
+ val = event
+ while len(parts) > 0:
+ if parts[0] not in val:
+ return None
+ val = val[parts[0]]
+ parts = parts[1:]
+ return val
+
+def _tweaks_for_actions(actions):
+ tweaks = {}
+ for a in actions:
+ if not isinstance(a, dict):
+ continue
+ if 'set_sound' in a:
+ tweaks['sound'] = a['set_sound']
+ return tweaks
+
class PusherConfigException(Exception):
def __init__(self, msg):
super(PusherConfigException, self).__init__(msg)
\ No newline at end of file
diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py
index 46433ad4a9..25db1dded5 100644
--- a/synapse/push/httppusher.py
+++ b/synapse/push/httppusher.py
@@ -52,7 +52,7 @@ class HttpPusher(Pusher):
del self.data_minus_url['url']
@defer.inlineCallbacks
- def _build_notification_dict(self, event):
+ def _build_notification_dict(self, event, tweaks):
# we probably do not want to push for every presence update
# (we may want to be able to set up notifications when specific
# people sign in, but we'd want to only deliver the pertinent ones)
@@ -83,7 +83,8 @@ class HttpPusher(Pusher):
'app_id': self.app_id,
'pushkey': self.pushkey,
'pushkey_ts': long(self.pushkey_ts / 1000),
- 'data': self.data_minus_url
+ 'data': self.data_minus_url,
+ 'tweaks': tweaks
}
]
}
@@ -97,8 +98,8 @@ class HttpPusher(Pusher):
defer.returnValue(d)
@defer.inlineCallbacks
- def dispatch_push(self, event):
- notification_dict = yield self._build_notification_dict(event)
+ def dispatch_push(self, event, tweaks):
+ notification_dict = yield self._build_notification_dict(event, tweaks)
if not notification_dict:
defer.returnValue([])
try:
diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py
index ce2f0febf4..9dc2c0e11e 100644
--- a/synapse/rest/client/v1/push_rule.py
+++ b/synapse/rest/client/v1/push_rule.py
@@ -96,10 +96,15 @@ class PushRuleRestServlet(RestServlet):
elif rule_template == 'content':
if 'pattern' not in req_obj:
raise InvalidRuleException("Content rule missing 'pattern'")
+ pat = req_obj['pattern']
+ if pat.strip("*?[]") == pat:
+ # no special glob characters so we assume the user means
+ # 'contains this string' rather than 'is this string'
+ pat = "*%s*" % (pat)
conditions = [{
'kind': 'event_match',
'key': 'content.body',
- 'pattern': req_obj['pattern']
+ 'pattern': pat
}]
else:
raise InvalidRuleException("Unknown rule template: %s" % (rule_template,))
@@ -115,7 +120,7 @@ class PushRuleRestServlet(RestServlet):
actions = req_obj['actions']
for a in actions:
- if a in ['notify', 'dont-notify', 'coalesce']:
+ if a in ['notify', 'dont_notify', 'coalesce']:
pass
elif isinstance(a, dict) and 'set_sound' in a:
pass
@@ -124,21 +129,11 @@ class PushRuleRestServlet(RestServlet):
return conditions, actions
- def priority_class_from_spec(self, spec):
- if spec['template'] not in PushRuleRestServlet.PRIORITY_CLASS_MAP.keys():
- raise InvalidRuleException("Unknown template: %s" % (spec['kind']))
- pc = PushRuleRestServlet.PRIORITY_CLASS_MAP[spec['template']]
-
- if spec['scope'] == 'device':
- pc += 5
-
- return pc
-
@defer.inlineCallbacks
def on_PUT(self, request):
spec = self.rule_spec_from_path(request.postpath)
try:
- priority_class = self.priority_class_from_spec(spec)
+ priority_class = _priority_class_from_spec(spec)
except InvalidRuleException as e:
raise SynapseError(400, e.message)
@@ -204,6 +199,7 @@ class PushRuleRestServlet(RestServlet):
if r['priority_class'] > PushRuleRestServlet.PRIORITY_CLASS_MAP['override']:
# per-device rule
instance_handle = _instance_handle_from_conditions(r["conditions"])
+ r = _strip_device_condition(r)
if not instance_handle:
continue
if instance_handle not in rules['device']:
@@ -239,6 +235,7 @@ class PushRuleRestServlet(RestServlet):
defer.returnValue((200, rules['device']))
instance_handle = path[0]
+ path = path[1:]
if instance_handle not in rules['device']:
ret = {}
ret = _add_empty_priority_class_arrays(ret)
@@ -290,10 +287,21 @@ def _filter_ruleset_with_path(ruleset, path):
raise NotFoundError
+def _priority_class_from_spec(spec):
+ if spec['template'] not in PushRuleRestServlet.PRIORITY_CLASS_MAP.keys():
+ raise InvalidRuleException("Unknown template: %s" % (spec['kind']))
+ pc = PushRuleRestServlet.PRIORITY_CLASS_MAP[spec['template']]
+
+ if spec['scope'] == 'device':
+ pc += len(PushRuleRestServlet.PRIORITY_CLASS_MAP)
+
+ return pc
+
+
def _priority_class_to_template_name(pc):
if pc > PushRuleRestServlet.PRIORITY_CLASS_MAP['override']:
# per-device
- prio_class_index = pc - PushRuleRestServlet.PRIORITY_CLASS_MAP['override']
+ prio_class_index = pc - len(PushRuleRestServlet.PRIORITY_CLASS_MAP)
return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[prio_class_index]
else:
return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[pc]
@@ -316,6 +324,13 @@ def _rule_to_template(rule):
return ret
+def _strip_device_condition(rule):
+ for i,c in enumerate(rule['conditions']):
+ if c['kind'] == 'device':
+ del rule['conditions'][i]
+ return rule
+
+
class InvalidRuleException(Exception):
pass
|