mirror of
https://mau.dev/maunium/synapse.git
synced 2024-12-15 10:53:52 +01:00
Use push settings!
This commit is contained in:
parent
fc7a05c443
commit
f21f9fa3c5
3 changed files with 117 additions and 26 deletions
|
@ -21,6 +21,8 @@ from synapse.types import StreamToken
|
||||||
import synapse.util.async
|
import synapse.util.async
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import fnmatch
|
||||||
|
import json
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -29,6 +31,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']
|
||||||
|
|
||||||
def __init__(self, _hs, instance_handle, user_name, app_id,
|
def __init__(self, _hs, instance_handle, user_name, app_id,
|
||||||
app_display_name, device_display_name, pushkey, pushkey_ts,
|
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.evStreamHandler = self.hs.get_handlers().event_stream_handler
|
||||||
self.store = self.hs.get_datastore()
|
self.store = self.hs.get_datastore()
|
||||||
self.clock = self.hs.get_clock()
|
self.clock = self.hs.get_clock()
|
||||||
self.instance_handle = instance_handle,
|
self.instance_handle = instance_handle
|
||||||
self.user_name = user_name
|
self.user_name = user_name
|
||||||
self.app_id = app_id
|
self.app_id = app_id
|
||||||
self.app_display_name = app_display_name
|
self.app_display_name = app_display_name
|
||||||
|
@ -51,7 +54,8 @@ class Pusher(object):
|
||||||
self.failing_since = failing_since
|
self.failing_since = failing_since
|
||||||
self.alive = True
|
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
|
This should take into account notification settings that the user
|
||||||
has configured both globally and per-room when we have the ability
|
has configured both globally and per-room when we have the ability
|
||||||
|
@ -59,7 +63,46 @@ class Pusher(object):
|
||||||
"""
|
"""
|
||||||
if ev['user_id'] == self.user_name:
|
if ev['user_id'] == self.user_name:
|
||||||
# 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'])
|
||||||
|
|
||||||
|
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
|
return False
|
||||||
|
pat = condition['pattern']
|
||||||
|
|
||||||
|
val = _value_for_dotted_key(condition['key'], ev)
|
||||||
|
if fnmatch.fnmatch(val, pat):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
elif condition['kind'] == 'device':
|
||||||
|
if 'instance_handle' not in condition:
|
||||||
|
return True
|
||||||
|
return condition['instance_handle'] == self.instance_handle
|
||||||
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
@ -113,8 +156,23 @@ class Pusher(object):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
processed = False
|
processed = False
|
||||||
if self._should_notify_for_event(single_event):
|
actions = yield self._actions_for_event(single_event)
|
||||||
rejected = yield self.dispatch_push(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:
|
if not rejected is False:
|
||||||
processed = True
|
processed = True
|
||||||
for pk in rejected:
|
for pk in rejected:
|
||||||
|
@ -133,8 +191,6 @@ class Pusher(object):
|
||||||
yield self.hs.get_pusherpool().remove_pusher(
|
yield self.hs.get_pusherpool().remove_pusher(
|
||||||
self.app_id, pk
|
self.app_id, pk
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
processed = True
|
|
||||||
|
|
||||||
if not self.alive:
|
if not self.alive:
|
||||||
continue
|
continue
|
||||||
|
@ -202,7 +258,7 @@ class Pusher(object):
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.alive = False
|
self.alive = False
|
||||||
|
|
||||||
def dispatch_push(self, p):
|
def dispatch_push(self, p, tweaks):
|
||||||
"""
|
"""
|
||||||
Overridden by implementing classes to actually deliver the notification
|
Overridden by implementing classes to actually deliver the notification
|
||||||
:param p: The event to notify for as a single event from the event stream
|
:param p: The event to notify for as a single event from the event stream
|
||||||
|
@ -214,6 +270,25 @@ class Pusher(object):
|
||||||
pass
|
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):
|
class PusherConfigException(Exception):
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
super(PusherConfigException, self).__init__(msg)
|
super(PusherConfigException, self).__init__(msg)
|
|
@ -52,7 +52,7 @@ class HttpPusher(Pusher):
|
||||||
del self.data_minus_url['url']
|
del self.data_minus_url['url']
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@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 probably do not want to push for every presence update
|
||||||
# (we may want to be able to set up notifications when specific
|
# (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)
|
# 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,
|
'app_id': self.app_id,
|
||||||
'pushkey': self.pushkey,
|
'pushkey': self.pushkey,
|
||||||
'pushkey_ts': long(self.pushkey_ts / 1000),
|
'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.returnValue(d)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def dispatch_push(self, event):
|
def dispatch_push(self, event, tweaks):
|
||||||
notification_dict = yield self._build_notification_dict(event)
|
notification_dict = yield self._build_notification_dict(event, tweaks)
|
||||||
if not notification_dict:
|
if not notification_dict:
|
||||||
defer.returnValue([])
|
defer.returnValue([])
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -96,10 +96,15 @@ class PushRuleRestServlet(RestServlet):
|
||||||
elif rule_template == 'content':
|
elif rule_template == 'content':
|
||||||
if 'pattern' not in req_obj:
|
if 'pattern' not in req_obj:
|
||||||
raise InvalidRuleException("Content rule missing 'pattern'")
|
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 = [{
|
conditions = [{
|
||||||
'kind': 'event_match',
|
'kind': 'event_match',
|
||||||
'key': 'content.body',
|
'key': 'content.body',
|
||||||
'pattern': req_obj['pattern']
|
'pattern': pat
|
||||||
}]
|
}]
|
||||||
else:
|
else:
|
||||||
raise InvalidRuleException("Unknown rule template: %s" % (rule_template,))
|
raise InvalidRuleException("Unknown rule template: %s" % (rule_template,))
|
||||||
|
@ -115,7 +120,7 @@ class PushRuleRestServlet(RestServlet):
|
||||||
actions = req_obj['actions']
|
actions = req_obj['actions']
|
||||||
|
|
||||||
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_sound' in a:
|
||||||
pass
|
pass
|
||||||
|
@ -124,21 +129,11 @@ class PushRuleRestServlet(RestServlet):
|
||||||
|
|
||||||
return conditions, actions
|
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
|
@defer.inlineCallbacks
|
||||||
def on_PUT(self, request):
|
def on_PUT(self, request):
|
||||||
spec = self.rule_spec_from_path(request.postpath)
|
spec = self.rule_spec_from_path(request.postpath)
|
||||||
try:
|
try:
|
||||||
priority_class = self.priority_class_from_spec(spec)
|
priority_class = _priority_class_from_spec(spec)
|
||||||
except InvalidRuleException as e:
|
except InvalidRuleException as e:
|
||||||
raise SynapseError(400, e.message)
|
raise SynapseError(400, e.message)
|
||||||
|
|
||||||
|
@ -204,6 +199,7 @@ class PushRuleRestServlet(RestServlet):
|
||||||
if r['priority_class'] > PushRuleRestServlet.PRIORITY_CLASS_MAP['override']:
|
if r['priority_class'] > PushRuleRestServlet.PRIORITY_CLASS_MAP['override']:
|
||||||
# per-device rule
|
# per-device rule
|
||||||
instance_handle = _instance_handle_from_conditions(r["conditions"])
|
instance_handle = _instance_handle_from_conditions(r["conditions"])
|
||||||
|
r = _strip_device_condition(r)
|
||||||
if not instance_handle:
|
if not instance_handle:
|
||||||
continue
|
continue
|
||||||
if instance_handle not in rules['device']:
|
if instance_handle not in rules['device']:
|
||||||
|
@ -239,6 +235,7 @@ class PushRuleRestServlet(RestServlet):
|
||||||
defer.returnValue((200, rules['device']))
|
defer.returnValue((200, rules['device']))
|
||||||
|
|
||||||
instance_handle = path[0]
|
instance_handle = path[0]
|
||||||
|
path = path[1:]
|
||||||
if instance_handle not in rules['device']:
|
if instance_handle not in rules['device']:
|
||||||
ret = {}
|
ret = {}
|
||||||
ret = _add_empty_priority_class_arrays(ret)
|
ret = _add_empty_priority_class_arrays(ret)
|
||||||
|
@ -290,10 +287,21 @@ def _filter_ruleset_with_path(ruleset, path):
|
||||||
raise NotFoundError
|
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):
|
def _priority_class_to_template_name(pc):
|
||||||
if pc > PushRuleRestServlet.PRIORITY_CLASS_MAP['override']:
|
if pc > PushRuleRestServlet.PRIORITY_CLASS_MAP['override']:
|
||||||
# per-device
|
# 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]
|
return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[prio_class_index]
|
||||||
else:
|
else:
|
||||||
return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[pc]
|
return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[pc]
|
||||||
|
@ -316,6 +324,13 @@ def _rule_to_template(rule):
|
||||||
return ret
|
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):
|
class InvalidRuleException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue