mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-10-01 11:49:51 -04:00
4ffac34a64
Means that now you can't do exact matches even in override rules, but I think we can live with that. Advantage is that you'll now always get back what was put in to the API.
399 lines
13 KiB
Python
399 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2014 OpenMarket Ltd
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from twisted.internet import defer
|
|
|
|
from synapse.api.errors import SynapseError, Codes, UnrecognizedRequestError, NotFoundError, \
|
|
StoreError
|
|
from .base import ClientV1RestServlet, client_path_pattern
|
|
from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException
|
|
|
|
import json
|
|
|
|
|
|
class PushRuleRestServlet(ClientV1RestServlet):
|
|
PATTERN = client_path_pattern("/pushrules/.*$")
|
|
PRIORITY_CLASS_MAP = {
|
|
'underride': 1,
|
|
'sender': 2,
|
|
'room': 3,
|
|
'content': 4,
|
|
'override': 5,
|
|
}
|
|
PRIORITY_CLASS_INVERSE_MAP = {v: k for k, v in PRIORITY_CLASS_MAP.items()}
|
|
SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR = (
|
|
"Unrecognised request: You probably wanted a trailing slash")
|
|
|
|
def rule_spec_from_path(self, path):
|
|
if len(path) < 2:
|
|
raise UnrecognizedRequestError()
|
|
if path[0] != 'pushrules':
|
|
raise UnrecognizedRequestError()
|
|
|
|
scope = path[1]
|
|
path = path[2:]
|
|
if scope not in ['global', 'device']:
|
|
raise UnrecognizedRequestError()
|
|
|
|
device = None
|
|
if scope == 'device':
|
|
if len(path) == 0:
|
|
raise UnrecognizedRequestError()
|
|
device = path[0]
|
|
path = path[1:]
|
|
|
|
if len(path) == 0:
|
|
raise UnrecognizedRequestError()
|
|
|
|
template = path[0]
|
|
path = path[1:]
|
|
|
|
if len(path) == 0:
|
|
raise UnrecognizedRequestError()
|
|
|
|
rule_id = path[0]
|
|
|
|
spec = {
|
|
'scope': scope,
|
|
'template': template,
|
|
'rule_id': rule_id
|
|
}
|
|
if device:
|
|
spec['device'] = device
|
|
return spec
|
|
|
|
def rule_tuple_from_request_object(self, rule_template, rule_id, req_obj, device=None):
|
|
if rule_template in ['override', 'underride']:
|
|
if 'conditions' not in req_obj:
|
|
raise InvalidRuleException("Missing 'conditions'")
|
|
conditions = req_obj['conditions']
|
|
for c in conditions:
|
|
if 'kind' not in c:
|
|
raise InvalidRuleException("Condition without 'kind'")
|
|
elif rule_template == 'room':
|
|
conditions = [{
|
|
'kind': 'event_match',
|
|
'key': 'room_id',
|
|
'pattern': rule_id
|
|
}]
|
|
elif rule_template == 'sender':
|
|
conditions = [{
|
|
'kind': 'event_match',
|
|
'key': 'user_id',
|
|
'pattern': rule_id
|
|
}]
|
|
elif rule_template == 'content':
|
|
if 'pattern' not in req_obj:
|
|
raise InvalidRuleException("Content rule missing 'pattern'")
|
|
pat = req_obj['pattern']
|
|
|
|
conditions = [{
|
|
'kind': 'event_match',
|
|
'key': 'content.body',
|
|
'pattern': pat
|
|
}]
|
|
else:
|
|
raise InvalidRuleException("Unknown rule template: %s" % (rule_template,))
|
|
|
|
if device:
|
|
conditions.append({
|
|
'kind': 'device',
|
|
'instance_handle': device
|
|
})
|
|
|
|
if 'actions' not in req_obj:
|
|
raise InvalidRuleException("No actions found")
|
|
actions = req_obj['actions']
|
|
|
|
for a in actions:
|
|
if a in ['notify', 'dont_notify', 'coalesce']:
|
|
pass
|
|
elif isinstance(a, dict) and 'set_sound' in a:
|
|
pass
|
|
else:
|
|
raise InvalidRuleException("Unrecognised action")
|
|
|
|
return conditions, actions
|
|
|
|
@defer.inlineCallbacks
|
|
def on_PUT(self, request):
|
|
spec = self.rule_spec_from_path(request.postpath)
|
|
try:
|
|
priority_class = _priority_class_from_spec(spec)
|
|
except InvalidRuleException as e:
|
|
raise SynapseError(400, e.message)
|
|
|
|
user, _ = yield self.auth.get_user_by_req(request)
|
|
|
|
content = _parse_json(request)
|
|
|
|
try:
|
|
(conditions, actions) = self.rule_tuple_from_request_object(
|
|
spec['template'],
|
|
spec['rule_id'],
|
|
content,
|
|
device=spec['device'] if 'device' in spec else None
|
|
)
|
|
except InvalidRuleException as e:
|
|
raise SynapseError(400, e.message)
|
|
|
|
before = request.args.get("before", None)
|
|
if before and len(before):
|
|
before = before[0]
|
|
after = request.args.get("after", None)
|
|
if after and len(after):
|
|
after = after[0]
|
|
|
|
try:
|
|
yield self.hs.get_datastore().add_push_rule(
|
|
user_name=user.to_string(),
|
|
rule_id=spec['rule_id'],
|
|
priority_class=priority_class,
|
|
conditions=conditions,
|
|
actions=actions,
|
|
before=before,
|
|
after=after
|
|
)
|
|
except InconsistentRuleException as e:
|
|
raise SynapseError(400, e.message)
|
|
except RuleNotFoundException as e:
|
|
raise SynapseError(400, e.message)
|
|
|
|
defer.returnValue((200, {}))
|
|
|
|
@defer.inlineCallbacks
|
|
def on_DELETE(self, request):
|
|
spec = self.rule_spec_from_path(request.postpath)
|
|
try:
|
|
priority_class = _priority_class_from_spec(spec)
|
|
except InvalidRuleException as e:
|
|
raise SynapseError(400, e.message)
|
|
|
|
user, _ = yield self.auth.get_user_by_req(request)
|
|
|
|
if 'device' in spec:
|
|
rules = yield self.hs.get_datastore().get_push_rules_for_user_name(
|
|
user.to_string()
|
|
)
|
|
|
|
for r in rules:
|
|
conditions = json.loads(r['conditions'])
|
|
ih = _instance_handle_from_conditions(conditions)
|
|
if ih == spec['device'] and r['priority_class'] == priority_class:
|
|
yield self.hs.get_datastore().delete_push_rule(
|
|
user.to_string(), spec['rule_id']
|
|
)
|
|
defer.returnValue((200, {}))
|
|
raise NotFoundError()
|
|
else:
|
|
try:
|
|
yield self.hs.get_datastore().delete_push_rule(
|
|
user.to_string(), spec['rule_id'],
|
|
priority_class=priority_class
|
|
)
|
|
defer.returnValue((200, {}))
|
|
except StoreError as e:
|
|
if e.code == 404:
|
|
raise NotFoundError()
|
|
else:
|
|
raise
|
|
|
|
@defer.inlineCallbacks
|
|
def on_GET(self, request):
|
|
user, _ = yield self.auth.get_user_by_req(request)
|
|
|
|
# we build up the full structure and then decide which bits of it
|
|
# to send which means doing unnecessary work sometimes but is
|
|
# is probably not going to make a whole lot of difference
|
|
rawrules = yield self.hs.get_datastore().get_push_rules_for_user_name(user.to_string())
|
|
|
|
rules = {'global': {}, 'device': {}}
|
|
|
|
rules['global'] = _add_empty_priority_class_arrays(rules['global'])
|
|
|
|
for r in rawrules:
|
|
rulearray = None
|
|
|
|
r["conditions"] = json.loads(r["conditions"])
|
|
r["actions"] = json.loads(r["actions"])
|
|
|
|
template_name = _priority_class_to_template_name(r['priority_class'])
|
|
|
|
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']:
|
|
rules['device'][instance_handle] = {}
|
|
rules['device'][instance_handle] = (
|
|
_add_empty_priority_class_arrays(
|
|
rules['device'][instance_handle]
|
|
)
|
|
)
|
|
|
|
rulearray = rules['device'][instance_handle][template_name]
|
|
else:
|
|
rulearray = rules['global'][template_name]
|
|
|
|
template_rule = _rule_to_template(r)
|
|
if template_rule:
|
|
rulearray.append(template_rule)
|
|
|
|
path = request.postpath[1:]
|
|
|
|
if path == []:
|
|
# we're a reference impl: pedantry is our job.
|
|
raise UnrecognizedRequestError(
|
|
PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR
|
|
)
|
|
|
|
if path[0] == '':
|
|
defer.returnValue((200, rules))
|
|
elif path[0] == 'global':
|
|
path = path[1:]
|
|
result = _filter_ruleset_with_path(rules['global'], path)
|
|
defer.returnValue((200, result))
|
|
elif path[0] == 'device':
|
|
path = path[1:]
|
|
if path == []:
|
|
raise UnrecognizedRequestError(
|
|
PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR
|
|
)
|
|
if path[0] == '':
|
|
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)
|
|
defer.returnValue((200, ret))
|
|
ruleset = rules['device'][instance_handle]
|
|
result = _filter_ruleset_with_path(ruleset, path)
|
|
defer.returnValue((200, result))
|
|
else:
|
|
raise UnrecognizedRequestError()
|
|
|
|
def on_OPTIONS(self, _):
|
|
return 200, {}
|
|
|
|
|
|
def _add_empty_priority_class_arrays(d):
|
|
for pc in PushRuleRestServlet.PRIORITY_CLASS_MAP.keys():
|
|
d[pc] = []
|
|
return d
|
|
|
|
|
|
def _instance_handle_from_conditions(conditions):
|
|
"""
|
|
Given a list of conditions, return the instance handle of the
|
|
device rule if there is one
|
|
"""
|
|
for c in conditions:
|
|
if c['kind'] == 'device':
|
|
return c['instance_handle']
|
|
return None
|
|
|
|
|
|
def _filter_ruleset_with_path(ruleset, path):
|
|
if path == []:
|
|
raise UnrecognizedRequestError(
|
|
PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR
|
|
)
|
|
|
|
if path[0] == '':
|
|
return ruleset
|
|
template_kind = path[0]
|
|
if template_kind not in ruleset:
|
|
raise UnrecognizedRequestError()
|
|
path = path[1:]
|
|
if path == []:
|
|
raise UnrecognizedRequestError(
|
|
PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR
|
|
)
|
|
if path[0] == '':
|
|
return ruleset[template_kind]
|
|
rule_id = path[0]
|
|
for r in ruleset[template_kind]:
|
|
if r['rule_id'] == rule_id:
|
|
return r
|
|
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 - len(PushRuleRestServlet.PRIORITY_CLASS_MAP)
|
|
return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[prio_class_index]
|
|
else:
|
|
return PushRuleRestServlet.PRIORITY_CLASS_INVERSE_MAP[pc]
|
|
|
|
|
|
def _rule_to_template(rule):
|
|
template_name = _priority_class_to_template_name(rule['priority_class'])
|
|
if template_name in ['override', 'underride']:
|
|
return {k: rule[k] for k in ["rule_id", "conditions", "actions"]}
|
|
elif template_name in ["sender", "room"]:
|
|
return {k: rule[k] for k in ["rule_id", "actions"]}
|
|
elif template_name == 'content':
|
|
if len(rule["conditions"]) != 1:
|
|
return None
|
|
thecond = rule["conditions"][0]
|
|
if "pattern" not in thecond:
|
|
return None
|
|
ret = {k: rule[k] for k in ["rule_id", "actions"]}
|
|
ret["pattern"] = thecond["pattern"]
|
|
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
|
|
|
|
|
|
# XXX: C+ped from rest/room.py - surely this should be common?
|
|
def _parse_json(request):
|
|
try:
|
|
content = json.loads(request.content.read())
|
|
if type(content) != dict:
|
|
raise SynapseError(400, "Content must be a JSON object.",
|
|
errcode=Codes.NOT_JSON)
|
|
return content
|
|
except ValueError:
|
|
raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
|
|
|
|
|
|
def register_servlets(hs, http_server):
|
|
PushRuleRestServlet(hs).register(http_server)
|