summary refs log tree commit diff
path: root/synapse/push/__init__.py
diff options
context:
space:
mode:
authorDavid Baker <dave@matrix.org>2015-12-09 15:51:34 +0000
committerDavid Baker <dave@matrix.org>2015-12-09 15:51:34 +0000
commit4a728beba12c31a67eed2cc24a030add23378cc4 (patch)
treeae639cfb78a76d8bb68329baea89705c5ed46d54 /synapse/push/__init__.py
parentMerge pull request #431 from matrix-org/markjh/filter_inline (diff)
downloadsynapse-4a728beba12c31a67eed2cc24a030add23378cc4.tar.xz
Split out the push rule evaluator into a separate file so it can be more readily reused. Should be functionally identical.
Diffstat (limited to 'synapse/push/__init__.py')
-rw-r--r--synapse/push/__init__.py195
1 files changed, 10 insertions, 185 deletions
diff --git a/synapse/push/__init__.py b/synapse/push/__init__.py
index 0e0c61dec8..070ee6ae9a 100644
--- a/synapse/push/__init__.py
+++ b/synapse/push/__init__.py
@@ -16,14 +16,12 @@
 from twisted.internet import defer
 
 from synapse.streams.config import PaginationConfig
-from synapse.types import StreamToken, UserID
+from synapse.types import StreamToken
 
 import synapse.util.async
-import baserules
+import push_rule_evaluator as push_rule_evaluator
 
 import logging
-import simplejson as json
-import re
 import random
 
 logger = logging.getLogger(__name__)
@@ -33,9 +31,6 @@ class Pusher(object):
     INITIAL_BACKOFF = 1000
     MAX_BACKOFF = 60 * 60 * 1000
     GIVE_UP_AFTER = 24 * 60 * 60 * 1000
-    DEFAULT_ACTIONS = ['dont_notify']
-
-    INEQUALITY_EXPR = re.compile("^([=<>]*)([0-9]*)$")
 
     def __init__(self, _hs, profile_tag, user_name, app_id,
                  app_display_name, device_display_name, pushkey, pushkey_ts,
@@ -63,161 +58,6 @@ class Pusher(object):
         self.has_unread = True
 
     @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
-        to do such things.
-        """
-        if ev['user_id'] == self.user_name:
-            # let's assume you probably know about messages you sent yourself
-            defer.returnValue(['dont_notify'])
-
-        rawrules = yield self.store.get_push_rules_for_user(self.user_name)
-
-        rules = []
-        for rawrule in rawrules:
-            rule = dict(rawrule)
-            rule['conditions'] = json.loads(rawrule['conditions'])
-            rule['actions'] = json.loads(rawrule['actions'])
-            rules.append(rule)
-
-        enabled_map = yield self.store.get_push_rules_enabled_for_user(self.user_name)
-
-        user = UserID.from_string(self.user_name)
-
-        rules = baserules.list_with_base_rules(rules, user)
-
-        room_id = ev['room_id']
-
-        # get *our* member event for display name matching
-        my_display_name = None
-        our_member_event = yield self.store.get_current_state(
-            room_id=room_id,
-            event_type='m.room.member',
-            state_key=self.user_name,
-        )
-        if our_member_event:
-            my_display_name = our_member_event[0].content.get("displayname")
-
-        room_members = yield self.store.get_users_in_room(room_id)
-        room_member_count = len(room_members)
-
-        for r in rules:
-            if r['rule_id'] in enabled_map:
-                r['enabled'] = enabled_map[r['rule_id']]
-            elif 'enabled' not in r:
-                r['enabled'] = True
-            if not r['enabled']:
-                continue
-            matches = True
-
-            conditions = r['conditions']
-            actions = r['actions']
-
-            for c in conditions:
-                matches &= self._event_fulfills_condition(
-                    ev, c, display_name=my_display_name,
-                    room_member_count=room_member_count
-                )
-            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:
-                logger.warn(
-                    "Ignoring rule id %s with no actions for user %s",
-                    r['rule_id'], self.user_name
-                )
-                continue
-            if matches:
-                logger.info(
-                    "%s matches for user %s, event %s",
-                    r['rule_id'], self.user_name, ev['event_id']
-                )
-                defer.returnValue(actions)
-
-        logger.info(
-            "No rules match for user %s, event %s",
-            self.user_name, ev['event_id']
-        )
-        defer.returnValue(Pusher.DEFAULT_ACTIONS)
-
-    @staticmethod
-    def _glob_to_regexp(glob):
-        r = re.escape(glob)
-        r = re.sub(r'\\\*', r'.*?', r)
-        r = re.sub(r'\\\?', r'.', r)
-
-        # handle [abc], [a-z] and [!a-z] style ranges.
-        r = re.sub(r'\\\[(\\\!|)(.*)\\\]',
-                   lambda x: ('[%s%s]' % (x.group(1) and '^' or '',
-                                          re.sub(r'\\\-', '-', x.group(2)))), r)
-        return r
-
-    def _event_fulfills_condition(self, ev, condition, display_name, room_member_count):
-        if condition['kind'] == 'event_match':
-            if 'pattern' not in condition:
-                logger.warn("event_match condition with no pattern")
-                return False
-            # XXX: optimisation: cache our pattern regexps
-            if condition['key'] == 'content.body':
-                r = r'\b%s\b' % self._glob_to_regexp(condition['pattern'])
-            else:
-                r = r'^%s$' % self._glob_to_regexp(condition['pattern'])
-            val = _value_for_dotted_key(condition['key'], ev)
-            if val is None:
-                return False
-            return re.search(r, val, flags=re.IGNORECASE) is not None
-
-        elif condition['kind'] == 'device':
-            if 'profile_tag' not in condition:
-                return True
-            return condition['profile_tag'] == self.profile_tag
-
-        elif condition['kind'] == 'contains_display_name':
-            # This is special because display names can be different
-            # between rooms and so you can't really hard code it in a rule.
-            # Optimisation: we should cache these names and update them from
-            # the event stream.
-            if 'content' not in ev or 'body' not in ev['content']:
-                return False
-            if not display_name:
-                return False
-            return re.search(
-                r"\b%s\b" % re.escape(display_name), ev['content']['body'],
-                flags=re.IGNORECASE
-            ) is not None
-
-        elif condition['kind'] == 'room_member_count':
-            if 'is' not in condition:
-                return False
-            m = Pusher.INEQUALITY_EXPR.match(condition['is'])
-            if not m:
-                return False
-            ineq = m.group(1)
-            rhs = m.group(2)
-            if not rhs.isdigit():
-                return False
-            rhs = int(rhs)
-
-            if ineq == '' or ineq == '==':
-                return room_member_count == rhs
-            elif ineq == '<':
-                return room_member_count < rhs
-            elif ineq == '>':
-                return room_member_count > rhs
-            elif ineq == '>=':
-                return room_member_count >= rhs
-            elif ineq == '<=':
-                return room_member_count <= rhs
-            else:
-                return False
-        else:
-            return True
-
-    @defer.inlineCallbacks
     def get_context_for_event(self, ev):
         name_aliases = yield self.store.get_room_name_and_aliases(
             ev['room_id']
@@ -308,8 +148,14 @@ class Pusher(object):
             return
 
         processed = False
-        actions = yield self._actions_for_event(single_event)
-        tweaks = _tweaks_for_actions(actions)
+
+        rule_evaluator = yield push_rule_evaluator.\
+            evaluator_for_user_name_and_profile_tag(
+            self.user_name, self.profile_tag, single_event['room_id'], self.store
+        )
+
+        actions = yield rule_evaluator.actions_for_event(single_event)
+        tweaks = rule_evaluator.tweaks_for_actions(actions)
 
         if len(actions) == 0:
             logger.warn("Empty actions! Using default action.")
@@ -448,27 +294,6 @@ class Pusher(object):
                     self.has_unread = False
 
 
-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_tweak' in a and 'value' in a:
-            tweaks[a['set_tweak']] = a['value']
-    return tweaks
-
-
 class PusherConfigException(Exception):
     def __init__(self, msg):
         super(PusherConfigException, self).__init__(msg)