From 8d7accb28fdf2b8936eb865a716593a2ffcea0ed Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Fri, 23 Jan 2015 19:52:30 +0000 Subject: [PATCH 01/24] Initial minimal attempt at /user/:user_id/filter API - in-memory storage, no actual filter implementation --- synapse/rest/client/v2_alpha/__init__.py | 7 +- synapse/rest/client/v2_alpha/filter.py | 103 +++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 synapse/rest/client/v2_alpha/filter.py diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py index bb740e280..4349579ab 100644 --- a/synapse/rest/client/v2_alpha/__init__.py +++ b/synapse/rest/client/v2_alpha/__init__.py @@ -14,6 +14,11 @@ # limitations under the License. +from . import ( + filter +) + + from synapse.http.server import JsonResource @@ -26,4 +31,4 @@ class ClientV2AlphaRestResource(JsonResource): @staticmethod def register_servlets(client_resource, hs): - pass + filter.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/v2_alpha/filter.py b/synapse/rest/client/v2_alpha/filter.py new file mode 100644 index 000000000..a9a180ec0 --- /dev/null +++ b/synapse/rest/client/v2_alpha/filter.py @@ -0,0 +1,103 @@ +# -*- 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 AuthError, SynapseError +from synapse.http.servlet import RestServlet +from synapse.types import UserID + +from ._base import client_v2_pattern + +import json +import logging + + +logger = logging.getLogger(__name__) + + +# TODO(paul) +_filters_for_user = {} + + +class GetFilterRestServlet(RestServlet): + PATTERN = client_v2_pattern("/user/(?P[^/]*)/filter/(?P[^/]*)") + + def __init__(self, hs): + super(GetFilterRestServlet, self).__init__() + self.hs = hs + self.auth = hs.get_auth() + + @defer.inlineCallbacks + def on_GET(self, request, user_id, filter_id): + target_user = UserID.from_string(user_id) + auth_user = yield self.auth.get_user_by_req(request) + + if target_user != auth_user: + raise AuthError(403, "Cannot get filters for other users") + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only get filters for local users") + + try: + filter_id = int(filter_id) + except: + raise SynapseError(400, "Invalid filter_id") + + filters = _filters_for_user.get(target_user.localpart, None) + + if not filters or filter_id >= len(filters): + raise SynapseError(400, "No such filter") + + defer.returnValue((200, filters[filter_id])) + + +class CreateFilterRestServlet(RestServlet): + PATTERN = client_v2_pattern("/user/(?P[^/]*)/filter") + + def __init__(self, hs): + super(CreateFilterRestServlet, self).__init__() + self.hs = hs + self.auth = hs.get_auth() + + @defer.inlineCallbacks + def on_POST(self, request, user_id): + target_user = UserID.from_string(user_id) + auth_user = yield self.auth.get_user_by_req(request) + + if target_user != auth_user: + raise AuthError(403, "Cannot create filters for other users") + + if not self.hs.is_mine(target_user): + raise SynapseError(400, "Can only create filters for local users") + + try: + content = json.loads(request.content.read()) + + # TODO(paul): check for required keys and invalid keys + except: + raise SynapseError(400, "Invalid filter definition") + + filters = _filters_for_user.setdefault(target_user.localpart, []) + + filter_id = len(filters) + filters.append(content) + + defer.returnValue((200, {"filter_id": str(filter_id)})) + + +def register_servlets(hs, http_server): + GetFilterRestServlet(hs).register(http_server) + CreateFilterRestServlet(hs).register(http_server) From 37b8a71f1086b394874fb13ee63dcbeb2b2334d2 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Mon, 26 Jan 2015 15:27:40 +0000 Subject: [PATCH 02/24] Initial trivial REST test of v2_alpha filter API --- tests/rest/client/v2_alpha/test_filter.py | 74 +++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/rest/client/v2_alpha/test_filter.py diff --git a/tests/rest/client/v2_alpha/test_filter.py b/tests/rest/client/v2_alpha/test_filter.py new file mode 100644 index 000000000..1d1273ab9 --- /dev/null +++ b/tests/rest/client/v2_alpha/test_filter.py @@ -0,0 +1,74 @@ +# -*- 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 ....utils import MockHttpResource, MockKey + +from synapse.server import HomeServer +from synapse.rest.client.v2_alpha import filter +from synapse.types import UserID + + +myid = "@apple:test" +PATH_PREFIX = "/_matrix/client/v2_alpha" + + +class FilterTestCase(unittest.TestCase): + + def setUp(self): + self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) + + mock_config = Mock() + mock_config.signing_key = [MockKey()] + + hs = HomeServer("test", + db_pool=None, + datastore=Mock(spec=[ + "insert_client_ip", + ]), + http_client=None, + resource_for_client=self.mock_resource, + resource_for_federation=self.mock_resource, + config=mock_config, + ) + + def _get_user_by_token(token=None): + return { + "user": UserID.from_string(myid), + "admin": False, + "device_id": None, + } + hs.get_auth().get_user_by_token = _get_user_by_token + + filter.register_servlets(hs, self.mock_resource) + + @defer.inlineCallbacks + def test_filter(self): + (code, response) = yield self.mock_resource.trigger("POST", + "/user/%s/filter" % (myid), + '{"type": ["m.*"]}' + ) + self.assertEquals(200, code) + self.assertEquals({"filter_id": "0"}, response) + + (code, response) = yield self.mock_resource.trigger("GET", + "/user/%s/filter/0" % (myid), None + ) + self.assertEquals(200, code) + self.assertEquals({"type": ["m.*"]}, response) From 39c1892b22012e20ce6f43e92b01f6fad780081d Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 13:03:31 +0000 Subject: [PATCH 03/24] Minor changes to v2_alpha filter REST test to allow the setUp method to be shareable --- tests/rest/client/v2_alpha/test_filter.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/rest/client/v2_alpha/test_filter.py b/tests/rest/client/v2_alpha/test_filter.py index 1d1273ab9..91b19e88f 100644 --- a/tests/rest/client/v2_alpha/test_filter.py +++ b/tests/rest/client/v2_alpha/test_filter.py @@ -25,11 +25,12 @@ from synapse.rest.client.v2_alpha import filter from synapse.types import UserID -myid = "@apple:test" PATH_PREFIX = "/_matrix/client/v2_alpha" class FilterTestCase(unittest.TestCase): + USER_ID = "@apple:test" + TO_REGISTER = [filter] def setUp(self): self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) @@ -50,25 +51,26 @@ class FilterTestCase(unittest.TestCase): def _get_user_by_token(token=None): return { - "user": UserID.from_string(myid), + "user": UserID.from_string(self.USER_ID), "admin": False, "device_id": None, } hs.get_auth().get_user_by_token = _get_user_by_token - filter.register_servlets(hs, self.mock_resource) + for r in self.TO_REGISTER: + r.register_servlets(hs, self.mock_resource) @defer.inlineCallbacks def test_filter(self): (code, response) = yield self.mock_resource.trigger("POST", - "/user/%s/filter" % (myid), + "/user/%s/filter" % (self.USER_ID), '{"type": ["m.*"]}' ) self.assertEquals(200, code) self.assertEquals({"filter_id": "0"}, response) (code, response) = yield self.mock_resource.trigger("GET", - "/user/%s/filter/0" % (myid), None + "/user/%s/filter/0" % (self.USER_ID), None ) self.assertEquals(200, code) self.assertEquals({"type": ["m.*"]}, response) From f9958f34043bf4fdf5923f471f193b40188e67bb Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 13:17:25 +0000 Subject: [PATCH 04/24] Use new V2AlphaRestTestCase --- tests/rest/client/v2_alpha/test_filter.py | 40 ++--------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/tests/rest/client/v2_alpha/test_filter.py b/tests/rest/client/v2_alpha/test_filter.py index 91b19e88f..8629a1aed 100644 --- a/tests/rest/client/v2_alpha/test_filter.py +++ b/tests/rest/client/v2_alpha/test_filter.py @@ -13,53 +13,17 @@ # 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 . import V2AlphaRestTestCase -from ....utils import MockHttpResource, MockKey - -from synapse.server import HomeServer from synapse.rest.client.v2_alpha import filter -from synapse.types import UserID -PATH_PREFIX = "/_matrix/client/v2_alpha" - - -class FilterTestCase(unittest.TestCase): +class FilterTestCase(V2AlphaRestTestCase): USER_ID = "@apple:test" TO_REGISTER = [filter] - def setUp(self): - self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) - - mock_config = Mock() - mock_config.signing_key = [MockKey()] - - hs = HomeServer("test", - db_pool=None, - datastore=Mock(spec=[ - "insert_client_ip", - ]), - http_client=None, - resource_for_client=self.mock_resource, - resource_for_federation=self.mock_resource, - config=mock_config, - ) - - def _get_user_by_token(token=None): - return { - "user": UserID.from_string(self.USER_ID), - "admin": False, - "device_id": None, - } - hs.get_auth().get_user_by_token = _get_user_by_token - - for r in self.TO_REGISTER: - r.register_servlets(hs, self.mock_resource) - @defer.inlineCallbacks def test_filter(self): (code, response) = yield self.mock_resource.trigger("POST", From 05c7cba73a050f19cc52129b65b0183eaa832a42 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 14:28:56 +0000 Subject: [PATCH 05/24] Initial trivial implementation of an actual 'Filtering' object; move storage of user filters into there --- synapse/api/filtering.py | 41 ++++++++++++++++++++++++++ synapse/rest/client/v2_alpha/filter.py | 25 ++++++++-------- synapse/server.py | 5 ++++ 3 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 synapse/api/filtering.py diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py new file mode 100644 index 000000000..922c40004 --- /dev/null +++ b/synapse/api/filtering.py @@ -0,0 +1,41 @@ +# -*- 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. + + +# TODO(paul) +_filters_for_user = {} + + +class Filtering(object): + + def __init__(self, hs): + super(Filtering, self).__init__() + self.hs = hs + + def get_user_filter(self, user_localpart, filter_id): + filters = _filters_for_user.get(user_localpart, None) + + if not filters or filter_id >= len(filters): + raise KeyError() + + return filters[filter_id] + + def add_user_filter(self, user_localpart, definition): + filters = _filters_for_user.setdefault(user_localpart, []) + + filter_id = len(filters) + filters.append(definition) + + return filter_id diff --git a/synapse/rest/client/v2_alpha/filter.py b/synapse/rest/client/v2_alpha/filter.py index a9a180ec0..585c8e02e 100644 --- a/synapse/rest/client/v2_alpha/filter.py +++ b/synapse/rest/client/v2_alpha/filter.py @@ -28,10 +28,6 @@ import logging logger = logging.getLogger(__name__) -# TODO(paul) -_filters_for_user = {} - - class GetFilterRestServlet(RestServlet): PATTERN = client_v2_pattern("/user/(?P[^/]*)/filter/(?P[^/]*)") @@ -39,6 +35,7 @@ class GetFilterRestServlet(RestServlet): super(GetFilterRestServlet, self).__init__() self.hs = hs self.auth = hs.get_auth() + self.filtering = hs.get_filtering() @defer.inlineCallbacks def on_GET(self, request, user_id, filter_id): @@ -56,13 +53,14 @@ class GetFilterRestServlet(RestServlet): except: raise SynapseError(400, "Invalid filter_id") - filters = _filters_for_user.get(target_user.localpart, None) - - if not filters or filter_id >= len(filters): + try: + defer.returnValue((200, self.filtering.get_user_filter( + user_localpart=target_user.localpart, + filter_id=filter_id, + ))) + except KeyError: raise SynapseError(400, "No such filter") - defer.returnValue((200, filters[filter_id])) - class CreateFilterRestServlet(RestServlet): PATTERN = client_v2_pattern("/user/(?P[^/]*)/filter") @@ -71,6 +69,7 @@ class CreateFilterRestServlet(RestServlet): super(CreateFilterRestServlet, self).__init__() self.hs = hs self.auth = hs.get_auth() + self.filtering = hs.get_filtering() @defer.inlineCallbacks def on_POST(self, request, user_id): @@ -90,10 +89,10 @@ class CreateFilterRestServlet(RestServlet): except: raise SynapseError(400, "Invalid filter definition") - filters = _filters_for_user.setdefault(target_user.localpart, []) - - filter_id = len(filters) - filters.append(content) + filter_id = self.filtering.add_user_filter( + user_localpart=target_user.localpart, + definition=content, + ) defer.returnValue((200, {"filter_id": str(filter_id)})) diff --git a/synapse/server.py b/synapse/server.py index f09d5d581..9b42079e0 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -32,6 +32,7 @@ from synapse.streams.events import EventSources from synapse.api.ratelimiting import Ratelimiter from synapse.crypto.keyring import Keyring from synapse.events.builder import EventBuilderFactory +from synapse.api.filtering import Filtering class BaseHomeServer(object): @@ -79,6 +80,7 @@ class BaseHomeServer(object): 'ratelimiter', 'keyring', 'event_builder_factory', + 'filtering', ] def __init__(self, hostname, **kwargs): @@ -197,3 +199,6 @@ class HomeServer(BaseHomeServer): clock=self.get_clock(), hostname=self.hostname, ) + + def build_filtering(self): + return Filtering(self) From b1503112ce77e573aa8cfb7581ca4a916c7d018c Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 15:56:14 +0000 Subject: [PATCH 06/24] Initial trivial unittest of Filtering object --- tests/api/test_filtering.py | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/api/test_filtering.py diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py new file mode 100644 index 000000000..c6c531769 --- /dev/null +++ b/tests/api/test_filtering.py @@ -0,0 +1,67 @@ +# -*- 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, NonCallableMock +from tests.utils import ( + MockHttpResource, MockClock, DeferredMockCallable, SQLiteMemoryDbPool, + MockKey +) + +from synapse.server import HomeServer + + +user_localpart = "test_user" + +class FilteringTestCase(unittest.TestCase): + + @defer.inlineCallbacks + def setUp(self): + db_pool = SQLiteMemoryDbPool() + yield db_pool.prepare() + + self.mock_config = NonCallableMock() + self.mock_config.signing_key = [MockKey()] + + self.mock_federation_resource = MockHttpResource() + + self.mock_http_client = Mock(spec=[]) + self.mock_http_client.put_json = DeferredMockCallable() + + hs = HomeServer("test", + db_pool=db_pool, + handlers=None, + http_client=self.mock_http_client, + config=self.mock_config, + keyring=Mock(), + ) + + self.filtering = hs.get_filtering() + + def test_filter(self): + filter_id = self.filtering.add_user_filter( + user_localpart=user_localpart, + definition={"type": ["m.*"]}, + ) + self.assertEquals(filter_id, 0) + + filter = self.filtering.get_user_filter( + user_localpart=user_localpart, + filter_id=filter_id, + ) + self.assertEquals(filter, {"type": ["m.*"]}) From 059651efa19a88eb0823bce1d5beff2d95cb01c2 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 16:17:56 +0000 Subject: [PATCH 07/24] Have the Filtering API return Deferreds, so we can do the Datastore implementation nicely --- synapse/api/filtering.py | 16 ++++++++++++++-- synapse/rest/client/v2_alpha/filter.py | 8 +++++--- tests/api/test_filtering.py | 5 +++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 922c40004..014e2e1fc 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from twisted.internet import defer + # TODO(paul) _filters_for_user = {} @@ -24,18 +26,28 @@ class Filtering(object): super(Filtering, self).__init__() self.hs = hs + @defer.inlineCallbacks def get_user_filter(self, user_localpart, filter_id): filters = _filters_for_user.get(user_localpart, None) if not filters or filter_id >= len(filters): raise KeyError() - return filters[filter_id] + # trivial yield to make it a generator so d.iC works + yield + defer.returnValue(filters[filter_id]) + @defer.inlineCallbacks def add_user_filter(self, user_localpart, definition): filters = _filters_for_user.setdefault(user_localpart, []) filter_id = len(filters) filters.append(definition) - return filter_id + # trivial yield, see above + yield + defer.returnValue(filter_id) + + # TODO(paul): surely we should probably add a delete_user_filter or + # replace_user_filter at some point? There's no REST API specified for + # them however diff --git a/synapse/rest/client/v2_alpha/filter.py b/synapse/rest/client/v2_alpha/filter.py index 585c8e02e..09e44e8ae 100644 --- a/synapse/rest/client/v2_alpha/filter.py +++ b/synapse/rest/client/v2_alpha/filter.py @@ -54,10 +54,12 @@ class GetFilterRestServlet(RestServlet): raise SynapseError(400, "Invalid filter_id") try: - defer.returnValue((200, self.filtering.get_user_filter( + filter = yield self.filtering.get_user_filter( user_localpart=target_user.localpart, filter_id=filter_id, - ))) + ) + + defer.returnValue((200, filter)) except KeyError: raise SynapseError(400, "No such filter") @@ -89,7 +91,7 @@ class CreateFilterRestServlet(RestServlet): except: raise SynapseError(400, "Invalid filter definition") - filter_id = self.filtering.add_user_filter( + filter_id = yield self.filtering.add_user_filter( user_localpart=target_user.localpart, definition=content, ) diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index c6c531769..fecadd105 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -53,14 +53,15 @@ class FilteringTestCase(unittest.TestCase): self.filtering = hs.get_filtering() + @defer.inlineCallbacks def test_filter(self): - filter_id = self.filtering.add_user_filter( + filter_id = yield self.filtering.add_user_filter( user_localpart=user_localpart, definition={"type": ["m.*"]}, ) self.assertEquals(filter_id, 0) - filter = self.filtering.get_user_filter( + filter = yield self.filtering.get_user_filter( user_localpart=user_localpart, filter_id=filter_id, ) From 54e513b4e6b5c644b9a2aeb02cef8258e87ae26a Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 17:48:13 +0000 Subject: [PATCH 08/24] Move storage of user filters into real datastore layer; now have to mock it out in the REST-level tests --- synapse/api/filtering.py | 27 ++----------- synapse/storage/__init__.py | 3 +- synapse/storage/filtering.py | 46 +++++++++++++++++++++++ tests/rest/client/v2_alpha/__init__.py | 9 +++-- tests/rest/client/v2_alpha/test_filter.py | 21 +++++++++++ 5 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 synapse/storage/filtering.py diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 014e2e1fc..20b6951d4 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -16,37 +16,18 @@ from twisted.internet import defer -# TODO(paul) -_filters_for_user = {} - - class Filtering(object): def __init__(self, hs): super(Filtering, self).__init__() - self.hs = hs + self.store = hs.get_datastore() - @defer.inlineCallbacks def get_user_filter(self, user_localpart, filter_id): - filters = _filters_for_user.get(user_localpart, None) + return self.store.get_user_filter(user_localpart, filter_id) - if not filters or filter_id >= len(filters): - raise KeyError() - - # trivial yield to make it a generator so d.iC works - yield - defer.returnValue(filters[filter_id]) - - @defer.inlineCallbacks def add_user_filter(self, user_localpart, definition): - filters = _filters_for_user.setdefault(user_localpart, []) - - filter_id = len(filters) - filters.append(definition) - - # trivial yield, see above - yield - defer.returnValue(filter_id) + # TODO(paul): implement sanity checking of the definition + return self.store.add_user_filter(user_localpart, definition) # TODO(paul): surely we should probably add a delete_user_filter or # replace_user_filter at some point? There's no REST API specified for diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 4beb951b9..efa63031b 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -30,9 +30,9 @@ from .transactions import TransactionStore from .keys import KeyStore from .event_federation import EventFederationStore from .media_repository import MediaRepositoryStore - from .state import StateStore from .signatures import SignatureStore +from .filtering import FilteringStore from syutil.base64util import decode_base64 from syutil.jsonutil import encode_canonical_json @@ -82,6 +82,7 @@ class DataStore(RoomMemberStore, RoomStore, DirectoryStore, KeyStore, StateStore, SignatureStore, EventFederationStore, MediaRepositoryStore, + FilteringStore, ): def __init__(self, hs): diff --git a/synapse/storage/filtering.py b/synapse/storage/filtering.py new file mode 100644 index 000000000..18e0e7c29 --- /dev/null +++ b/synapse/storage/filtering.py @@ -0,0 +1,46 @@ +# -*- 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 ._base import SQLBaseStore + + +# TODO(paul) +_filters_for_user = {} + + +class FilteringStore(SQLBaseStore): + @defer.inlineCallbacks + def get_user_filter(self, user_localpart, filter_id): + filters = _filters_for_user.get(user_localpart, None) + + if not filters or filter_id >= len(filters): + raise KeyError() + + # trivial yield to make it a generator so d.iC works + yield + defer.returnValue(filters[filter_id]) + + @defer.inlineCallbacks + def add_user_filter(self, user_localpart, definition): + filters = _filters_for_user.setdefault(user_localpart, []) + + filter_id = len(filters) + filters.append(definition) + + # trivial yield, see above + yield + defer.returnValue(filter_id) diff --git a/tests/rest/client/v2_alpha/__init__.py b/tests/rest/client/v2_alpha/__init__.py index f59745e13..3fe62d5ac 100644 --- a/tests/rest/client/v2_alpha/__init__.py +++ b/tests/rest/client/v2_alpha/__init__.py @@ -39,9 +39,7 @@ class V2AlphaRestTestCase(unittest.TestCase): hs = HomeServer("test", db_pool=None, - datastore=Mock(spec=[ - "insert_client_ip", - ]), + datastore=self.make_datastore_mock(), http_client=None, resource_for_client=self.mock_resource, resource_for_federation=self.mock_resource, @@ -58,3 +56,8 @@ class V2AlphaRestTestCase(unittest.TestCase): for r in self.TO_REGISTER: r.register_servlets(hs, self.mock_resource) + + def make_datastore_mock(self): + return Mock(spec=[ + "insert_client_ip", + ]) diff --git a/tests/rest/client/v2_alpha/test_filter.py b/tests/rest/client/v2_alpha/test_filter.py index 8629a1aed..1add727e6 100644 --- a/tests/rest/client/v2_alpha/test_filter.py +++ b/tests/rest/client/v2_alpha/test_filter.py @@ -15,6 +15,8 @@ from twisted.internet import defer +from mock import Mock + from . import V2AlphaRestTestCase from synapse.rest.client.v2_alpha import filter @@ -24,6 +26,25 @@ class FilterTestCase(V2AlphaRestTestCase): USER_ID = "@apple:test" TO_REGISTER = [filter] + def make_datastore_mock(self): + datastore = super(FilterTestCase, self).make_datastore_mock() + + self._user_filters = {} + + def add_user_filter(user_localpart, definition): + filters = self._user_filters.setdefault(user_localpart, []) + filter_id = len(filters) + filters.append(definition) + return defer.succeed(filter_id) + datastore.add_user_filter = add_user_filter + + def get_user_filter(user_localpart, filter_id): + filters = self._user_filters[user_localpart] + return defer.succeed(filters[filter_id]) + datastore.get_user_filter = get_user_filter + + return datastore + @defer.inlineCallbacks def test_filter(self): (code, response) = yield self.mock_resource.trigger("POST", From 0c14a699bb4e103a1845b0808821138cfea99552 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 18:07:21 +0000 Subject: [PATCH 09/24] More unit-testing of REST errors --- tests/rest/client/v2_alpha/test_filter.py | 36 ++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/rest/client/v2_alpha/test_filter.py b/tests/rest/client/v2_alpha/test_filter.py index 1add727e6..80ddabf81 100644 --- a/tests/rest/client/v2_alpha/test_filter.py +++ b/tests/rest/client/v2_alpha/test_filter.py @@ -21,6 +21,8 @@ from . import V2AlphaRestTestCase from synapse.rest.client.v2_alpha import filter +from synapse.api.errors import StoreError + class FilterTestCase(V2AlphaRestTestCase): USER_ID = "@apple:test" @@ -39,14 +41,18 @@ class FilterTestCase(V2AlphaRestTestCase): datastore.add_user_filter = add_user_filter def get_user_filter(user_localpart, filter_id): + if user_localpart not in self._user_filters: + raise StoreError(404, "No user") filters = self._user_filters[user_localpart] + if filter_id >= len(filters): + raise StoreError(404, "No filter") return defer.succeed(filters[filter_id]) datastore.get_user_filter = get_user_filter return datastore @defer.inlineCallbacks - def test_filter(self): + def test_add_filter(self): (code, response) = yield self.mock_resource.trigger("POST", "/user/%s/filter" % (self.USER_ID), '{"type": ["m.*"]}' @@ -54,8 +60,36 @@ class FilterTestCase(V2AlphaRestTestCase): self.assertEquals(200, code) self.assertEquals({"filter_id": "0"}, response) + self.assertIn("apple", self._user_filters) + self.assertEquals(len(self._user_filters["apple"]), 1) + self.assertEquals({"type": ["m.*"]}, self._user_filters["apple"][0]) + + @defer.inlineCallbacks + def test_get_filter(self): + self._user_filters["apple"] = [ + {"type": ["m.*"]} + ] + (code, response) = yield self.mock_resource.trigger("GET", "/user/%s/filter/0" % (self.USER_ID), None ) self.assertEquals(200, code) self.assertEquals({"type": ["m.*"]}, response) + + @defer.inlineCallbacks + def test_get_filter_no_id(self): + self._user_filters["apple"] = [ + {"type": ["m.*"]} + ] + + (code, response) = yield self.mock_resource.trigger("GET", + "/user/%s/filter/2" % (self.USER_ID), None + ) + self.assertEquals(404, code) + + @defer.inlineCallbacks + def test_get_filter_no_user(self): + (code, response) = yield self.mock_resource.trigger("GET", + "/user/%s/filter/0" % (self.USER_ID), None + ) + self.assertEquals(404, code) From 06cc1470129d443f71bfc81ba716f63b9505467d Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 18:46:03 +0000 Subject: [PATCH 10/24] Initial stab at real SQL storage implementation of user filter definitions --- synapse/storage/__init__.py | 1 + synapse/storage/filtering.py | 49 ++++++++++++++++++++-------- synapse/storage/schema/filtering.sql | 24 ++++++++++++++ tests/api/test_filtering.py | 19 ++++++++++- 4 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 synapse/storage/schema/filtering.sql diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index efa63031b..7c5631d01 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -61,6 +61,7 @@ SCHEMAS = [ "event_edges", "event_signatures", "media_repository", + "filtering", ] diff --git a/synapse/storage/filtering.py b/synapse/storage/filtering.py index 18e0e7c29..e98eaf803 100644 --- a/synapse/storage/filtering.py +++ b/synapse/storage/filtering.py @@ -17,6 +17,8 @@ from twisted.internet import defer from ._base import SQLBaseStore +import json + # TODO(paul) _filters_for_user = {} @@ -25,22 +27,41 @@ _filters_for_user = {} class FilteringStore(SQLBaseStore): @defer.inlineCallbacks def get_user_filter(self, user_localpart, filter_id): - filters = _filters_for_user.get(user_localpart, None) + def_json = yield self._simple_select_one_onecol( + table="user_filters", + keyvalues={ + "user_id": user_localpart, + "filter_id": filter_id, + }, + retcol="definition", + allow_none=False, + ) - if not filters or filter_id >= len(filters): - raise KeyError() + defer.returnValue(json.loads(def_json)) - # trivial yield to make it a generator so d.iC works - yield - defer.returnValue(filters[filter_id]) - - @defer.inlineCallbacks def add_user_filter(self, user_localpart, definition): - filters = _filters_for_user.setdefault(user_localpart, []) + def_json = json.dumps(definition) - filter_id = len(filters) - filters.append(definition) + # Need an atomic transaction to SELECT the maximal ID so far then + # INSERT a new one + def _do_txn(txn): + sql = ( + "SELECT MAX(filter_id) FROM user_filters " + "WHERE user_id = ?" + ) + txn.execute(sql, (user_localpart,)) + max_id = txn.fetchone()[0] + if max_id is None: + filter_id = 0 + else: + filter_id = max_id + 1 - # trivial yield, see above - yield - defer.returnValue(filter_id) + sql = ( + "INSERT INTO user_filters (user_id, filter_id, definition)" + "VALUES(?, ?, ?)" + ) + txn.execute(sql, (user_localpart, filter_id, def_json)) + + return filter_id + + return self.runInteraction("add_user_filter", _do_txn) diff --git a/synapse/storage/schema/filtering.sql b/synapse/storage/schema/filtering.sql new file mode 100644 index 000000000..795aca4af --- /dev/null +++ b/synapse/storage/schema/filtering.sql @@ -0,0 +1,24 @@ +/* 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 user_filters( + user_id TEXT, + filter_id INTEGER, + definition TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS user_filters_by_user_id_filter_id ON user_filters( + user_id, filter_id +); diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index fecadd105..149948374 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -53,16 +53,33 @@ class FilteringTestCase(unittest.TestCase): self.filtering = hs.get_filtering() + self.datastore = hs.get_datastore() + @defer.inlineCallbacks - def test_filter(self): + def test_add_filter(self): filter_id = yield self.filtering.add_user_filter( user_localpart=user_localpart, definition={"type": ["m.*"]}, ) + self.assertEquals(filter_id, 0) + self.assertEquals({"type": ["m.*"]}, + (yield self.datastore.get_user_filter( + user_localpart=user_localpart, + filter_id=0, + )) + ) + + @defer.inlineCallbacks + def test_get_filter(self): + filter_id = yield self.datastore.add_user_filter( + user_localpart=user_localpart, + definition={"type": ["m.*"]}, + ) filter = yield self.filtering.get_user_filter( user_localpart=user_localpart, filter_id=filter_id, ) + self.assertEquals(filter, {"type": ["m.*"]}) From 8398f19bcea8fb0134b37efa303dc65b017d75ce Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 27 Jan 2015 19:00:09 +0000 Subject: [PATCH 11/24] Created schema delta --- synapse/storage/__init__.py | 2 +- synapse/storage/schema/delta/v12.sql | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 synapse/storage/schema/delta/v12.sql diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 7c5631d01..00a04f565 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -67,7 +67,7 @@ SCHEMAS = [ # Remember to update this number every time an incompatible change is made to # database schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 11 +SCHEMA_VERSION = 12 class _RollbackButIsFineException(Exception): diff --git a/synapse/storage/schema/delta/v12.sql b/synapse/storage/schema/delta/v12.sql new file mode 100644 index 000000000..795aca4af --- /dev/null +++ b/synapse/storage/schema/delta/v12.sql @@ -0,0 +1,24 @@ +/* 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 user_filters( + user_id TEXT, + filter_id INTEGER, + definition TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS user_filters_by_user_id_filter_id ON user_filters( + user_id, filter_id +); From c23e3db544eb940d95a092b661e3872480f3bf30 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 28 Jan 2015 16:45:18 +0000 Subject: [PATCH 12/24] Add filter JSON sanity checks. --- synapse/api/filtering.py | 109 ++++++++++++++++++++++++- synapse/rest/client/v2_alpha/filter.py | 2 +- synapse/storage/filtering.py | 4 +- tests/api/test_filtering.py | 24 +++++- 4 files changed, 128 insertions(+), 11 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 20b6951d4..6c7a73b6d 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -13,7 +13,8 @@ # 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 +from synapse.types import UserID, RoomID class Filtering(object): @@ -25,10 +26,110 @@ class Filtering(object): def get_user_filter(self, user_localpart, filter_id): return self.store.get_user_filter(user_localpart, filter_id) - def add_user_filter(self, user_localpart, definition): - # TODO(paul): implement sanity checking of the definition - return self.store.add_user_filter(user_localpart, definition) + def add_user_filter(self, user_localpart, user_filter): + self._check_valid_filter(user_filter) + return self.store.add_user_filter(user_localpart, user_filter) # TODO(paul): surely we should probably add a delete_user_filter or # replace_user_filter at some point? There's no REST API specified for # them however + + def _check_valid_filter(self, user_filter): + """Check if the provided filter is valid. + + This inspects all definitions contained within the filter. + + Args: + user_filter(dict): The filter + Raises: + SynapseError: If the filter is not valid. + """ + # NB: Filters are the complete json blobs. "Definitions" are an + # individual top-level key e.g. public_user_data. Filters are made of + # many definitions. + + top_level_definitions = [ + "public_user_data", "private_user_data", "server_data" + ] + + room_level_definitions = [ + "state", "events", "ephemeral" + ] + + for key in top_level_definitions: + if key in user_filter: + self._check_definition(user_filter[key]) + + if "room" in user_filter: + for key in room_level_definitions: + if key in user_filter["room"]: + self._check_definition(user_filter["room"][key]) + + + def _check_definition(self, definition): + """Check if the provided definition is valid. + + This inspects not only the types but also the values to make sure they + make sense. + + Args: + definition(dict): The filter definition + Raises: + SynapseError: If there was a problem with this definition. + """ + # NB: Filters are the complete json blobs. "Definitions" are an + # individual top-level key e.g. public_user_data. Filters are made of + # many definitions. + if type(definition) != dict: + raise SynapseError( + 400, "Expected JSON object, not %s" % (definition,) + ) + + # check rooms are valid room IDs + room_id_keys = ["rooms", "not_rooms"] + for key in room_id_keys: + if key in definition: + if type(definition[key]) != list: + raise SynapseError(400, "Expected %s to be a list." % key) + for room_id in definition[key]: + RoomID.from_string(room_id) + + # check senders are valid user IDs + user_id_keys = ["senders", "not_senders"] + for key in user_id_keys: + if key in definition: + if type(definition[key]) != list: + raise SynapseError(400, "Expected %s to be a list." % key) + for user_id in definition[key]: + UserID.from_string(user_id) + + # TODO: We don't limit event type values but we probably should... + # check types are valid event types + event_keys = ["types", "not_types"] + for key in event_keys: + if key in definition: + if type(definition[key]) != list: + raise SynapseError(400, "Expected %s to be a list." % key) + for event_type in definition[key]: + if not isinstance(event_type, basestring): + raise SynapseError(400, "Event type should be a string") + + try: + event_format = definition["format"] + if event_format not in ["federation", "events"]: + raise SynapseError(400, "Invalid format: %s" % (event_format,)) + except KeyError: + pass # format is optional + + try: + event_select_list = definition["select"] + for select_key in event_select_list: + if select_key not in ["event_id", "origin_server_ts", + "thread_id", "content", "content.body"]: + raise SynapseError(400, "Bad select: %s" % (select_key,)) + except KeyError: + pass # select is optional + + if ("bundle_updates" in definition and + type(definition["bundle_updates"]) != bool): + raise SynapseError(400, "Bad bundle_updates: expected bool.") diff --git a/synapse/rest/client/v2_alpha/filter.py b/synapse/rest/client/v2_alpha/filter.py index 09e44e8ae..81a3e9515 100644 --- a/synapse/rest/client/v2_alpha/filter.py +++ b/synapse/rest/client/v2_alpha/filter.py @@ -93,7 +93,7 @@ class CreateFilterRestServlet(RestServlet): filter_id = yield self.filtering.add_user_filter( user_localpart=target_user.localpart, - definition=content, + user_filter=content, ) defer.returnValue((200, {"filter_id": str(filter_id)})) diff --git a/synapse/storage/filtering.py b/synapse/storage/filtering.py index e98eaf803..bab68a9ee 100644 --- a/synapse/storage/filtering.py +++ b/synapse/storage/filtering.py @@ -39,8 +39,8 @@ class FilteringStore(SQLBaseStore): defer.returnValue(json.loads(def_json)) - def add_user_filter(self, user_localpart, definition): - def_json = json.dumps(definition) + def add_user_filter(self, user_localpart, user_filter): + def_json = json.dumps(user_filter) # Need an atomic transaction to SELECT the maximal ID so far then # INSERT a new one diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 149948374..188fbfb91 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -57,13 +57,21 @@ class FilteringTestCase(unittest.TestCase): @defer.inlineCallbacks def test_add_filter(self): + user_filter = { + "room": { + "state": { + "types": ["m.*"] + } + } + } + filter_id = yield self.filtering.add_user_filter( user_localpart=user_localpart, - definition={"type": ["m.*"]}, + user_filter=user_filter, ) self.assertEquals(filter_id, 0) - self.assertEquals({"type": ["m.*"]}, + self.assertEquals(user_filter, (yield self.datastore.get_user_filter( user_localpart=user_localpart, filter_id=0, @@ -72,9 +80,17 @@ class FilteringTestCase(unittest.TestCase): @defer.inlineCallbacks def test_get_filter(self): + user_filter = { + "room": { + "state": { + "types": ["m.*"] + } + } + } + filter_id = yield self.datastore.add_user_filter( user_localpart=user_localpart, - definition={"type": ["m.*"]}, + user_filter=user_filter, ) filter = yield self.filtering.get_user_filter( @@ -82,4 +98,4 @@ class FilteringTestCase(unittest.TestCase): filter_id=filter_id, ) - self.assertEquals(filter, {"type": ["m.*"]}) + self.assertEquals(filter, user_filter) From 11634017f47779d784325da5513517ad76b0dbc1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 28 Jan 2015 17:42:19 +0000 Subject: [PATCH 13/24] s/definition/filter_json/ since definition is now used to mean a component of the filter, rather than the complete json --- synapse/storage/filtering.py | 4 ++-- synapse/storage/schema/filtering.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/storage/filtering.py b/synapse/storage/filtering.py index bab68a9ee..cb01c2040 100644 --- a/synapse/storage/filtering.py +++ b/synapse/storage/filtering.py @@ -33,7 +33,7 @@ class FilteringStore(SQLBaseStore): "user_id": user_localpart, "filter_id": filter_id, }, - retcol="definition", + retcol="filter_json", allow_none=False, ) @@ -57,7 +57,7 @@ class FilteringStore(SQLBaseStore): filter_id = max_id + 1 sql = ( - "INSERT INTO user_filters (user_id, filter_id, definition)" + "INSERT INTO user_filters (user_id, filter_id, filter_json)" "VALUES(?, ?, ?)" ) txn.execute(sql, (user_localpart, filter_id, def_json)) diff --git a/synapse/storage/schema/filtering.sql b/synapse/storage/schema/filtering.sql index 795aca4af..beb39ca20 100644 --- a/synapse/storage/schema/filtering.sql +++ b/synapse/storage/schema/filtering.sql @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS user_filters( user_id TEXT, filter_id INTEGER, - definition TEXT, + filter_json TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ); From 8552ed8df2990d79b0015e0e84dd98de25fd0a9d Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Jan 2015 18:04:40 +0000 Subject: [PATCH 14/24] Change uses of get_user_by_req because it returns a tuple now. --- synapse/rest/client/v1/push_rule.py | 6 +++--- synapse/rest/client/v1/pusher.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py index 2b33bdac0..64743a2f4 100644 --- a/synapse/rest/client/v1/push_rule.py +++ b/synapse/rest/client/v1/push_rule.py @@ -138,7 +138,7 @@ class PushRuleRestServlet(ClientV1RestServlet): except InvalidRuleException as e: raise SynapseError(400, e.message) - user = yield self.auth.get_user_by_req(request) + user, _ = yield self.auth.get_user_by_req(request) content = _parse_json(request) @@ -184,7 +184,7 @@ class PushRuleRestServlet(ClientV1RestServlet): except InvalidRuleException as e: raise SynapseError(400, e.message) - user = yield self.auth.get_user_by_req(request) + 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( @@ -215,7 +215,7 @@ class PushRuleRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_GET(self, request): - user = yield self.auth.get_user_by_req(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 diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py index 80a11890a..72d5e9e47 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/v1/pusher.py @@ -27,7 +27,7 @@ class PusherRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_POST(self, request): - user = yield self.auth.get_user_by_req(request) + user, _ = yield self.auth.get_user_by_req(request) content = _parse_json(request) From d5bdf3c0c7958e6a080f9ec4b38a51428717d02a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Jan 2015 18:06:04 +0000 Subject: [PATCH 15/24] Allow the push rule delete method to take more specifiers. --- synapse/storage/push_rule.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/synapse/storage/push_rule.py b/synapse/storage/push_rule.py index 0342996ed..c7b553292 100644 --- a/synapse/storage/push_rule.py +++ b/synapse/storage/push_rule.py @@ -175,14 +175,17 @@ class PushRuleStore(SQLBaseStore): txn.execute(sql, new_rule.values()) @defer.inlineCallbacks - def delete_push_rule(self, user_name, rule_id): - yield self._simple_delete_one( - PushRuleTable.table_name, - { - 'user_name': user_name, - 'rule_id': rule_id - } - ) + def delete_push_rule(self, user_name, rule_id, **kwargs): + """ + Delete a push rule. Args specify the row to be deleted and can be + any of the columns in the push_rule table, but below are the + standard ones + + Args: + user_name (str): The matrix ID of the push rule owner + rule_id (str): The rule_id of the rule to be deleted + """ + yield self._simple_delete_one(PushRuleTable.table_name, kwargs) class RuleNotFoundException(Exception): From b0b80074e04dcb2b70dff56b0368060a19c065d3 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 29 Jan 2015 01:48:36 +0000 Subject: [PATCH 16/24] SYN-252: Supply the stream and topological parts in the correct order to the constructor --- synapse/storage/stream.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index 8ac2adab0..062ca06fb 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -82,10 +82,10 @@ class _StreamToken(namedtuple("_StreamToken", "topological stream")): def parse(cls, string): try: if string[0] == 's': - return cls(None, int(string[1:])) + return cls(topological=None, stream=int(string[1:])) if string[0] == 't': parts = string[1:].split('-', 1) - return cls(int(parts[1]), int(parts[0])) + return cls(topological=int(parts[0]), stream=int(parts[1])) except: pass raise SynapseError(400, "Invalid token %r" % (string,)) @@ -94,7 +94,7 @@ class _StreamToken(namedtuple("_StreamToken", "topological stream")): def parse_stream_token(cls, string): try: if string[0] == 's': - return cls(None, int(string[1:])) + return cls(topological=None, stream=int(string[1:])) except: pass raise SynapseError(400, "Invalid token %r" % (string,)) From 3773759c0f1e25e6905e23368f770da99ceb3ea0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 29 Jan 2015 09:15:33 +0000 Subject: [PATCH 17/24] Also edit the filter column on the delta SQL --- synapse/storage/schema/delta/v12.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/schema/delta/v12.sql b/synapse/storage/schema/delta/v12.sql index 795aca4af..beb39ca20 100644 --- a/synapse/storage/schema/delta/v12.sql +++ b/synapse/storage/schema/delta/v12.sql @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS user_filters( user_id TEXT, filter_id INTEGER, - definition TEXT, + filter_json TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ); From 2a4fda7b88cf91db8de2e524df162153d3f27094 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 29 Jan 2015 09:27:16 +0000 Subject: [PATCH 18/24] Add filtering.filter_events function, with stub passes_filter function. --- synapse/api/filtering.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 6c7a73b6d..d7ba6510e 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -34,6 +34,21 @@ class Filtering(object): # replace_user_filter at some point? There's no REST API specified for # them however + def passes_filter(self, filter_json, event): + """Check if the event passes through the filter. + + Args: + filter_json(dict): The filter specification + event(Event): The event to check + Returns: + True if the event passes through the filter. + """ + return True + + def filter_events(self, events, user, filter_id): + filter_json = self.get_user_filter(user, filter_id) + return [e for e in events if self.passes_filter(filter_json, e)] + def _check_valid_filter(self, user_filter): """Check if the provided filter is valid. From 50de1eaad94715a1dda470f44f379683e5fa552b Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 29 Jan 2015 10:24:57 +0000 Subject: [PATCH 19/24] Add filtering public API; outline filtering algorithm. --- synapse/api/filtering.py | 60 ++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index d7ba6510e..21fe72d6c 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -30,25 +30,69 @@ class Filtering(object): self._check_valid_filter(user_filter) return self.store.add_user_filter(user_localpart, user_filter) + def filter_public_user_data(self, events, user, filter_id): + return self._filter_on_key( + events, user, filter_id, ["public_user_data"] + ) + + def filter_private_user_data(self, events, user, filter_id): + return self._filter_on_key( + events, user, filter_id, ["private_user_data"] + ) + + def filter_room_state(self, events, user, filter_id): + return self._filter_on_key( + events, user, filter_id, ["room", "state"] + ) + + def filter_room_events(self, events, user, filter_id): + return self._filter_on_key( + events, user, filter_id, ["room", "events"] + ) + + def filter_room_ephemeral(self, events, user, filter_id): + return self._filter_on_key( + events, user, filter_id, ["room", "ephemeral"] + ) + # TODO(paul): surely we should probably add a delete_user_filter or # replace_user_filter at some point? There's no REST API specified for # them however - def passes_filter(self, filter_json, event): - """Check if the event passes through the filter. + def _filter_on_key(self, events, user, filter_id, keys): + filter_json = self.get_user_filter(user.localpart, filter_id) + if not filter_json: + return events + + try: + # extract the right definition from the filter + definition = filter_json + for key in keys: + definition = definition[key] + return self._filter_with_definition(events, definition) + except KeyError: + return events # return all events if definition isn't specified. + + def _filter_with_definition(self, events, definition): + return [e for e in events if self._passes_definition(definition, e)] + + def _passes_definition(self, definition, event): + """Check if the event passes through the given definition. Args: - filter_json(dict): The filter specification - event(Event): The event to check + definition(dict): The definition to check against. + event(Event): The event to check. Returns: True if the event passes through the filter. """ + # Algorithm notes: + # For each key in the definition, check the event meets the criteria: + # * For types: Literal match or prefix match (if ends with wildcard) + # * For senders/rooms: Literal match only + # * "not_" checks take presedence (e.g. if "m.*" is in both 'types' + # and 'not_types' then it is treated as only being in 'not_types') return True - def filter_events(self, events, user, filter_id): - filter_json = self.get_user_filter(user, filter_id) - return [e for e in events if self.passes_filter(filter_json, e)] - def _check_valid_filter(self, user_filter): """Check if the provided filter is valid. From 777d9914b537d06ebba91948a26d74d3a04b7284 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 29 Jan 2015 11:38:06 +0000 Subject: [PATCH 20/24] Implement filter algorithm. Add basic event type unit tests to assert it works. --- synapse/api/filtering.py | 49 +++++++++++++++++++++++++++++++++++++ tests/api/test_filtering.py | 45 +++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 21fe72d6c..8bc95aa39 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -91,8 +91,57 @@ class Filtering(object): # * For senders/rooms: Literal match only # * "not_" checks take presedence (e.g. if "m.*" is in both 'types' # and 'not_types' then it is treated as only being in 'not_types') + + # room checks + if hasattr(event, "room_id"): + room_id = event.room_id + allow_rooms = definition["rooms"] if "rooms" in definition else None + reject_rooms = ( + definition["not_rooms"] if "not_rooms" in definition else None + ) + if reject_rooms and room_id in reject_rooms: + return False + if allow_rooms and room_id not in allow_rooms: + return False + + # sender checks + if hasattr(event, "sender"): + # Should we be including event.state_key for some event types? + sender = event.sender + allow_senders = ( + definition["senders"] if "senders" in definition else None + ) + reject_senders = ( + definition["not_senders"] if "not_senders" in definition else None + ) + if reject_senders and sender in reject_senders: + return False + if allow_senders and sender not in allow_senders: + return False + + # type checks + if "not_types" in definition: + for def_type in definition["not_types"]: + if self._event_matches_type(event, def_type): + return False + if "types" in definition: + included = False + for def_type in definition["types"]: + if self._event_matches_type(event, def_type): + included = True + break + if not included: + return False + return True + def _event_matches_type(self, event, def_type): + if def_type.endswith("*"): + type_prefix = def_type[:-1] + return event.type.startswith(type_prefix) + else: + return event.type == def_type + def _check_valid_filter(self, user_filter): """Check if the provided filter is valid. diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 188fbfb91..4d40d88b0 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - +from collections import namedtuple from tests import unittest from twisted.internet import defer @@ -27,6 +27,7 @@ from synapse.server import HomeServer user_localpart = "test_user" +MockEvent = namedtuple("MockEvent", "sender type room_id") class FilteringTestCase(unittest.TestCase): @@ -55,6 +56,48 @@ class FilteringTestCase(unittest.TestCase): self.datastore = hs.get_datastore() + def test_definition_include_literal_types(self): + definition = { + "types": ["m.room.message", "org.matrix.foo.bar"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!foo:bar" + ) + + self.assertTrue( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_include_wildcard_types(self): + definition = { + "types": ["m.*", "org.matrix.foo.bar"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!foo:bar" + ) + + self.assertTrue( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_exclude_unknown_types(self): + definition = { + "types": ["m.room.message", "org.matrix.foo.bar"] + } + event = MockEvent( + sender="@foo:bar", + type="now.for.something.completely.different", + room_id="!foo:bar" + ) + + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + @defer.inlineCallbacks def test_add_filter(self): user_filter = { From 5561a879205316ae2c4cd0106cfd99d4fe35bceb Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 29 Jan 2015 12:06:16 +0000 Subject: [PATCH 21/24] Add more unit tests for the filter algorithm. --- tests/api/test_filtering.py | 264 +++++++++++++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 5 deletions(-) diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 4d40d88b0..380dd9793 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -56,7 +56,7 @@ class FilteringTestCase(unittest.TestCase): self.datastore = hs.get_datastore() - def test_definition_include_literal_types(self): + def test_definition_types_works_with_literals(self): definition = { "types": ["m.room.message", "org.matrix.foo.bar"] } @@ -65,12 +65,11 @@ class FilteringTestCase(unittest.TestCase): type="m.room.message", room_id="!foo:bar" ) - self.assertTrue( self.filtering._passes_definition(definition, event) ) - def test_definition_include_wildcard_types(self): + def test_definition_types_works_with_wildcards(self): definition = { "types": ["m.*", "org.matrix.foo.bar"] } @@ -79,12 +78,11 @@ class FilteringTestCase(unittest.TestCase): type="m.room.message", room_id="!foo:bar" ) - self.assertTrue( self.filtering._passes_definition(definition, event) ) - def test_definition_exclude_unknown_types(self): + def test_definition_types_works_with_unknowns(self): definition = { "types": ["m.room.message", "org.matrix.foo.bar"] } @@ -93,7 +91,263 @@ class FilteringTestCase(unittest.TestCase): type="now.for.something.completely.different", room_id="!foo:bar" ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + def test_definition_not_types_works_with_literals(self): + definition = { + "not_types": ["m.room.message", "org.matrix.foo.bar"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!foo:bar" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_types_works_with_wildcards(self): + definition = { + "not_types": ["m.room.message", "org.matrix.*"] + } + event = MockEvent( + sender="@foo:bar", + type="org.matrix.custom.event", + room_id="!foo:bar" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_types_works_with_unknowns(self): + definition = { + "not_types": ["m.*", "org.*"] + } + event = MockEvent( + sender="@foo:bar", + type="com.nom.nom.nom", + room_id="!foo:bar" + ) + self.assertTrue( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_types_takes_priority_over_types(self): + definition = { + "not_types": ["m.*", "org.*"], + "types": ["m.room.message", "m.room.topic"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.topic", + room_id="!foo:bar" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_senders_works_with_literals(self): + definition = { + "senders": ["@flibble:wibble"] + } + event = MockEvent( + sender="@flibble:wibble", + type="com.nom.nom.nom", + room_id="!foo:bar" + ) + self.assertTrue( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_senders_works_with_unknowns(self): + definition = { + "senders": ["@flibble:wibble"] + } + event = MockEvent( + sender="@challenger:appears", + type="com.nom.nom.nom", + room_id="!foo:bar" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_senders_works_with_literals(self): + definition = { + "not_senders": ["@flibble:wibble"] + } + event = MockEvent( + sender="@flibble:wibble", + type="com.nom.nom.nom", + room_id="!foo:bar" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_senders_works_with_unknowns(self): + definition = { + "not_senders": ["@flibble:wibble"] + } + event = MockEvent( + sender="@challenger:appears", + type="com.nom.nom.nom", + room_id="!foo:bar" + ) + self.assertTrue( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_senders_takes_priority_over_senders(self): + definition = { + "not_senders": ["@misspiggy:muppets"], + "senders": ["@kermit:muppets", "@misspiggy:muppets"] + } + event = MockEvent( + sender="@misspiggy:muppets", + type="m.room.topic", + room_id="!foo:bar" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_rooms_works_with_literals(self): + definition = { + "rooms": ["!secretbase:unknown"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!secretbase:unknown" + ) + self.assertTrue( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_rooms_works_with_unknowns(self): + definition = { + "rooms": ["!secretbase:unknown"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!anothersecretbase:unknown" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_rooms_works_with_literals(self): + definition = { + "not_rooms": ["!anothersecretbase:unknown"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!anothersecretbase:unknown" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_rooms_works_with_unknowns(self): + definition = { + "not_rooms": ["!secretbase:unknown"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!anothersecretbase:unknown" + ) + self.assertTrue( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_not_rooms_takes_priority_over_rooms(self): + definition = { + "not_rooms": ["!secretbase:unknown"], + "rooms": ["!secretbase:unknown"] + } + event = MockEvent( + sender="@foo:bar", + type="m.room.message", + room_id="!secretbase:unknown" + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_combined_event(self): + definition = { + "not_senders": ["@misspiggy:muppets"], + "senders": ["@kermit:muppets"], + "rooms": ["!stage:unknown"], + "not_rooms": ["!piggyshouse:muppets"], + "types": ["m.room.message", "muppets.kermit.*"], + "not_types": ["muppets.misspiggy.*"] + } + event = MockEvent( + sender="@kermit:muppets", # yup + type="m.room.message", # yup + room_id="!stage:unknown" # yup + ) + self.assertTrue( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_combined_event_bad_sender(self): + definition = { + "not_senders": ["@misspiggy:muppets"], + "senders": ["@kermit:muppets"], + "rooms": ["!stage:unknown"], + "not_rooms": ["!piggyshouse:muppets"], + "types": ["m.room.message", "muppets.kermit.*"], + "not_types": ["muppets.misspiggy.*"] + } + event = MockEvent( + sender="@misspiggy:muppets", # nope + type="m.room.message", # yup + room_id="!stage:unknown" # yup + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_combined_event_bad_room(self): + definition = { + "not_senders": ["@misspiggy:muppets"], + "senders": ["@kermit:muppets"], + "rooms": ["!stage:unknown"], + "not_rooms": ["!piggyshouse:muppets"], + "types": ["m.room.message", "muppets.kermit.*"], + "not_types": ["muppets.misspiggy.*"] + } + event = MockEvent( + sender="@kermit:muppets", # yup + type="m.room.message", # yup + room_id="!piggyshouse:muppets" # nope + ) + self.assertFalse( + self.filtering._passes_definition(definition, event) + ) + + def test_definition_combined_event_bad_type(self): + definition = { + "not_senders": ["@misspiggy:muppets"], + "senders": ["@kermit:muppets"], + "rooms": ["!stage:unknown"], + "not_rooms": ["!piggyshouse:muppets"], + "types": ["m.room.message", "muppets.kermit.*"], + "not_types": ["muppets.misspiggy.*"] + } + event = MockEvent( + sender="@kermit:muppets", # yup + type="muppets.misspiggy.kisses", # nope + room_id="!stage:unknown" # yup + ) self.assertFalse( self.filtering._passes_definition(definition, event) ) From 83172487b05d7d99ccae0b353daee2f242445011 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 29 Jan 2015 12:20:59 +0000 Subject: [PATCH 22/24] Add basic filtering public API unit tests. Use defers in the right places. --- synapse/api/filtering.py | 11 +++++--- tests/api/test_filtering.py | 54 ++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 8bc95aa39..7e239138b 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -12,6 +12,7 @@ # 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 from synapse.types import UserID, RoomID @@ -59,19 +60,21 @@ class Filtering(object): # replace_user_filter at some point? There's no REST API specified for # them however + @defer.inlineCallbacks def _filter_on_key(self, events, user, filter_id, keys): - filter_json = self.get_user_filter(user.localpart, filter_id) + filter_json = yield self.get_user_filter(user.localpart, filter_id) if not filter_json: - return events + defer.returnValue(events) try: # extract the right definition from the filter definition = filter_json for key in keys: definition = definition[key] - return self._filter_with_definition(events, definition) + defer.returnValue(self._filter_with_definition(events, definition)) except KeyError: - return events # return all events if definition isn't specified. + # return all events if definition isn't specified. + defer.returnValue(events) def _filter_with_definition(self, events, definition): return [e for e in events if self._passes_definition(definition, e)] diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 380dd9793..97fb9758e 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -24,7 +24,7 @@ from tests.utils import ( ) from synapse.server import HomeServer - +from synapse.types import UserID user_localpart = "test_user" MockEvent = namedtuple("MockEvent", "sender type room_id") @@ -352,6 +352,58 @@ class FilteringTestCase(unittest.TestCase): self.filtering._passes_definition(definition, event) ) + @defer.inlineCallbacks + def test_filter_public_user_data_match(self): + user_filter = { + "public_user_data": { + "types": ["m.*"] + } + } + user = UserID.from_string("@" + user_localpart + ":test") + filter_id = yield self.datastore.add_user_filter( + user_localpart=user_localpart, + user_filter=user_filter, + ) + event = MockEvent( + sender="@foo:bar", + type="m.profile", + room_id="!foo:bar" + ) + events = [event] + + results = yield self.filtering.filter_public_user_data( + events=events, + user=user, + filter_id=filter_id + ) + self.assertEquals(events, results) + + @defer.inlineCallbacks + def test_filter_public_user_data_no_match(self): + user_filter = { + "public_user_data": { + "types": ["m.*"] + } + } + user = UserID.from_string("@" + user_localpart + ":test") + filter_id = yield self.datastore.add_user_filter( + user_localpart=user_localpart, + user_filter=user_filter, + ) + event = MockEvent( + sender="@foo:bar", + type="custom.avatar.3d.crazy", + room_id="!foo:bar" + ) + events = [event] + + results = yield self.filtering.filter_public_user_data( + events=events, + user=user, + filter_id=filter_id + ) + self.assertEquals([], results) + @defer.inlineCallbacks def test_add_filter(self): user_filter = { From 38b27bd2cbf38141938d6170c41e1d1dac9928cd Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 29 Jan 2015 14:28:34 +0000 Subject: [PATCH 23/24] Add filter_room_state unit tests. --- tests/api/test_filtering.py | 56 +++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 97fb9758e..aa93616a9 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -404,6 +404,62 @@ class FilteringTestCase(unittest.TestCase): ) self.assertEquals([], results) + @defer.inlineCallbacks + def test_filter_room_state_match(self): + user_filter = { + "room": { + "state": { + "types": ["m.*"] + } + } + } + user = UserID.from_string("@" + user_localpart + ":test") + filter_id = yield self.datastore.add_user_filter( + user_localpart=user_localpart, + user_filter=user_filter, + ) + event = MockEvent( + sender="@foo:bar", + type="m.room.topic", + room_id="!foo:bar" + ) + events = [event] + + results = yield self.filtering.filter_room_state( + events=events, + user=user, + filter_id=filter_id + ) + self.assertEquals(events, results) + + @defer.inlineCallbacks + def test_filter_room_state_no_match(self): + user_filter = { + "room": { + "state": { + "types": ["m.*"] + } + } + } + user = UserID.from_string("@" + user_localpart + ":test") + filter_id = yield self.datastore.add_user_filter( + user_localpart=user_localpart, + user_filter=user_filter, + ) + event = MockEvent( + sender="@foo:bar", + type="org.matrix.custom.event", + room_id="!foo:bar" + ) + events = [event] + + results = yield self.filtering.filter_room_state( + events=events, + user=user, + filter_id=filter_id + ) + self.assertEquals([], results) + @defer.inlineCallbacks def test_add_filter(self): user_filter = { From e4f50fa0aa3426a272b1526072c4c42802989ba4 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Thu, 29 Jan 2015 14:53:18 +0000 Subject: [PATCH 24/24] Move bump schema delta --- synapse/storage/schema/delta/{v12.sql => v13.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename synapse/storage/schema/delta/{v12.sql => v13.sql} (100%) diff --git a/synapse/storage/schema/delta/v12.sql b/synapse/storage/schema/delta/v13.sql similarity index 100% rename from synapse/storage/schema/delta/v12.sql rename to synapse/storage/schema/delta/v13.sql