From 1dffa78701d43b299419090d544fb8bb91ab4d5b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 12:21:59 +0000 Subject: [PATCH 01/14] Filter events_before and events_after in /context requests While the current version of the spec doesn't say much about how this endpoint uses filters (see https://github.com/matrix-org/matrix-doc/issues/2338), the current implementation is that some fields of an EventFilter apply (the ones that are used when running the SQL query) and others don't (the ones that are used by the filter itself) because we don't call event_filter.filter(...). This seems counter-intuitive and probably not what we want so this commit fixes it. --- synapse/handlers/room.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index e92b2eafd5..899bb63114 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -874,8 +874,10 @@ class RoomContextHandler(object): room_id, event_id, before_limit, after_limit, event_filter ) - results["events_before"] = yield filter_evts(results["events_before"]) - results["events_after"] = yield filter_evts(results["events_after"]) + filtered_before_events = event_filter.filter(results["events_before"]) + results["events_before"] = yield filter_evts(filtered_before_events) + filtered_after_events = event_filter.filter(results["events_after"]) + results["events_after"] = yield filter_evts(filtered_after_events) results["event"] = event if results["events_after"]: From a7c818c79b70d6b70abc5b26f0e1e78fd60c087e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 13:21:26 +0000 Subject: [PATCH 02/14] Add test case --- tests/rest/client/v1/test_rooms.py | 182 +++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 5e38fd6ced..621c894e35 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1106,3 +1106,185 @@ class PerRoomProfilesForbiddenTestCase(unittest.HomeserverTestCase): res_displayname = channel.json_body["content"]["displayname"] self.assertEqual(res_displayname, self.displayname, channel.result) + + +class ContextTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + profile.register_servlets, + ] + + def test_context_filter_labels(self): + """Test that we can filter by a label.""" + context_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.labels": ["#fun"], + } + ) + + res = self._test_context_filter_labels(context_filter) + + self.assertEqual( + res["event"]["content"]["body"], "with right label", res["event"] + ) + + events_before = res["events_before"] + + self.assertEqual( + len(events_before), 1, [event["content"] for event in events_before] + ) + self.assertEqual( + events_before[0]["content"]["body"], "with right label", events_before[0] + ) + + events_after = res["events_before"] + + self.assertEqual( + len(events_after), 1, [event["content"] for event in events_after] + ) + self.assertEqual( + events_after[0]["content"]["body"], "with right label", events_after[0] + ) + + def test_context_filter_not_labels(self): + """Test that we can filter by the absence of a label.""" + context_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.not_labels": ["#fun"], + } + ) + + res = self._test_context_filter_labels(context_filter) + + events_before = res["events_before"] + + self.assertEqual( + len(events_before), 1, [event["content"] for event in events_before] + ) + self.assertEqual( + events_before[0]["content"]["body"], "without label", events_before[0] + ) + + events_after = res["events_after"] + + self.assertEqual( + len(events_after), 2, [event["content"] for event in events_after] + ) + self.assertEqual( + events_after[0]["content"]["body"], "with wrong label", events_after[0] + ) + self.assertEqual( + events_after[1]["content"]["body"], "with two wrong labels", events_after[1] + ) + + def test_context_filter_labels_not_labels(self): + """Test that we can filter by both a label and the absence of another label.""" + context_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + } + ) + + res = self._test_context_filter_labels(context_filter) + + events_before = res["events_before"] + + self.assertEqual( + len(events_before), 0, [event["content"] for event in events_before] + ) + + events_after = res["events_after"] + + self.assertEqual( + len(events_after), 1, [event["content"] for event in events_after] + ) + self.assertEqual( + events_after[0]["content"]["body"], "with wrong label", events_after[0] + ) + + def _test_context_filter_labels(self, context_filter): + user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test") + + room_id = self.helper.create_room_as(user_id, tok=tok) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", + EventContentFields.LABELS: ["#fun"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={"msgtype": "m.text", "body": "without label"}, + tok=tok, + ) + + # The event we'll look up the context for. + res = self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", + EventContentFields.LABELS: ["#fun"], + }, + tok=tok, + ) + event_id = res["event_id"] + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with wrong label", + EventContentFields.LABELS: ["#work"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with two wrong labels", + EventContentFields.LABELS: ["#work", "#notfun"], + }, + tok=tok, + ) + + self.helper.send_event( + room_id=room_id, + type=EventTypes.Message, + content={ + "msgtype": "m.text", + "body": "with right label", + EventContentFields.LABELS: ["#fun"], + }, + tok=tok, + ) + + request, channel = self.make_request( + "GET", + "/rooms/%s/context/%s?filter=%s" % (room_id, event_id, context_filter), + access_token=tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + return channel.json_body + From c9e4748cb75271a2178d0cae05d551829249ada3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 13:47:47 +0000 Subject: [PATCH 03/14] Merge labels tests for /context and /messages --- tests/rest/client/v1/test_rooms.py | 276 ++++++++++++++--------------- 1 file changed, 130 insertions(+), 146 deletions(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 621c894e35..fe327d1bf8 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -811,105 +811,6 @@ class RoomMessageListTestCase(RoomBase): self.assertTrue("chunk" in channel.json_body) self.assertTrue("end" in channel.json_body) - def test_filter_labels(self): - """Test that we can filter by a label.""" - message_filter = json.dumps( - {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} - ) - - events = self._test_filter_labels(message_filter) - - self.assertEqual(len(events), 2, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "with right label", events[0]) - self.assertEqual(events[1]["content"]["body"], "with right label", events[1]) - - def test_filter_not_labels(self): - """Test that we can filter by the absence of a label.""" - message_filter = json.dumps( - {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} - ) - - events = self._test_filter_labels(message_filter) - - self.assertEqual(len(events), 3, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "without label", events[0]) - self.assertEqual(events[1]["content"]["body"], "with wrong label", events[1]) - self.assertEqual( - events[2]["content"]["body"], "with two wrong labels", events[2] - ) - - def test_filter_labels_not_labels(self): - """Test that we can filter by both a label and the absence of another label.""" - sync_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.labels": ["#work"], - "org.matrix.not_labels": ["#notfun"], - } - ) - - events = self._test_filter_labels(sync_filter) - - self.assertEqual(len(events), 1, [event["content"] for event in events]) - self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) - - def _test_filter_labels(self, message_filter): - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with right label", - EventContentFields.LABELS: ["#fun"], - }, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={"msgtype": "m.text", "body": "without label"}, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with wrong label", - EventContentFields.LABELS: ["#work"], - }, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with two wrong labels", - EventContentFields.LABELS: ["#work", "#notfun"], - }, - ) - - self.helper.send_event( - room_id=self.room_id, - type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with right label", - EventContentFields.LABELS: ["#fun"], - }, - ) - - token = "s0_0_0_0_0_0_0_0_0" - request, channel = self.make_request( - "GET", - "/rooms/%s/messages?access_token=x&from=%s&filter=%s" - % (self.room_id, token, message_filter), - ) - self.render(request) - - return channel.json_body["chunk"] - class RoomSearchTestCase(unittest.HomeserverTestCase): servlets = [ @@ -1108,7 +1009,7 @@ class PerRoomProfilesForbiddenTestCase(unittest.HomeserverTestCase): self.assertEqual(res_displayname, self.displayname, channel.result) -class ContextTestCase(unittest.HomeserverTestCase): +class LabelsTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets_for_client_rest_resource, room.register_servlets, @@ -1116,8 +1017,13 @@ class ContextTestCase(unittest.HomeserverTestCase): profile.register_servlets, ] + def prepare(self, reactor, clock, homeserver): + self.user_id = self.register_user("test", "test") + self.tok = self.login("test", "test") + self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + def test_context_filter_labels(self): - """Test that we can filter by a label.""" + """Test that we can filter by a label on a /context request.""" context_filter = json.dumps( { "types": [EventTypes.Message], @@ -1125,13 +1031,17 @@ class ContextTestCase(unittest.HomeserverTestCase): } ) - res = self._test_context_filter_labels(context_filter) + event_id = self._send_labelled_messages_in_room() - self.assertEqual( - res["event"]["content"]["body"], "with right label", res["event"] + request, channel = self.make_request( + "GET", + "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + access_token=self.tok, ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) - events_before = res["events_before"] + events_before = channel.json_body["events_before"] self.assertEqual( len(events_before), 1, [event["content"] for event in events_before] @@ -1140,7 +1050,7 @@ class ContextTestCase(unittest.HomeserverTestCase): events_before[0]["content"]["body"], "with right label", events_before[0] ) - events_after = res["events_before"] + events_after = channel.json_body["events_before"] self.assertEqual( len(events_after), 1, [event["content"] for event in events_after] @@ -1150,7 +1060,7 @@ class ContextTestCase(unittest.HomeserverTestCase): ) def test_context_filter_not_labels(self): - """Test that we can filter by the absence of a label.""" + """Test that we can filter by the absence of a label on a /context request.""" context_filter = json.dumps( { "types": [EventTypes.Message], @@ -1158,9 +1068,17 @@ class ContextTestCase(unittest.HomeserverTestCase): } ) - res = self._test_context_filter_labels(context_filter) + event_id = self._send_labelled_messages_in_room() - events_before = res["events_before"] + request, channel = self.make_request( + "GET", + "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + access_token=self.tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + events_before = channel.json_body["events_before"] self.assertEqual( len(events_before), 1, [event["content"] for event in events_before] @@ -1169,7 +1087,7 @@ class ContextTestCase(unittest.HomeserverTestCase): events_before[0]["content"]["body"], "without label", events_before[0] ) - events_after = res["events_after"] + events_after = channel.json_body["events_after"] self.assertEqual( len(events_after), 2, [event["content"] for event in events_after] @@ -1182,7 +1100,9 @@ class ContextTestCase(unittest.HomeserverTestCase): ) def test_context_filter_labels_not_labels(self): - """Test that we can filter by both a label and the absence of another label.""" + """Test that we can filter by both a label and the absence of another label on a + /context request. + """ context_filter = json.dumps( { "types": [EventTypes.Message], @@ -1191,15 +1111,23 @@ class ContextTestCase(unittest.HomeserverTestCase): } ) - res = self._test_context_filter_labels(context_filter) + event_id = self._send_labelled_messages_in_room() - events_before = res["events_before"] + request, channel = self.make_request( + "GET", + "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + access_token=self.tok, + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.result) + + events_before = channel.json_body["events_before"] self.assertEqual( len(events_before), 0, [event["content"] for event in events_before] ) - events_after = res["events_after"] + events_after = channel.json_body["events_after"] self.assertEqual( len(events_after), 1, [event["content"] for event in events_after] @@ -1208,83 +1136,139 @@ class ContextTestCase(unittest.HomeserverTestCase): events_after[0]["content"]["body"], "with wrong label", events_after[0] ) - def _test_context_filter_labels(self, context_filter): - user_id = self.register_user("kermit", "test") - tok = self.login("kermit", "test") + def test_messages_filter_labels(self): + """Test that we can filter by a label on a /messages request.""" + message_filter = json.dumps( + {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} + ) - room_id = self.helper.create_room_as(user_id, tok=tok) + self._send_labelled_messages_in_room() + token = "s0_0_0_0_0_0_0_0_0" + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" + % (self.room_id, self.tok, token, message_filter), + ) + self.render(request) + + events = channel.json_body["chunk"] + + self.assertEqual(len(events), 2, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "with right label", events[0]) + self.assertEqual(events[1]["content"]["body"], "with right label", events[1]) + + def test_messages_filter_not_labels(self): + """Test that we can filter by the absence of a label on a /messages request.""" + message_filter = json.dumps( + {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} + ) + + self._send_labelled_messages_in_room() + + token = "s0_0_0_0_0_0_0_0_0" + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" + % (self.room_id, self.tok, token, message_filter), + ) + self.render(request) + + events = channel.json_body["chunk"] + + self.assertEqual(len(events), 4, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "without label", events[0]) + self.assertEqual(events[1]["content"]["body"], "without label", events[1]) + self.assertEqual(events[2]["content"]["body"], "with wrong label", events[2]) + self.assertEqual( + events[3]["content"]["body"], "with two wrong labels", events[3] + ) + + def test_messages_filter_labels_not_labels(self): + """Test that we can filter by both a label and the absence of another label on a + /messages request. + """ + message_filter = json.dumps( + { + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + } + ) + + self._send_labelled_messages_in_room() + + token = "s0_0_0_0_0_0_0_0_0" + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" + % (self.room_id, self.tok, token, message_filter), + ) + self.render(request) + + events = channel.json_body["chunk"] + + self.assertEqual(len(events), 1, [event["content"] for event in events]) + self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) + + def _send_labelled_messages_in_room(self): self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={ "msgtype": "m.text", "body": "with right label", EventContentFields.LABELS: ["#fun"], }, - tok=tok, + tok=self.tok, ) self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={"msgtype": "m.text", "body": "without label"}, - tok=tok, + tok=self.tok, ) - # The event we'll look up the context for. res = self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, - content={ - "msgtype": "m.text", - "body": "with right label", - EventContentFields.LABELS: ["#fun"], - }, - tok=tok, + content={"msgtype": "m.text", "body": "without label"}, + tok=self.tok, ) event_id = res["event_id"] self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={ "msgtype": "m.text", "body": "with wrong label", EventContentFields.LABELS: ["#work"], }, - tok=tok, + tok=self.tok, ) self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={ "msgtype": "m.text", "body": "with two wrong labels", EventContentFields.LABELS: ["#work", "#notfun"], }, - tok=tok, + tok=self.tok, ) self.helper.send_event( - room_id=room_id, + room_id=self.room_id, type=EventTypes.Message, content={ "msgtype": "m.text", "body": "with right label", EventContentFields.LABELS: ["#fun"], }, - tok=tok, + tok=self.tok, ) - request, channel = self.make_request( - "GET", - "/rooms/%s/context/%s?filter=%s" % (room_id, event_id, context_filter), - access_token=tok, - ) - self.render(request) - self.assertEqual(channel.code, 200, channel.result) - - return channel.json_body - + return event_id From 037360e6cf2ca181b7cf03884375d4a4d52ad64e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:33:18 +0000 Subject: [PATCH 04/14] Add tests for /search --- tests/rest/client/v1/test_rooms.py | 187 ++++++++++++++++++++++------- 1 file changed, 143 insertions(+), 44 deletions(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index fe327d1bf8..cc7499dcc0 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1017,6 +1017,18 @@ class LabelsTestCase(unittest.HomeserverTestCase): profile.register_servlets, ] + # Filter that should only catch messages with the label "#fun". + FILTER_LABELS = {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} + # Filter that should only catch messages without the label "#fun". + FILTER_NOT_LABELS = {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} + # Filter that should only catch messages with the label "#work" but without the label + # "#notfun". + FILTER_LABELS_NOT_LABELS = { + "types": [EventTypes.Message], + "org.matrix.labels": ["#work"], + "org.matrix.not_labels": ["#notfun"], + } + def prepare(self, reactor, clock, homeserver): self.user_id = self.register_user("test", "test") self.tok = self.login("test", "test") @@ -1024,18 +1036,12 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_context_filter_labels(self): """Test that we can filter by a label on a /context request.""" - context_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.labels": ["#fun"], - } - ) - event_id = self._send_labelled_messages_in_room() request, channel = self.make_request( "GET", - "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + "/rooms/%s/context/%s?filter=%s" + % (self.room_id, event_id, json.dumps(self.FILTER_LABELS)), access_token=self.tok, ) self.render(request) @@ -1061,18 +1067,12 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_context_filter_not_labels(self): """Test that we can filter by the absence of a label on a /context request.""" - context_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.not_labels": ["#fun"], - } - ) - event_id = self._send_labelled_messages_in_room() request, channel = self.make_request( "GET", - "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + "/rooms/%s/context/%s?filter=%s" + % (self.room_id, event_id, json.dumps(self.FILTER_NOT_LABELS)), access_token=self.tok, ) self.render(request) @@ -1103,19 +1103,12 @@ class LabelsTestCase(unittest.HomeserverTestCase): """Test that we can filter by both a label and the absence of another label on a /context request. """ - context_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.labels": ["#work"], - "org.matrix.not_labels": ["#notfun"], - } - ) - event_id = self._send_labelled_messages_in_room() request, channel = self.make_request( "GET", - "/rooms/%s/context/%s?filter=%s" % (self.room_id, event_id, context_filter), + "/rooms/%s/context/%s?filter=%s" + % (self.room_id, event_id, json.dumps(self.FILTER_LABELS_NOT_LABELS)), access_token=self.tok, ) self.render(request) @@ -1138,17 +1131,13 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_messages_filter_labels(self): """Test that we can filter by a label on a /messages request.""" - message_filter = json.dumps( - {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} - ) - self._send_labelled_messages_in_room() token = "s0_0_0_0_0_0_0_0_0" request, channel = self.make_request( "GET", "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, message_filter), + % (self.room_id, self.tok, token, json.dumps(self.FILTER_LABELS)), ) self.render(request) @@ -1160,17 +1149,13 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_messages_filter_not_labels(self): """Test that we can filter by the absence of a label on a /messages request.""" - message_filter = json.dumps( - {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} - ) - self._send_labelled_messages_in_room() token = "s0_0_0_0_0_0_0_0_0" request, channel = self.make_request( "GET", "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, message_filter), + % (self.room_id, self.tok, token, json.dumps(self.FILTER_NOT_LABELS)), ) self.render(request) @@ -1188,21 +1173,13 @@ class LabelsTestCase(unittest.HomeserverTestCase): """Test that we can filter by both a label and the absence of another label on a /messages request. """ - message_filter = json.dumps( - { - "types": [EventTypes.Message], - "org.matrix.labels": ["#work"], - "org.matrix.not_labels": ["#notfun"], - } - ) - self._send_labelled_messages_in_room() token = "s0_0_0_0_0_0_0_0_0" request, channel = self.make_request( "GET", "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, message_filter), + % (self.room_id, self.tok, token, json.dumps(self.FILTER_LABELS_NOT_LABELS)), ) self.render(request) @@ -1211,7 +1188,128 @@ class LabelsTestCase(unittest.HomeserverTestCase): self.assertEqual(len(events), 1, [event["content"] for event in events]) self.assertEqual(events[0]["content"]["body"], "with wrong label", events[0]) + def test_search_filter_labels(self): + """Test that we can filter by a label on a /search request.""" + request_data = json.dumps({ + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_LABELS, + } + } + }) + + self._send_labelled_messages_in_room() + + request, channel = self.make_request( + "POST", "/search?access_token=%s" % self.tok, request_data + ) + self.render(request) + + results = channel.json_body["search_categories"]["room_events"]["results"] + + self.assertEqual( + len(results), + 2, + [result["result"]["content"] for result in results], + ) + self.assertEqual( + results[0]["result"]["content"]["body"], + "with right label", + results[0]["result"]["content"]["body"], + ) + self.assertEqual( + results[1]["result"]["content"]["body"], + "with right label", + results[1]["result"]["content"]["body"], + ) + + def test_search_filter_not_labels(self): + """Test that we can filter by the absence of a label on a /search request.""" + request_data = json.dumps({ + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_NOT_LABELS, + } + } + }) + + self._send_labelled_messages_in_room() + + request, channel = self.make_request( + "POST", "/search?access_token=%s" % self.tok, request_data + ) + self.render(request) + + results = channel.json_body["search_categories"]["room_events"]["results"] + + self.assertEqual( + len(results), + 4, + [result["result"]["content"] for result in results], + ) + self.assertEqual( + results[0]["result"]["content"]["body"], + "without label", + results[0]["result"]["content"]["body"], + ) + self.assertEqual( + results[1]["result"]["content"]["body"], + "without label", + results[1]["result"]["content"]["body"], + ) + self.assertEqual( + results[2]["result"]["content"]["body"], + "with wrong label", + results[2]["result"]["content"]["body"], + ) + self.assertEqual( + results[3]["result"]["content"]["body"], + "with two wrong labels", + results[3]["result"]["content"]["body"], + ) + + def test_search_filter_labels_not_labels(self): + """Test that we can filter by both a label and the absence of another label on a + /search request. + """ + request_data = json.dumps({ + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_LABELS_NOT_LABELS, + } + } + }) + + self._send_labelled_messages_in_room() + + request, channel = self.make_request( + "POST", "/search?access_token=%s" % self.tok, request_data + ) + self.render(request) + + results = channel.json_body["search_categories"]["room_events"]["results"] + + self.assertEqual( + len(results), + 1, + [result["result"]["content"] for result in results], + ) + self.assertEqual( + results[0]["result"]["content"]["body"], + "with wrong label", + results[0]["result"]["content"]["body"], + ) + def _send_labelled_messages_in_room(self): + """Sends several messages to a room with different labels (or without any) to test + filtering by label. + + Returns: + The ID of the event to use if we're testing filtering on /context. + """ self.helper.send_event( room_id=self.room_id, type=EventTypes.Message, @@ -1236,6 +1334,7 @@ class LabelsTestCase(unittest.HomeserverTestCase): content={"msgtype": "m.text", "body": "without label"}, tok=self.tok, ) + # Return this event's ID when we test filtering in /context requests. event_id = res["event_id"] self.helper.send_event( From d8d808db64c3464924016fab88879085d6c63880 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:42:05 +0000 Subject: [PATCH 05/14] Changelog --- changelog.d/6329.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6329.feature diff --git a/changelog.d/6329.feature b/changelog.d/6329.feature new file mode 100644 index 0000000000..78a187a1dc --- /dev/null +++ b/changelog.d/6329.feature @@ -0,0 +1 @@ +Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). From 8822b331114a2f6fdcd5916f0c91991c0acae07e Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 10:56:39 +0000 Subject: [PATCH 06/14] Update copyrights --- synapse/api/constants.py | 3 ++- synapse/api/filtering.py | 3 +++ synapse/rest/client/versions.py | 3 +++ synapse/storage/data_stores/main/stream.py | 3 +++ tests/api/test_filtering.py | 3 +++ tests/rest/client/v1/test_rooms.py | 2 ++ tests/rest/client/v1/utils.py | 3 +++ tests/rest/client/v2_alpha/test_sync.py | 3 ++- 8 files changed, 21 insertions(+), 2 deletions(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 49c4b85054..312acff3d6 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index bec13f08d8..6eab1f13f0 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index bb30ce3f34..2a477ad22e 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/storage/data_stores/main/stream.py b/synapse/storage/data_stores/main/stream.py index 616ef91d4e..9cac664880 100644 --- a/synapse/storage/data_stores/main/stream.py +++ b/synapse/storage/data_stores/main/stream.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index 2dc5052249..63d8633582 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index cc7499dcc0..b2c1ef6f0e 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index 8ea0cb05ea..e7417b3d14 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd +# Copyright 2017 Vector Creations Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 3283c0e47b..661c1f88b9 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright 2018 New Vector +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From a6863da24934dcbb2ae09a9e0b6e37140ef390ff Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:50:19 +0000 Subject: [PATCH 07/14] Lint --- tests/rest/client/v1/test_rooms.py | 71 +++++++++++++++++------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index b2c1ef6f0e..c5d67fc1cd 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1020,9 +1020,15 @@ class LabelsTestCase(unittest.HomeserverTestCase): ] # Filter that should only catch messages with the label "#fun". - FILTER_LABELS = {"types": [EventTypes.Message], "org.matrix.labels": ["#fun"]} + FILTER_LABELS = { + "types": [EventTypes.Message], + "org.matrix.labels": ["#fun"], + } # Filter that should only catch messages without the label "#fun". - FILTER_NOT_LABELS = {"types": [EventTypes.Message], "org.matrix.not_labels": ["#fun"]} + FILTER_NOT_LABELS = { + "types": [EventTypes.Message], + "org.matrix.not_labels": ["#fun"], + } # Filter that should only catch messages with the label "#work" but without the label # "#notfun". FILTER_LABELS_NOT_LABELS = { @@ -1181,7 +1187,12 @@ class LabelsTestCase(unittest.HomeserverTestCase): request, channel = self.make_request( "GET", "/rooms/%s/messages?access_token=%s&from=%s&filter=%s" - % (self.room_id, self.tok, token, json.dumps(self.FILTER_LABELS_NOT_LABELS)), + % ( + self.room_id, + self.tok, + token, + json.dumps(self.FILTER_LABELS_NOT_LABELS), + ), ) self.render(request) @@ -1192,14 +1203,16 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_search_filter_labels(self): """Test that we can filter by a label on a /search request.""" - request_data = json.dumps({ - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_LABELS, + request_data = json.dumps( + { + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_LABELS, + } } } - }) + ) self._send_labelled_messages_in_room() @@ -1211,9 +1224,7 @@ class LabelsTestCase(unittest.HomeserverTestCase): results = channel.json_body["search_categories"]["room_events"]["results"] self.assertEqual( - len(results), - 2, - [result["result"]["content"] for result in results], + len(results), 2, [result["result"]["content"] for result in results], ) self.assertEqual( results[0]["result"]["content"]["body"], @@ -1228,14 +1239,16 @@ class LabelsTestCase(unittest.HomeserverTestCase): def test_search_filter_not_labels(self): """Test that we can filter by the absence of a label on a /search request.""" - request_data = json.dumps({ - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_NOT_LABELS, + request_data = json.dumps( + { + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_NOT_LABELS, + } } } - }) + ) self._send_labelled_messages_in_room() @@ -1247,9 +1260,7 @@ class LabelsTestCase(unittest.HomeserverTestCase): results = channel.json_body["search_categories"]["room_events"]["results"] self.assertEqual( - len(results), - 4, - [result["result"]["content"] for result in results], + len(results), 4, [result["result"]["content"] for result in results], ) self.assertEqual( results[0]["result"]["content"]["body"], @@ -1276,14 +1287,16 @@ class LabelsTestCase(unittest.HomeserverTestCase): """Test that we can filter by both a label and the absence of another label on a /search request. """ - request_data = json.dumps({ - "search_categories": { - "room_events": { - "search_term": "label", - "filter": self.FILTER_LABELS_NOT_LABELS, + request_data = json.dumps( + { + "search_categories": { + "room_events": { + "search_term": "label", + "filter": self.FILTER_LABELS_NOT_LABELS, + } } } - }) + ) self._send_labelled_messages_in_room() @@ -1295,9 +1308,7 @@ class LabelsTestCase(unittest.HomeserverTestCase): results = channel.json_body["search_categories"]["room_events"]["results"] self.assertEqual( - len(results), - 1, - [result["result"]["content"] for result in results], + len(results), 1, [result["result"]["content"] for result in results], ) self.assertEqual( results[0]["result"]["content"]["body"], From f141af4c79b2be8e87d683420e2d8117e2a8525c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 14:52:38 +0000 Subject: [PATCH 08/14] Update copyright --- synapse/handlers/room.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 899bb63114..f6e162484c 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2018-2019 New Vector Ltd +# Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From cb2cbe4d26b5d0c082c82a62260c0c05afde8aeb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 5 Nov 2019 15:27:38 +0000 Subject: [PATCH 09/14] Only filter if a filter was provided --- synapse/handlers/room.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index f6e162484c..f47237b3fb 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -875,10 +875,12 @@ class RoomContextHandler(object): room_id, event_id, before_limit, after_limit, event_filter ) - filtered_before_events = event_filter.filter(results["events_before"]) - results["events_before"] = yield filter_evts(filtered_before_events) - filtered_after_events = event_filter.filter(results["events_after"]) - results["events_after"] = yield filter_evts(filtered_after_events) + if event_filter: + results["events_before"] = event_filter.filter(results["events_before"]) + results["events_after"] = event_filter.filter(results["events_after"]) + + results["events_before"] = yield filter_evts(results["events_before"]) + results["events_after"] = yield filter_evts(results["events_after"]) results["event"] = event if results["events_after"]: From eda14737cf0faf789ec587633b12bb2cf65fa305 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 6 Nov 2019 18:14:03 +0000 Subject: [PATCH 10/14] Also filter state events --- synapse/handlers/room.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index f47237b3fb..3148df0de9 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -907,7 +907,13 @@ class RoomContextHandler(object): state = yield self.state_store.get_state_for_events( [last_event_id], state_filter=state_filter ) - results["state"] = list(state[last_event_id].values()) + + # Apply the filter on state events. + state_events = list(state[last_event_id].values()) + if event_filter: + state_events = event_filter.filter(state_events) + + results["state"] = list(state_events) # We use a dummy token here as we only care about the room portion of # the token, which we replace. From 49243c55a4c0a9dd82d3ba95f111bc2df430b587 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Nov 2019 16:09:11 +0000 Subject: [PATCH 11/14] Update changelog since this isn't going to be featured in 1.6.0 --- changelog.d/6329.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6329.feature b/changelog.d/6329.feature index 78a187a1dc..48263cdd86 100644 --- a/changelog.d/6329.feature +++ b/changelog.d/6329.feature @@ -1 +1 @@ -Implement label-based filtering on `/sync` and `/messages` ([MSC2326](https://github.com/matrix-org/matrix-doc/pull/2326)). +Filter state, events_before and events_after in /context requests. From b2f8c21a9b9389251c9343166c63b003fad278a2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Nov 2019 16:10:27 +0000 Subject: [PATCH 12/14] Format changelog --- changelog.d/6329.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/6329.feature b/changelog.d/6329.feature index 48263cdd86..c27dbb06a4 100644 --- a/changelog.d/6329.feature +++ b/changelog.d/6329.feature @@ -1 +1 @@ -Filter state, events_before and events_after in /context requests. +Filter `state`, `events_before` and `events_after` in `/context` requests. From 08a436ecb25de2c4c8f2daf423bfcaf72e985143 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Dec 2019 14:18:46 +0000 Subject: [PATCH 13/14] Incorporate review --- changelog.d/6329.bugfix | 1 + changelog.d/6329.feature | 1 - synapse/handlers/room.py | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 changelog.d/6329.bugfix delete mode 100644 changelog.d/6329.feature diff --git a/changelog.d/6329.bugfix b/changelog.d/6329.bugfix new file mode 100644 index 0000000000..e558d13b7d --- /dev/null +++ b/changelog.d/6329.bugfix @@ -0,0 +1 @@ +Correctly apply the event filter to the `state`, `events_before` and `events_after` fields in the response to `/context` requests. \ No newline at end of file diff --git a/changelog.d/6329.feature b/changelog.d/6329.feature deleted file mode 100644 index c27dbb06a4..0000000000 --- a/changelog.d/6329.feature +++ /dev/null @@ -1 +0,0 @@ -Filter `state`, `events_before` and `events_after` in `/context` requests. diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 3148df0de9..fd3ea8daf8 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -908,12 +908,11 @@ class RoomContextHandler(object): [last_event_id], state_filter=state_filter ) - # Apply the filter on state events. state_events = list(state[last_event_id].values()) if event_filter: state_events = event_filter.filter(state_events) - results["state"] = list(state_events) + results["state"] = state_events # We use a dummy token here as we only care about the room portion of # the token, which we replace. From 65c6aee621fecff1c6a863d6b910c973196ad6bc Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Dec 2019 14:36:39 +0000 Subject: [PATCH 14/14] Un-remove room purge test --- tests/rest/client/v1/test_rooms.py | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 4095e63aef..1ca7fa742f 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -815,6 +815,78 @@ class RoomMessageListTestCase(RoomBase): self.assertTrue("chunk" in channel.json_body) self.assertTrue("end" in channel.json_body) + def test_room_messages_purge(self): + store = self.hs.get_datastore() + pagination_handler = self.hs.get_pagination_handler() + + # Send a first message in the room, which will be removed by the purge. + first_event_id = self.helper.send(self.room_id, "message 1")["event_id"] + first_token = self.get_success( + store.get_topological_token_for_event(first_event_id) + ) + + # Send a second message in the room, which won't be removed, and which we'll + # use as the marker to purge events before. + second_event_id = self.helper.send(self.room_id, "message 2")["event_id"] + second_token = self.get_success( + store.get_topological_token_for_event(second_event_id) + ) + + # Send a third event in the room to ensure we don't fall under any edge case + # due to our marker being the latest forward extremity in the room. + self.helper.send(self.room_id, "message 3") + + # Check that we get the first and second message when querying /messages. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, second_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 2, [event["content"] for event in chunk]) + + # Purge every event before the second event. + purge_id = random_string(16) + pagination_handler._purges_by_id[purge_id] = PurgeStatus() + self.get_success( + pagination_handler._purge_history( + purge_id=purge_id, + room_id=self.room_id, + token=second_token, + delete_local_events=True, + ) + ) + + # Check that we only get the second message through /message now that the first + # has been purged. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, second_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 1, [event["content"] for event in chunk]) + + # Check that we get no event, but also no error, when querying /messages with + # the token that was pointing at the first event, because we don't have it + # anymore. + request, channel = self.make_request( + "GET", + "/rooms/%s/messages?access_token=x&from=%s&dir=b&filter=%s" + % (self.room_id, first_token, json.dumps({"types": [EventTypes.Message]})), + ) + self.render(request) + self.assertEqual(channel.code, 200, channel.json_body) + + chunk = channel.json_body["chunk"] + self.assertEqual(len(chunk), 0, [event["content"] for event in chunk]) + class RoomSearchTestCase(unittest.HomeserverTestCase): servlets = [