summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--synapse/push/__init__.py91
-rw-r--r--synapse/push/httppusher.py9
-rw-r--r--synapse/rest/client/v1/push_rule.py43
3 files changed, 117 insertions, 26 deletions
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