push_rule_evaluator.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015, 2016 OpenMarket Ltd
  3. # Copyright 2017 New Vector Ltd
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import logging
  17. import re
  18. from six import string_types
  19. from synapse.types import UserID
  20. from synapse.util.caches import CACHE_SIZE_FACTOR, register_cache
  21. from synapse.util.caches.lrucache import LruCache
  22. logger = logging.getLogger(__name__)
  23. GLOB_REGEX = re.compile(r'\\\[(\\\!|)(.*)\\\]')
  24. IS_GLOB = re.compile(r'[\?\*\[\]]')
  25. INEQUALITY_EXPR = re.compile("^([=<>]*)([0-9]*)$")
  26. def _room_member_count(ev, condition, room_member_count):
  27. return _test_ineq_condition(condition, room_member_count)
  28. def _sender_notification_permission(ev, condition, sender_power_level, power_levels):
  29. notif_level_key = condition.get('key')
  30. if notif_level_key is None:
  31. return False
  32. notif_levels = power_levels.get('notifications', {})
  33. room_notif_level = notif_levels.get(notif_level_key, 50)
  34. return sender_power_level >= room_notif_level
  35. def _test_ineq_condition(condition, number):
  36. if 'is' not in condition:
  37. return False
  38. m = INEQUALITY_EXPR.match(condition['is'])
  39. if not m:
  40. return False
  41. ineq = m.group(1)
  42. rhs = m.group(2)
  43. if not rhs.isdigit():
  44. return False
  45. rhs = int(rhs)
  46. if ineq == '' or ineq == '==':
  47. return number == rhs
  48. elif ineq == '<':
  49. return number < rhs
  50. elif ineq == '>':
  51. return number > rhs
  52. elif ineq == '>=':
  53. return number >= rhs
  54. elif ineq == '<=':
  55. return number <= rhs
  56. else:
  57. return False
  58. def tweaks_for_actions(actions):
  59. tweaks = {}
  60. for a in actions:
  61. if not isinstance(a, dict):
  62. continue
  63. if 'set_tweak' in a and 'value' in a:
  64. tweaks[a['set_tweak']] = a['value']
  65. return tweaks
  66. class PushRuleEvaluatorForEvent(object):
  67. def __init__(self, event, room_member_count, sender_power_level, power_levels):
  68. self._event = event
  69. self._room_member_count = room_member_count
  70. self._sender_power_level = sender_power_level
  71. self._power_levels = power_levels
  72. # Maps strings of e.g. 'content.body' -> event["content"]["body"]
  73. self._value_cache = _flatten_dict(event)
  74. def matches(self, condition, user_id, display_name):
  75. if condition['kind'] == 'event_match':
  76. return self._event_match(condition, user_id)
  77. elif condition['kind'] == 'contains_display_name':
  78. return self._contains_display_name(display_name)
  79. elif condition['kind'] == 'room_member_count':
  80. return _room_member_count(
  81. self._event, condition, self._room_member_count
  82. )
  83. elif condition['kind'] == 'sender_notification_permission':
  84. return _sender_notification_permission(
  85. self._event, condition, self._sender_power_level, self._power_levels,
  86. )
  87. else:
  88. return True
  89. def _event_match(self, condition, user_id):
  90. pattern = condition.get('pattern', None)
  91. if not pattern:
  92. pattern_type = condition.get('pattern_type', None)
  93. if pattern_type == "user_id":
  94. pattern = user_id
  95. elif pattern_type == "user_localpart":
  96. pattern = UserID.from_string(user_id).localpart
  97. if not pattern:
  98. logger.warn("event_match condition with no pattern")
  99. return False
  100. # XXX: optimisation: cache our pattern regexps
  101. if condition['key'] == 'content.body':
  102. body = self._event.content.get("body", None)
  103. if not body:
  104. return False
  105. return _glob_matches(pattern, body, word_boundary=True)
  106. else:
  107. haystack = self._get_value(condition['key'])
  108. if haystack is None:
  109. return False
  110. return _glob_matches(pattern, haystack)
  111. def _contains_display_name(self, display_name):
  112. if not display_name:
  113. return False
  114. body = self._event.content.get("body", None)
  115. if not body:
  116. return False
  117. return _glob_matches(display_name, body, word_boundary=True)
  118. def _get_value(self, dotted_key):
  119. return self._value_cache.get(dotted_key, None)
  120. # Caches (glob, word_boundary) -> regex for push. See _glob_matches
  121. regex_cache = LruCache(50000 * CACHE_SIZE_FACTOR)
  122. register_cache("cache", "regex_push_cache", regex_cache)
  123. def _glob_matches(glob, value, word_boundary=False):
  124. """Tests if value matches glob.
  125. Args:
  126. glob (string)
  127. value (string): String to test against glob.
  128. word_boundary (bool): Whether to match against word boundaries or entire
  129. string. Defaults to False.
  130. Returns:
  131. bool
  132. """
  133. try:
  134. r = regex_cache.get((glob, word_boundary), None)
  135. if not r:
  136. r = _glob_to_re(glob, word_boundary)
  137. regex_cache[(glob, word_boundary)] = r
  138. return r.search(value)
  139. except re.error:
  140. logger.warn("Failed to parse glob to regex: %r", glob)
  141. return False
  142. def _glob_to_re(glob, word_boundary):
  143. """Generates regex for a given glob.
  144. Args:
  145. glob (string)
  146. word_boundary (bool): Whether to match against word boundaries or entire
  147. string. Defaults to False.
  148. Returns:
  149. regex object
  150. """
  151. if IS_GLOB.search(glob):
  152. r = re.escape(glob)
  153. r = r.replace(r'\*', '.*?')
  154. r = r.replace(r'\?', '.')
  155. # handle [abc], [a-z] and [!a-z] style ranges.
  156. r = GLOB_REGEX.sub(
  157. lambda x: (
  158. '[%s%s]' % (
  159. x.group(1) and '^' or '',
  160. x.group(2).replace(r'\\\-', '-')
  161. )
  162. ),
  163. r,
  164. )
  165. if word_boundary:
  166. r = _re_word_boundary(r)
  167. return re.compile(r, flags=re.IGNORECASE)
  168. else:
  169. r = "^" + r + "$"
  170. return re.compile(r, flags=re.IGNORECASE)
  171. elif word_boundary:
  172. r = re.escape(glob)
  173. r = _re_word_boundary(r)
  174. return re.compile(r, flags=re.IGNORECASE)
  175. else:
  176. r = "^" + re.escape(glob) + "$"
  177. return re.compile(r, flags=re.IGNORECASE)
  178. def _re_word_boundary(r):
  179. """
  180. Adds word boundary characters to the start and end of an
  181. expression to require that the match occur as a whole word,
  182. but do so respecting the fact that strings starting or ending
  183. with non-word characters will change word boundaries.
  184. """
  185. # we can't use \b as it chokes on unicode. however \W seems to be okay
  186. # as shorthand for [^0-9A-Za-z_].
  187. return r"(^|\W)%s(\W|$)" % (r,)
  188. def _flatten_dict(d, prefix=[], result=None):
  189. if result is None:
  190. result = {}
  191. for key, value in d.items():
  192. if isinstance(value, string_types):
  193. result[".".join(prefix + [key])] = value.lower()
  194. elif hasattr(value, "items"):
  195. _flatten_dict(value, prefix=(prefix + [key]), result=result)
  196. return result