mirror of https://github.com/watcha-fr/synapse
commit
c02da58a9d
@ -0,0 +1,147 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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 synapse.api.constants import EventTypes |
||||
|
||||
import logging |
||||
import re |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class ApplicationService(object): |
||||
"""Defines an application service. This definition is mostly what is |
||||
provided to the /register AS API. |
||||
|
||||
Provides methods to check if this service is "interested" in events. |
||||
""" |
||||
NS_USERS = "users" |
||||
NS_ALIASES = "aliases" |
||||
NS_ROOMS = "rooms" |
||||
# The ordering here is important as it is used to map database values (which |
||||
# are stored as ints representing the position in this list) to namespace |
||||
# values. |
||||
NS_LIST = [NS_USERS, NS_ALIASES, NS_ROOMS] |
||||
|
||||
def __init__(self, token, url=None, namespaces=None, hs_token=None, |
||||
sender=None, txn_id=None): |
||||
self.token = token |
||||
self.url = url |
||||
self.hs_token = hs_token |
||||
self.sender = sender |
||||
self.namespaces = self._check_namespaces(namespaces) |
||||
self.txn_id = txn_id |
||||
|
||||
def _check_namespaces(self, namespaces): |
||||
# Sanity check that it is of the form: |
||||
# { |
||||
# users: ["regex",...], |
||||
# aliases: ["regex",...], |
||||
# rooms: ["regex",...], |
||||
# } |
||||
if not namespaces: |
||||
return None |
||||
|
||||
for ns in ApplicationService.NS_LIST: |
||||
if type(namespaces[ns]) != list: |
||||
raise ValueError("Bad namespace value for '%s'", ns) |
||||
for regex in namespaces[ns]: |
||||
if not isinstance(regex, basestring): |
||||
raise ValueError("Expected string regex for ns '%s'", ns) |
||||
return namespaces |
||||
|
||||
def _matches_regex(self, test_string, namespace_key): |
||||
if not isinstance(test_string, basestring): |
||||
logger.error( |
||||
"Expected a string to test regex against, but got %s", |
||||
test_string |
||||
) |
||||
return False |
||||
|
||||
for regex in self.namespaces[namespace_key]: |
||||
if re.match(regex, test_string): |
||||
return True |
||||
return False |
||||
|
||||
def _matches_user(self, event, member_list): |
||||
if (hasattr(event, "sender") and |
||||
self.is_interested_in_user(event.sender)): |
||||
return True |
||||
# also check m.room.member state key |
||||
if (hasattr(event, "type") and event.type == EventTypes.Member |
||||
and hasattr(event, "state_key") |
||||
and self.is_interested_in_user(event.state_key)): |
||||
return True |
||||
# check joined member events |
||||
for member in member_list: |
||||
if self.is_interested_in_user(member.state_key): |
||||
return True |
||||
return False |
||||
|
||||
def _matches_room_id(self, event): |
||||
if hasattr(event, "room_id"): |
||||
return self.is_interested_in_room(event.room_id) |
||||
return False |
||||
|
||||
def _matches_aliases(self, event, alias_list): |
||||
for alias in alias_list: |
||||
if self.is_interested_in_alias(alias): |
||||
return True |
||||
return False |
||||
|
||||
def is_interested(self, event, restrict_to=None, aliases_for_event=None, |
||||
member_list=None): |
||||
"""Check if this service is interested in this event. |
||||
|
||||
Args: |
||||
event(Event): The event to check. |
||||
restrict_to(str): The namespace to restrict regex tests to. |
||||
aliases_for_event(list): A list of all the known room aliases for |
||||
this event. |
||||
member_list(list): A list of all joined room members in this room. |
||||
Returns: |
||||
bool: True if this service would like to know about this event. |
||||
""" |
||||
if aliases_for_event is None: |
||||
aliases_for_event = [] |
||||
if member_list is None: |
||||
member_list = [] |
||||
|
||||
if restrict_to and restrict_to not in ApplicationService.NS_LIST: |
||||
# this is a programming error, so fail early and raise a general |
||||
# exception |
||||
raise Exception("Unexpected restrict_to value: %s". restrict_to) |
||||
|
||||
if not restrict_to: |
||||
return (self._matches_user(event, member_list) |
||||
or self._matches_aliases(event, aliases_for_event) |
||||
or self._matches_room_id(event)) |
||||
elif restrict_to == ApplicationService.NS_ALIASES: |
||||
return self._matches_aliases(event, aliases_for_event) |
||||
elif restrict_to == ApplicationService.NS_ROOMS: |
||||
return self._matches_room_id(event) |
||||
elif restrict_to == ApplicationService.NS_USERS: |
||||
return self._matches_user(event, member_list) |
||||
|
||||
def is_interested_in_user(self, user_id): |
||||
return self._matches_regex(user_id, ApplicationService.NS_USERS) |
||||
|
||||
def is_interested_in_alias(self, alias): |
||||
return self._matches_regex(alias, ApplicationService.NS_ALIASES) |
||||
|
||||
def is_interested_in_room(self, room_id): |
||||
return self._matches_regex(room_id, ApplicationService.NS_ROOMS) |
||||
|
||||
def __str__(self): |
||||
return "ApplicationService: %s" % (self.__dict__,) |
@ -0,0 +1,108 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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 CodeMessageException |
||||
from synapse.http.client import SimpleHttpClient |
||||
from synapse.events.utils import serialize_event |
||||
|
||||
import logging |
||||
import urllib |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class ApplicationServiceApi(SimpleHttpClient): |
||||
"""This class manages HS -> AS communications, including querying and |
||||
pushing. |
||||
""" |
||||
|
||||
def __init__(self, hs): |
||||
super(ApplicationServiceApi, self).__init__(hs) |
||||
self.clock = hs.get_clock() |
||||
|
||||
@defer.inlineCallbacks |
||||
def query_user(self, service, user_id): |
||||
uri = service.url + ("/users/%s" % urllib.quote(user_id)) |
||||
response = None |
||||
try: |
||||
response = yield self.get_json(uri, { |
||||
"access_token": service.hs_token |
||||
}) |
||||
if response is not None: # just an empty json object |
||||
defer.returnValue(True) |
||||
except CodeMessageException as e: |
||||
if e.code == 404: |
||||
defer.returnValue(False) |
||||
return |
||||
logger.warning("query_user to %s received %s", uri, e.code) |
||||
except Exception as ex: |
||||
logger.warning("query_user to %s threw exception %s", uri, ex) |
||||
defer.returnValue(False) |
||||
|
||||
@defer.inlineCallbacks |
||||
def query_alias(self, service, alias): |
||||
uri = service.url + ("/rooms/%s" % urllib.quote(alias)) |
||||
response = None |
||||
try: |
||||
response = yield self.get_json(uri, { |
||||
"access_token": service.hs_token |
||||
}) |
||||
if response is not None: # just an empty json object |
||||
defer.returnValue(True) |
||||
except CodeMessageException as e: |
||||
logger.warning("query_alias to %s received %s", uri, e.code) |
||||
if e.code == 404: |
||||
defer.returnValue(False) |
||||
return |
||||
except Exception as ex: |
||||
logger.warning("query_alias to %s threw exception %s", uri, ex) |
||||
defer.returnValue(False) |
||||
|
||||
@defer.inlineCallbacks |
||||
def push_bulk(self, service, events): |
||||
events = self._serialize(events) |
||||
|
||||
uri = service.url + ("/transactions/%s" % |
||||
urllib.quote(str(0))) # TODO txn_ids |
||||
response = None |
||||
try: |
||||
response = yield self.put_json( |
||||
uri=uri, |
||||
json_body={ |
||||
"events": events |
||||
}, |
||||
args={ |
||||
"access_token": service.hs_token |
||||
}) |
||||
if response: # just an empty json object |
||||
# TODO: Mark txn as sent successfully |
||||
defer.returnValue(True) |
||||
except CodeMessageException as e: |
||||
logger.warning("push_bulk to %s received %s", uri, e.code) |
||||
except Exception as ex: |
||||
logger.warning("push_bulk to %s threw exception %s", uri, ex) |
||||
defer.returnValue(False) |
||||
|
||||
@defer.inlineCallbacks |
||||
def push(self, service, event): |
||||
response = yield self.push_bulk(service, [event]) |
||||
defer.returnValue(response) |
||||
|
||||
def _serialize(self, events): |
||||
time_now = self.clock.time_msec() |
||||
return [ |
||||
serialize_event(e, time_now, as_client_event=True) for e in events |
||||
] |
@ -0,0 +1,211 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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.constants import EventTypes, Membership |
||||
from synapse.api.errors import Codes, StoreError, SynapseError |
||||
from synapse.appservice import ApplicationService |
||||
from synapse.types import UserID |
||||
import synapse.util.stringutils as stringutils |
||||
|
||||
import logging |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
# NB: Purposefully not inheriting BaseHandler since that contains way too much |
||||
# setup code which this handler does not need or use. This makes testing a lot |
||||
# easier. |
||||
class ApplicationServicesHandler(object): |
||||
|
||||
def __init__(self, hs, appservice_api): |
||||
self.store = hs.get_datastore() |
||||
self.hs = hs |
||||
self.appservice_api = appservice_api |
||||
|
||||
@defer.inlineCallbacks |
||||
def register(self, app_service): |
||||
logger.info("Register -> %s", app_service) |
||||
# check the token is recognised |
||||
try: |
||||
stored_service = yield self.store.get_app_service_by_token( |
||||
app_service.token |
||||
) |
||||
if not stored_service: |
||||
raise StoreError(404, "Application service not found") |
||||
except StoreError: |
||||
raise SynapseError( |
||||
403, "Unrecognised application services token. " |
||||
"Consult the home server admin.", |
||||
errcode=Codes.FORBIDDEN |
||||
) |
||||
|
||||
app_service.hs_token = self._generate_hs_token() |
||||
|
||||
# create a sender for this application service which is used when |
||||
# creating rooms, etc.. |
||||
account = yield self.hs.get_handlers().registration_handler.register() |
||||
app_service.sender = account[0] |
||||
|
||||
yield self.store.update_app_service(app_service) |
||||
defer.returnValue(app_service) |
||||
|
||||
@defer.inlineCallbacks |
||||
def unregister(self, token): |
||||
logger.info("Unregister as_token=%s", token) |
||||
yield self.store.unregister_app_service(token) |
||||
|
||||
@defer.inlineCallbacks |
||||
def notify_interested_services(self, event): |
||||
"""Notifies (pushes) all application services interested in this event. |
||||
|
||||
Pushing is done asynchronously, so this method won't block for any |
||||
prolonged length of time. |
||||
|
||||
Args: |
||||
event(Event): The event to push out to interested services. |
||||
""" |
||||
# Gather interested services |
||||
services = yield self._get_services_for_event(event) |
||||
if len(services) == 0: |
||||
return # no services need notifying |
||||
|
||||
# Do we know this user exists? If not, poke the user query API for |
||||
# all services which match that user regex. This needs to block as these |
||||
# user queries need to be made BEFORE pushing the event. |
||||
yield self._check_user_exists(event.sender) |
||||
if event.type == EventTypes.Member: |
||||
yield self._check_user_exists(event.state_key) |
||||
|
||||
# Fork off pushes to these services - XXX First cut, best effort |
||||
for service in services: |
||||
self.appservice_api.push(service, event) |
||||
|
||||
@defer.inlineCallbacks |
||||
def query_user_exists(self, user_id): |
||||
"""Check if any application service knows this user_id exists. |
||||
|
||||
Args: |
||||
user_id(str): The user to query if they exist on any AS. |
||||
Returns: |
||||
True if this user exists on at least one application service. |
||||
""" |
||||
user_query_services = yield self._get_services_for_user( |
||||
user_id=user_id |
||||
) |
||||
for user_service in user_query_services: |
||||
is_known_user = yield self.appservice_api.query_user( |
||||
user_service, user_id |
||||
) |
||||
if is_known_user: |
||||
defer.returnValue(True) |
||||
defer.returnValue(False) |
||||
|
||||
@defer.inlineCallbacks |
||||
def query_room_alias_exists(self, room_alias): |
||||
"""Check if an application service knows this room alias exists. |
||||
|
||||
Args: |
||||
room_alias(RoomAlias): The room alias to query. |
||||
Returns: |
||||
namedtuple: with keys "room_id" and "servers" or None if no |
||||
association can be found. |
||||
""" |
||||
room_alias_str = room_alias.to_string() |
||||
alias_query_services = yield self._get_services_for_event( |
||||
event=None, |
||||
restrict_to=ApplicationService.NS_ALIASES, |
||||
alias_list=[room_alias_str] |
||||
) |
||||
for alias_service in alias_query_services: |
||||
is_known_alias = yield self.appservice_api.query_alias( |
||||
alias_service, room_alias_str |
||||
) |
||||
if is_known_alias: |
||||
# the alias exists now so don't query more ASes. |
||||
result = yield self.store.get_association_from_room_alias( |
||||
room_alias |
||||
) |
||||
defer.returnValue(result) |
||||
|
||||
@defer.inlineCallbacks |
||||
def _get_services_for_event(self, event, restrict_to="", alias_list=None): |
||||
"""Retrieve a list of application services interested in this event. |
||||
|
||||
Args: |
||||
event(Event): The event to check. Can be None if alias_list is not. |
||||
restrict_to(str): The namespace to restrict regex tests to. |
||||
alias_list: A list of aliases to get services for. If None, this |
||||
list is obtained from the database. |
||||
Returns: |
||||
list<ApplicationService>: A list of services interested in this |
||||
event based on the service regex. |
||||
""" |
||||
member_list = None |
||||
if hasattr(event, "room_id"): |
||||
# We need to know the aliases associated with this event.room_id, |
||||
# if any. |
||||
if not alias_list: |
||||
alias_list = yield self.store.get_aliases_for_room( |
||||
event.room_id |
||||
) |
||||
# We need to know the members associated with this event.room_id, |
||||
# if any. |
||||
member_list = yield self.store.get_room_members( |
||||
room_id=event.room_id, |
||||
membership=Membership.JOIN |
||||
) |
||||
|
||||
services = yield self.store.get_app_services() |
||||
interested_list = [ |
||||
s for s in services if ( |
||||
s.is_interested(event, restrict_to, alias_list, member_list) |
||||
) |
||||
] |
||||
defer.returnValue(interested_list) |
||||
|
||||
@defer.inlineCallbacks |
||||
def _get_services_for_user(self, user_id): |
||||
services = yield self.store.get_app_services() |
||||
interested_list = [ |
||||
s for s in services if ( |
||||
s.is_interested_in_user(user_id) |
||||
) |
||||
] |
||||
defer.returnValue(interested_list) |
||||
|
||||
@defer.inlineCallbacks |
||||
def _is_unknown_user(self, user_id): |
||||
user = UserID.from_string(user_id) |
||||
if not self.hs.is_mine(user): |
||||
# we don't know if they are unknown or not since it isn't one of our |
||||
# users. We can't poke ASes. |
||||
defer.returnValue(False) |
||||
return |
||||
|
||||
user_info = yield self.store.get_user_by_id(user_id) |
||||
defer.returnValue(len(user_info) == 0) |
||||
|
||||
@defer.inlineCallbacks |
||||
def _check_user_exists(self, user_id): |
||||
unknown_user = yield self._is_unknown_user(user_id) |
||||
if unknown_user: |
||||
exists = yield self.query_user_exists(user_id) |
||||
defer.returnValue(exists) |
||||
defer.returnValue(True) |
||||
|
||||
def _generate_hs_token(self): |
||||
return stringutils.random_string(24) |
@ -0,0 +1,14 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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. |
@ -0,0 +1,29 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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 . import register |
||||
|
||||
from synapse.http.server import JsonResource |
||||
|
||||
|
||||
class AppServiceRestResource(JsonResource): |
||||
"""A resource for version 1 of the matrix application service API.""" |
||||
|
||||
def __init__(self, hs): |
||||
JsonResource.__init__(self, hs) |
||||
self.register_servlets(self, hs) |
||||
|
||||
@staticmethod |
||||
def register_servlets(appservice_resource, hs): |
||||
register.register_servlets(hs, appservice_resource) |
@ -0,0 +1,48 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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. |
||||
|
||||
"""This module contains base REST classes for constructing client v1 servlets. |
||||
""" |
||||
|
||||
from synapse.http.servlet import RestServlet |
||||
from synapse.api.urls import APP_SERVICE_PREFIX |
||||
import re |
||||
|
||||
import logging |
||||
|
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
def as_path_pattern(path_regex): |
||||
"""Creates a regex compiled appservice path with the correct path |
||||
prefix. |
||||
|
||||
Args: |
||||
path_regex (str): The regex string to match. This should NOT have a ^ |
||||
as this will be prefixed. |
||||
Returns: |
||||
SRE_Pattern |
||||
""" |
||||
return re.compile("^" + APP_SERVICE_PREFIX + path_regex) |
||||
|
||||
|
||||
class AppServiceRestServlet(RestServlet): |
||||
"""A base Synapse REST Servlet for the application services version 1 API. |
||||
""" |
||||
|
||||
def __init__(self, hs): |
||||
self.hs = hs |
||||
self.handler = hs.get_handlers().appservice_handler |
@ -0,0 +1,121 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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. |
||||
|
||||
"""This module contains REST servlets to do with registration: /register""" |
||||
from twisted.internet import defer |
||||
|
||||
from base import AppServiceRestServlet, as_path_pattern |
||||
from synapse.api.errors import CodeMessageException, SynapseError |
||||
from synapse.storage.appservice import ApplicationService |
||||
|
||||
import json |
||||
import logging |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class RegisterRestServlet(AppServiceRestServlet): |
||||
"""Handles AS registration with the home server. |
||||
""" |
||||
|
||||
PATTERN = as_path_pattern("/register$") |
||||
|
||||
@defer.inlineCallbacks |
||||
def on_POST(self, request): |
||||
params = _parse_json(request) |
||||
|
||||
# sanity check required params |
||||
try: |
||||
as_token = params["as_token"] |
||||
as_url = params["url"] |
||||
if (not isinstance(as_token, basestring) or |
||||
not isinstance(as_url, basestring)): |
||||
raise ValueError |
||||
except (KeyError, ValueError): |
||||
raise SynapseError( |
||||
400, "Missed required keys: as_token(str) / url(str)." |
||||
) |
||||
|
||||
namespaces = { |
||||
"users": [], |
||||
"rooms": [], |
||||
"aliases": [] |
||||
} |
||||
|
||||
if "namespaces" in params: |
||||
self._parse_namespace(namespaces, params["namespaces"], "users") |
||||
self._parse_namespace(namespaces, params["namespaces"], "rooms") |
||||
self._parse_namespace(namespaces, params["namespaces"], "aliases") |
||||
|
||||
app_service = ApplicationService(as_token, as_url, namespaces) |
||||
|
||||
app_service = yield self.handler.register(app_service) |
||||
hs_token = app_service.hs_token |
||||
|
||||
defer.returnValue((200, { |
||||
"hs_token": hs_token |
||||
})) |
||||
|
||||
def _parse_namespace(self, target_ns, origin_ns, ns): |
||||
if ns not in target_ns or ns not in origin_ns: |
||||
return # nothing to parse / map through to. |
||||
|
||||
possible_regex_list = origin_ns[ns] |
||||
if not type(possible_regex_list) == list: |
||||
raise SynapseError(400, "Namespace %s isn't an array." % ns) |
||||
|
||||
for regex in possible_regex_list: |
||||
if not isinstance(regex, basestring): |
||||
raise SynapseError( |
||||
400, "Regex '%s' isn't a string in namespace %s" % |
||||
(regex, ns) |
||||
) |
||||
|
||||
target_ns[ns] = origin_ns[ns] |
||||
|
||||
|
||||
class UnregisterRestServlet(AppServiceRestServlet): |
||||
"""Handles AS registration with the home server. |
||||
""" |
||||
|
||||
PATTERN = as_path_pattern("/unregister$") |
||||
|
||||
def on_POST(self, request): |
||||
params = _parse_json(request) |
||||
try: |
||||
as_token = params["as_token"] |
||||
if not isinstance(as_token, basestring): |
||||
raise ValueError |
||||
except (KeyError, ValueError): |
||||
raise SynapseError(400, "Missing required key: as_token(str)") |
||||
|
||||
yield self.handler.unregister(as_token) |
||||
|
||||
raise CodeMessageException(500, "Not implemented") |
||||
|
||||
|
||||
def _parse_json(request): |
||||
try: |
||||
content = json.loads(request.content.read()) |
||||
if type(content) != dict: |
||||
raise SynapseError(400, "Content must be a JSON object.") |
||||
return content |
||||
except ValueError: |
||||
raise SynapseError(400, "Content not JSON.") |
||||
|
||||
|
||||
def register_servlets(hs, http_server): |
||||
RegisterRestServlet(hs).register(http_server) |
||||
UnregisterRestServlet(hs).register(http_server) |
@ -0,0 +1,244 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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. |
||||
import logging |
||||
from twisted.internet import defer |
||||
|
||||
from synapse.api.errors import StoreError |
||||
from synapse.appservice import ApplicationService |
||||
from ._base import SQLBaseStore |
||||
|
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class ApplicationServiceCache(object): |
||||
"""Caches ApplicationServices and provides utility functions on top. |
||||
|
||||
This class is designed to be invoked on incoming events in order to avoid |
||||
hammering the database every time to extract a list of application service |
||||
regexes. |
||||
""" |
||||
|
||||
def __init__(self): |
||||
self.services = [] |
||||
|
||||
|
||||
class ApplicationServiceStore(SQLBaseStore): |
||||
|
||||
def __init__(self, hs): |
||||
super(ApplicationServiceStore, self).__init__(hs) |
||||
self.cache = ApplicationServiceCache() |
||||
self.cache_defer = self._populate_cache() |
||||
|
||||
@defer.inlineCallbacks |
||||
def unregister_app_service(self, token): |
||||
"""Unregisters this service. |
||||
|
||||
This removes all AS specific regex and the base URL. The token is the |
||||
only thing preserved for future registration attempts. |
||||
""" |
||||
yield self.cache_defer # make sure the cache is ready |
||||
yield self.runInteraction( |
||||
"unregister_app_service", |
||||
self._unregister_app_service_txn, |
||||
token, |
||||
) |
||||
# update cache TODO: Should this be in the txn? |
||||
for service in self.cache.services: |
||||
if service.token == token: |
||||
service.url = None |
||||
service.namespaces = None |
||||
service.hs_token = None |
||||
|
||||
def _unregister_app_service_txn(self, txn, token): |
||||
# kill the url to prevent pushes |
||||
txn.execute( |
||||
"UPDATE application_services SET url=NULL WHERE token=?", |
||||
(token,) |
||||
) |
||||
|
||||
# cleanup regex |
||||
as_id = self._get_as_id_txn(txn, token) |
||||
if not as_id: |
||||
logger.warning( |
||||
"unregister_app_service_txn: Failed to find as_id for token=", |
||||
token |
||||
) |
||||
return False |
||||
|
||||
txn.execute( |
||||
"DELETE FROM application_services_regex WHERE as_id=?", |
||||
(as_id,) |
||||
) |
||||
return True |
||||
|
||||
@defer.inlineCallbacks |
||||
def update_app_service(self, service): |
||||
"""Update an application service, clobbering what was previously there. |
||||
|
||||
Args: |
||||
service(ApplicationService): The updated service. |
||||
""" |
||||
yield self.cache_defer # make sure the cache is ready |
||||
|
||||
# NB: There is no "insert" since we provide no public-facing API to |
||||
# allocate new ASes. It relies on the server admin inserting the AS |
||||
# token into the database manually. |
||||
|
||||
if not service.token or not service.url: |
||||
raise StoreError(400, "Token and url must be specified.") |
||||
|
||||
if not service.hs_token: |
||||
raise StoreError(500, "No HS token") |
||||
|
||||
yield self.runInteraction( |
||||
"update_app_service", |
||||
self._update_app_service_txn, |
||||
service |
||||
) |
||||
|
||||
# update cache TODO: Should this be in the txn? |
||||
for (index, cache_service) in enumerate(self.cache.services): |
||||
if service.token == cache_service.token: |
||||
self.cache.services[index] = service |
||||
logger.info("Updated: %s", service) |
||||
return |
||||
# new entry |
||||
self.cache.services.append(service) |
||||
logger.info("Updated(new): %s", service) |
||||
|
||||
def _update_app_service_txn(self, txn, service): |
||||
as_id = self._get_as_id_txn(txn, service.token) |
||||
if not as_id: |
||||
logger.warning( |
||||
"update_app_service_txn: Failed to find as_id for token=", |
||||
service.token |
||||
) |
||||
return False |
||||
|
||||
txn.execute( |
||||
"UPDATE application_services SET url=?, hs_token=?, sender=? " |
||||
"WHERE id=?", |
||||
(service.url, service.hs_token, service.sender, as_id,) |
||||
) |
||||
# cleanup regex |
||||
txn.execute( |
||||
"DELETE FROM application_services_regex WHERE as_id=?", |
||||
(as_id,) |
||||
) |
||||
for (ns_int, ns_str) in enumerate(ApplicationService.NS_LIST): |
||||
if ns_str in service.namespaces: |
||||
for regex in service.namespaces[ns_str]: |
||||
txn.execute( |
||||
"INSERT INTO application_services_regex(" |
||||
"as_id, namespace, regex) values(?,?,?)", |
||||
(as_id, ns_int, regex) |
||||
) |
||||
return True |
||||
|
||||
def _get_as_id_txn(self, txn, token): |
||||
cursor = txn.execute( |
||||
"SELECT id FROM application_services WHERE token=?", |
||||
(token,) |
||||
) |
||||
res = cursor.fetchone() |
||||
if res: |
||||
return res[0] |
||||
|
||||
@defer.inlineCallbacks |
||||
def get_app_services(self): |
||||
yield self.cache_defer # make sure the cache is ready |
||||
defer.returnValue(self.cache.services) |
||||
|
||||
@defer.inlineCallbacks |
||||
def get_app_service_by_token(self, token, from_cache=True): |
||||
"""Get the application service with the given token. |
||||
|
||||
Args: |
||||
token (str): The application service token. |
||||
from_cache (bool): True to get this service from the cache, False to |
||||
check the database. |
||||
Raises: |
||||
StoreError if there was a problem retrieving this service. |
||||
""" |
||||
yield self.cache_defer # make sure the cache is ready |
||||
|
||||
if from_cache: |
||||
for service in self.cache.services: |
||||
if service.token == token: |
||||
defer.returnValue(service) |
||||
return |
||||
defer.returnValue(None) |
||||
|
||||
# TODO: The from_cache=False impl |
||||
# TODO: This should be JOINed with the application_services_regex table. |
||||
|
||||
@defer.inlineCallbacks |
||||
def _populate_cache(self): |
||||
"""Populates the ApplicationServiceCache from the database.""" |
||||
sql = ("SELECT * FROM application_services LEFT JOIN " |
||||
"application_services_regex ON application_services.id = " |
||||
"application_services_regex.as_id") |
||||
# SQL results in the form: |
||||
# [ |
||||
# { |
||||
# 'regex': "something", |
||||
# 'url': "something", |
||||
# 'namespace': enum, |
||||
# 'as_id': 0, |
||||
# 'token': "something", |
||||
# 'hs_token': "otherthing", |
||||
# 'id': 0 |
||||
# } |
||||
# ] |
||||
services = {} |
||||
results = yield self._execute_and_decode(sql) |
||||
for res in results: |
||||
as_token = res["token"] |
||||
if as_token not in services: |
||||
# add the service |
||||
services[as_token] = { |
||||
"url": res["url"], |
||||
"token": as_token, |
||||
"hs_token": res["hs_token"], |
||||
"sender": res["sender"], |
||||
"namespaces": { |
||||
ApplicationService.NS_USERS: [], |
||||
ApplicationService.NS_ALIASES: [], |
||||
ApplicationService.NS_ROOMS: [] |
||||
} |
||||
} |
||||
# add the namespace regex if one exists |
||||
ns_int = res["namespace"] |
||||
if ns_int is None: |
||||
continue |
||||
try: |
||||
services[as_token]["namespaces"][ |
||||
ApplicationService.NS_LIST[ns_int]].append( |
||||
res["regex"] |
||||
) |
||||
except IndexError: |
||||
logger.error("Bad namespace enum '%s'. %s", ns_int, res) |
||||
|
||||
# TODO get last successful txn id f.e. service |
||||
for service in services.values(): |
||||
logger.info("Found application service: %s", service) |
||||
self.cache.services.append(ApplicationService( |
||||
token=service["token"], |
||||
url=service["url"], |
||||
namespaces=service["namespaces"], |
||||
hs_token=service["hs_token"], |
||||
sender=service["sender"] |
||||
)) |
@ -0,0 +1,34 @@ |
||||
/* Copyright 2015 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. |
||||
*/ |
||||
|
||||
CREATE TABLE IF NOT EXISTS application_services( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
url TEXT, |
||||
token TEXT, |
||||
hs_token TEXT, |
||||
sender TEXT, |
||||
UNIQUE(token) ON CONFLICT ROLLBACK |
||||
); |
||||
|
||||
CREATE TABLE IF NOT EXISTS application_services_regex( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
as_id INTEGER NOT NULL, |
||||
namespace INTEGER, /* enum[room_id|room_alias|user_id] */ |
||||
regex TEXT, |
||||
FOREIGN KEY(as_id) REFERENCES application_services(id) |
||||
); |
||||
|
||||
|
||||
|
@ -0,0 +1,34 @@ |
||||
/* Copyright 2015 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. |
||||
*/ |
||||
|
||||
CREATE TABLE IF NOT EXISTS application_services( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
url TEXT, |
||||
token TEXT, |
||||
hs_token TEXT, |
||||
sender TEXT, |
||||
UNIQUE(token) ON CONFLICT ROLLBACK |
||||
); |
||||
|
||||
CREATE TABLE IF NOT EXISTS application_services_regex( |
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
as_id INTEGER NOT NULL, |
||||
namespace INTEGER, /* enum[room_id|room_alias|user_id] */ |
||||
regex TEXT, |
||||
FOREIGN KEY(as_id) REFERENCES application_services(id) |
||||
); |
||||
|
||||
|
||||
|
@ -0,0 +1,139 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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 tests import unittest |
||||
from twisted.internet import defer |
||||
|
||||
from mock import Mock |
||||
|
||||
from synapse.api.auth import Auth |
||||
from synapse.api.errors import AuthError |
||||
|
||||
|
||||
class AuthTestCase(unittest.TestCase): |
||||
|
||||
def setUp(self): |
||||
self.state_handler = Mock() |
||||
self.store = Mock() |
||||
|
||||
self.hs = Mock() |
||||
self.hs.get_datastore = Mock(return_value=self.store) |
||||
self.hs.get_state_handler = Mock(return_value=self.state_handler) |
||||
self.auth = Auth(self.hs) |
||||
|
||||
self.test_user = "@foo:bar" |
||||
self.test_token = "_test_token_" |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_get_user_by_req_user_valid_token(self): |
||||
self.store.get_app_service_by_token = Mock(return_value=None) |
||||
user_info = { |
||||
"name": self.test_user, |
||||
"device_id": "nothing", |
||||
"token_id": "ditto", |
||||
"admin": False |
||||
} |
||||
self.store.get_user_by_token = Mock(return_value=user_info) |
||||
|
||||
request = Mock(args={}) |
||||
request.args["access_token"] = [self.test_token] |
||||
request.requestHeaders.getRawHeaders = Mock(return_value=[""]) |
||||
(user, info) = yield self.auth.get_user_by_req(request) |
||||
self.assertEquals(user.to_string(), self.test_user) |
||||
|
||||
def test_get_user_by_req_user_bad_token(self): |
||||
self.store.get_app_service_by_token = Mock(return_value=None) |
||||
self.store.get_user_by_token = Mock(return_value=None) |
||||
|
||||
request = Mock(args={}) |
||||
request.args["access_token"] = [self.test_token] |
||||
request.requestHeaders.getRawHeaders = Mock(return_value=[""]) |
||||
d = self.auth.get_user_by_req(request) |
||||
self.failureResultOf(d, AuthError) |
||||
|
||||
def test_get_user_by_req_user_missing_token(self): |
||||
self.store.get_app_service_by_token = Mock(return_value=None) |
||||
user_info = { |
||||
"name": self.test_user, |
||||
"device_id": "nothing", |
||||
"token_id": "ditto", |
||||
"admin": False |
||||
} |
||||
self.store.get_user_by_token = Mock(return_value=user_info) |
||||
|
||||
request = Mock(args={}) |
||||
request.requestHeaders.getRawHeaders = Mock(return_value=[""]) |
||||
d = self.auth.get_user_by_req(request) |
||||
self.failureResultOf(d, AuthError) |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_get_user_by_req_appservice_valid_token(self): |
||||
app_service = Mock(token="foobar", url="a_url", sender=self.test_user) |
||||
self.store.get_app_service_by_token = Mock(return_value=app_service) |
||||
self.store.get_user_by_token = Mock(return_value=None) |
||||
|
||||
request = Mock(args={}) |
||||
request.args["access_token"] = [self.test_token] |
||||
request.requestHeaders.getRawHeaders = Mock(return_value=[""]) |
||||
(user, info) = yield self.auth.get_user_by_req(request) |
||||
self.assertEquals(user.to_string(), self.test_user) |
||||
|
||||
def test_get_user_by_req_appservice_bad_token(self): |
||||
self.store.get_app_service_by_token = Mock(return_value=None) |
||||
self.store.get_user_by_token = Mock(return_value=None) |
||||
|
||||
request = Mock(args={}) |
||||
request.args["access_token"] = [self.test_token] |
||||
request.requestHeaders.getRawHeaders = Mock(return_value=[""]) |
||||
d = self.auth.get_user_by_req(request) |
||||
self.failureResultOf(d, AuthError) |
||||
|
||||
def test_get_user_by_req_appservice_missing_token(self): |
||||
app_service = Mock(token="foobar", url="a_url", sender=self.test_user) |
||||
self.store.get_app_service_by_token = Mock(return_value=app_service) |
||||
self.store.get_user_by_token = Mock(return_value=None) |
||||
|
||||
request = Mock(args={}) |
||||
request.requestHeaders.getRawHeaders = Mock(return_value=[""]) |
||||
d = self.auth.get_user_by_req(request) |
||||
self.failureResultOf(d, AuthError) |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_get_user_by_req_appservice_valid_token_valid_user_id(self): |
||||
masquerading_user_id = "@doppelganger:matrix.org" |
||||
app_service = Mock(token="foobar", url="a_url", sender=self.test_user) |
||||
app_service.is_interested_in_user = Mock(return_value=True) |
||||
self.store.get_app_service_by_token = Mock(return_value=app_service) |
||||
self.store.get_user_by_token = Mock(return_value=None) |
||||
|
||||
request = Mock(args={}) |
||||
request.args["access_token"] = [self.test_token] |
||||
request.args["user_id"] = [masquerading_user_id] |
||||
request.requestHeaders.getRawHeaders = Mock(return_value=[""]) |
||||
(user, info) = yield self.auth.get_user_by_req(request) |
||||
self.assertEquals(user.to_string(), masquerading_user_id) |
||||
|
||||
def test_get_user_by_req_appservice_valid_token_bad_user_id(self): |
||||
masquerading_user_id = "@doppelganger:matrix.org" |
||||
app_service = Mock(token="foobar", url="a_url", sender=self.test_user) |
||||
app_service.is_interested_in_user = Mock(return_value=False) |
||||
self.store.get_app_service_by_token = Mock(return_value=app_service) |
||||
self.store.get_user_by_token = Mock(return_value=None) |
||||
|
||||
request = Mock(args={}) |
||||
request.args["access_token"] = [self.test_token] |
||||
request.args["user_id"] = [masquerading_user_id] |
||||
request.requestHeaders.getRawHeaders = Mock(return_value=[""]) |
||||
d = self.auth.get_user_by_req(request) |
||||
self.failureResultOf(d, AuthError) |
@ -0,0 +1,14 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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. |
@ -0,0 +1,170 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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 synapse.appservice import ApplicationService |
||||
|
||||
from mock import Mock, PropertyMock |
||||
from tests import unittest |
||||
|
||||
|
||||
class ApplicationServiceTestCase(unittest.TestCase): |
||||
|
||||
def setUp(self): |
||||
self.service = ApplicationService( |
||||
url="some_url", |
||||
token="some_token", |
||||
namespaces={ |
||||
ApplicationService.NS_USERS: [], |
||||
ApplicationService.NS_ROOMS: [], |
||||
ApplicationService.NS_ALIASES: [] |
||||
} |
||||
) |
||||
self.event = Mock( |
||||
type="m.something", room_id="!foo:bar", sender="@someone:somewhere" |
||||
) |
||||
|
||||
def test_regex_user_id_prefix_match(self): |
||||
self.service.namespaces[ApplicationService.NS_USERS].append( |
||||
"@irc_.*" |
||||
) |
||||
self.event.sender = "@irc_foobar:matrix.org" |
||||
self.assertTrue(self.service.is_interested(self.event)) |
||||
|
||||
def test_regex_user_id_prefix_no_match(self): |
||||
self.service.namespaces[ApplicationService.NS_USERS].append( |
||||
"@irc_.*" |
||||
) |
||||
self.event.sender = "@someone_else:matrix.org" |
||||
self.assertFalse(self.service.is_interested(self.event)) |
||||
|
||||
def test_regex_room_member_is_checked(self): |
||||
self.service.namespaces[ApplicationService.NS_USERS].append( |
||||
"@irc_.*" |
||||
) |
||||
self.event.sender = "@someone_else:matrix.org" |
||||
self.event.type = "m.room.member" |
||||
self.event.state_key = "@irc_foobar:matrix.org" |
||||
self.assertTrue(self.service.is_interested(self.event)) |
||||
|
||||
def test_regex_room_id_match(self): |
||||
self.service.namespaces[ApplicationService.NS_ROOMS].append( |
||||
"!some_prefix.*some_suffix:matrix.org" |
||||
) |
||||
self.event.room_id = "!some_prefixs0m3th1nGsome_suffix:matrix.org" |
||||
self.assertTrue(self.service.is_interested(self.event)) |
||||
|
||||
def test_regex_room_id_no_match(self): |
||||
self.service.namespaces[ApplicationService.NS_ROOMS].append( |
||||
"!some_prefix.*some_suffix:matrix.org" |
||||
) |
||||
self.event.room_id = "!XqBunHwQIXUiqCaoxq:matrix.org" |
||||
self.assertFalse(self.service.is_interested(self.event)) |
||||
|
||||
def test_regex_alias_match(self): |
||||
self.service.namespaces[ApplicationService.NS_ALIASES].append( |
||||
"#irc_.*:matrix.org" |
||||
) |
||||
self.assertTrue(self.service.is_interested( |
||||
self.event, |
||||
aliases_for_event=["#irc_foobar:matrix.org", "#athing:matrix.org"] |
||||
)) |
||||
|
||||
def test_regex_alias_no_match(self): |
||||
self.service.namespaces[ApplicationService.NS_ALIASES].append( |
||||
"#irc_.*:matrix.org" |
||||
) |
||||
self.assertFalse(self.service.is_interested( |
||||
self.event, |
||||
aliases_for_event=["#xmpp_foobar:matrix.org", "#athing:matrix.org"] |
||||
)) |
||||
|
||||
def test_regex_multiple_matches(self): |
||||
self.service.namespaces[ApplicationService.NS_ALIASES].append( |
||||
"#irc_.*:matrix.org" |
||||
) |
||||
self.service.namespaces[ApplicationService.NS_USERS].append( |
||||
"@irc_.*" |
||||
) |
||||
self.event.sender = "@irc_foobar:matrix.org" |
||||
self.assertTrue(self.service.is_interested( |
||||
self.event, |
||||
aliases_for_event=["#irc_barfoo:matrix.org"] |
||||
)) |
||||
|
||||
def test_restrict_to_rooms(self): |
||||
self.service.namespaces[ApplicationService.NS_ROOMS].append( |
||||
"!flibble_.*:matrix.org" |
||||
) |
||||
self.service.namespaces[ApplicationService.NS_USERS].append( |
||||
"@irc_.*" |
||||
) |
||||
self.event.sender = "@irc_foobar:matrix.org" |
||||
self.event.room_id = "!wibblewoo:matrix.org" |
||||
self.assertFalse(self.service.is_interested( |
||||
self.event, |
||||
restrict_to=ApplicationService.NS_ROOMS |
||||
)) |
||||
|
||||
def test_restrict_to_aliases(self): |
||||
self.service.namespaces[ApplicationService.NS_ALIASES].append( |
||||
"#xmpp_.*:matrix.org" |
||||
) |
||||
self.service.namespaces[ApplicationService.NS_USERS].append( |
||||
"@irc_.*" |
||||
) |
||||
self.event.sender = "@irc_foobar:matrix.org" |
||||
self.assertFalse(self.service.is_interested( |
||||
self.event, |
||||
restrict_to=ApplicationService.NS_ALIASES, |
||||
aliases_for_event=["#irc_barfoo:matrix.org"] |
||||
)) |
||||
|
||||
def test_restrict_to_senders(self): |
||||
self.service.namespaces[ApplicationService.NS_ALIASES].append( |
||||
"#xmpp_.*:matrix.org" |
||||
) |
||||
self.service.namespaces[ApplicationService.NS_USERS].append( |
||||
"@irc_.*" |
||||
) |
||||
self.event.sender = "@xmpp_foobar:matrix.org" |
||||
self.assertFalse(self.service.is_interested( |
||||
self.event, |
||||
restrict_to=ApplicationService.NS_USERS, |
||||
aliases_for_event=["#xmpp_barfoo:matrix.org"] |
||||
)) |
||||
|
||||
def test_member_list_match(self): |
||||
self.service.namespaces[ApplicationService.NS_USERS].append( |
||||
"@irc_.*" |
||||
) |
||||
join_list = [ |
||||
Mock( |
||||
type="m.room.member", room_id="!foo:bar", sender="@alice:here", |
||||
state_key="@alice:here" |
||||
), |
||||
Mock( |
||||
type="m.room.member", room_id="!foo:bar", sender="@irc_fo:here", |
||||
state_key="@irc_fo:here" # AS user |
||||
), |
||||
Mock( |
||||
type="m.room.member", room_id="!foo:bar", sender="@bob:here", |
||||
state_key="@bob:here" |
||||
) |
||||
] |
||||
|
||||
self.event.sender = "@xmpp_foobar:matrix.org" |
||||
self.assertTrue(self.service.is_interested( |
||||
event=self.event, |
||||
member_list=join_list |
||||
)) |
@ -0,0 +1,93 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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 .. import unittest |
||||
|
||||
from synapse.handlers.appservice import ApplicationServicesHandler |
||||
|
||||
from mock import Mock |
||||
|
||||
|
||||
class AppServiceHandlerTestCase(unittest.TestCase): |
||||
""" Tests the ApplicationServicesHandler. """ |
||||
|
||||
def setUp(self): |
||||
self.mock_store = Mock() |
||||
self.mock_as_api = Mock() |
||||
hs = Mock() |
||||
hs.get_datastore = Mock(return_value=self.mock_store) |
||||
self.handler = ApplicationServicesHandler( |
||||
hs, self.mock_as_api |
||||
) |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_notify_interested_services(self): |
||||
interested_service = self._mkservice(is_interested=True) |
||||
services = [ |
||||
self._mkservice(is_interested=False), |
||||
interested_service, |
||||
self._mkservice(is_interested=False) |
||||
] |
||||
|
||||
self.mock_store.get_app_services = Mock(return_value=services) |
||||
self.mock_store.get_user_by_id = Mock(return_value=[]) |
||||
|
||||
event = Mock( |
||||
sender="@someone:anywhere", |
||||
type="m.room.message", |
||||
room_id="!foo:bar" |
||||
) |
||||
self.mock_as_api.push = Mock() |
||||
yield self.handler.notify_interested_services(event) |
||||
self.mock_as_api.push.assert_called_once_with(interested_service, event) |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_query_room_alias_exists(self): |
||||
room_alias_str = "#foo:bar" |
||||
room_alias = Mock() |
||||
room_alias.to_string = Mock(return_value=room_alias_str) |
||||
|
||||
room_id = "!alpha:bet" |
||||
servers = ["aperture"] |
||||
interested_service = self._mkservice(is_interested=True) |
||||
services = [ |
||||
self._mkservice(is_interested=False), |
||||
interested_service, |
||||
self._mkservice(is_interested=False) |
||||
] |
||||
|
||||
self.mock_store.get_app_services = Mock(return_value=services) |
||||
self.mock_store.get_association_from_room_alias = Mock( |
||||
return_value=Mock(room_id=room_id, servers=servers) |
||||
) |
||||
|
||||
result = yield self.handler.query_room_alias_exists(room_alias) |
||||
|
||||
self.mock_as_api.query_alias.assert_called_once_with( |
||||
interested_service, |
||||
room_alias_str |
||||
) |
||||
self.assertEquals(result.room_id, room_id) |
||||
self.assertEquals(result.servers, servers) |
||||
|
||||
|
||||
|
||||
def _mkservice(self, is_interested): |
||||
service = Mock() |
||||
service.is_interested = Mock(return_value=is_interested) |
||||
service.token = "mock_service_token" |
||||
service.url = "mock_service_url" |
||||
return service |
@ -0,0 +1,110 @@ |
||||
# -*- coding: utf-8 -*- |
||||
# Copyright 2015 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 tests import unittest |
||||
from twisted.internet import defer |
||||
|
||||
from synapse.appservice import ApplicationService |
||||
from synapse.server import HomeServer |
||||
from synapse.storage.appservice import ApplicationServiceStore |
||||
|
||||
from mock import Mock |
||||
from tests.utils import SQLiteMemoryDbPool, MockClock |
||||
|
||||
|
||||
class ApplicationServiceStoreTestCase(unittest.TestCase): |
||||
|
||||
@defer.inlineCallbacks |
||||
def setUp(self): |
||||
db_pool = SQLiteMemoryDbPool() |
||||
yield db_pool.prepare() |
||||
hs = HomeServer( |
||||
"test", db_pool=db_pool, clock=MockClock(), config=Mock() |
||||
) |
||||
self.as_token = "token1" |
||||
db_pool.runQuery( |
||||
"INSERT INTO application_services(token) VALUES(?)", |
||||
(self.as_token,) |
||||
) |
||||
db_pool.runQuery( |
||||
"INSERT INTO application_services(token) VALUES(?)", ("token2",) |
||||
) |
||||
db_pool.runQuery( |
||||
"INSERT INTO application_services(token) VALUES(?)", ("token3",) |
||||
) |
||||
# must be done after inserts |
||||
self.store = ApplicationServiceStore(hs) |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_update_and_retrieval_of_service(self): |
||||
url = "https://matrix.org/appservices/foobar" |
||||
hs_token = "hstok" |
||||
user_regex = ["@foobar_.*:matrix.org"] |
||||
alias_regex = ["#foobar_.*:matrix.org"] |
||||
room_regex = [] |
||||
service = ApplicationService( |
||||
url=url, hs_token=hs_token, token=self.as_token, namespaces={ |
||||
ApplicationService.NS_USERS: user_regex, |
||||
ApplicationService.NS_ALIASES: alias_regex, |
||||
ApplicationService.NS_ROOMS: room_regex |
||||
}) |
||||
yield self.store.update_app_service(service) |
||||
|
||||
stored_service = yield self.store.get_app_service_by_token( |
||||
self.as_token |
||||
) |
||||
self.assertEquals(stored_service.token, self.as_token) |
||||
self.assertEquals(stored_service.url, url) |
||||
self.assertEquals( |
||||
stored_service.namespaces[ApplicationService.NS_ALIASES], |
||||
alias_regex |
||||
) |
||||
self.assertEquals( |
||||
stored_service.namespaces[ApplicationService.NS_ROOMS], |
||||
room_regex |
||||
) |
||||
self.assertEquals( |
||||
stored_service.namespaces[ApplicationService.NS_USERS], |
||||
user_regex |
||||
) |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_retrieve_unknown_service_token(self): |
||||
service = yield self.store.get_app_service_by_token("invalid_token") |
||||
self.assertEquals(service, None) |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_retrieval_of_service(self): |
||||
stored_service = yield self.store.get_app_service_by_token( |
||||
self.as_token |
||||
) |
||||
self.assertEquals(stored_service.token, self.as_token) |
||||
self.assertEquals(stored_service.url, None) |
||||
self.assertEquals( |
||||
stored_service.namespaces[ApplicationService.NS_ALIASES], |
||||
[] |
||||
) |
||||
self.assertEquals( |
||||
stored_service.namespaces[ApplicationService.NS_ROOMS], |
||||
[] |
||||
) |
||||
self.assertEquals( |
||||
stored_service.namespaces[ApplicationService.NS_USERS], |
||||
[] |
||||
) |
||||
|
||||
@defer.inlineCallbacks |
||||
def test_retrieval_of_all_services(self): |
||||
services = yield self.store.get_app_services() |
||||
self.assertEquals(len(services), 3) |
Loading…
Reference in new issue