mirror of
https://github.com/element-hq/synapse.git
synced 2026-09-01 22:28:48 +00:00
feat: Support ignoring reply trees via relations
This commit is contained in:
+50
-3
@@ -42,12 +42,13 @@ from synapse.events.utils import clone_event, prune_event
|
||||
from synapse.logging.opentracing import trace
|
||||
from synapse.storage.controllers import StorageControllers
|
||||
from synapse.storage.databases.main import DataStore
|
||||
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
|
||||
from synapse.synapse_rust.events import event_visible_to_server
|
||||
from synapse.types import (
|
||||
RetentionPolicy,
|
||||
EventID, RetentionPolicy,
|
||||
StateMap,
|
||||
StrCollection,
|
||||
get_domain_from_id,
|
||||
UserID, get_domain_from_id,
|
||||
)
|
||||
from synapse.types.state import StateFilter
|
||||
from synapse.util.clock import Clock
|
||||
@@ -175,15 +176,21 @@ async def filter_and_transform_events_for_client(
|
||||
retention_policies[
|
||||
room_id
|
||||
] = await storage.main.get_retention_policy_for_room(room_id)
|
||||
sender_trees: dict[str, set[str]] = {}
|
||||
|
||||
def allowed(event: EventBase) -> EventBase | None:
|
||||
state_after_event = event_id_to_state.get(event.event_id)
|
||||
sender_ignored = event.sender in ignore_list
|
||||
if not sender_ignored and (tree := sender_trees.get(event.event_id)):
|
||||
logger.debug("%s is not in %r, checking if any senders in %r are", event.sender, ignore_list, tree)
|
||||
sender_ignored = any(sender in ignore_list for sender in tree)
|
||||
logger.debug("any sender in ignore list: %r", sender_ignored)
|
||||
filtered = _check_client_allowed_to_see_event(
|
||||
user_id=user_id,
|
||||
event=event,
|
||||
clock=storage.main.clock,
|
||||
filter_send_to_client=filter_send_to_client,
|
||||
sender_ignored=event.sender in ignore_list,
|
||||
sender_ignored=sender_ignored,
|
||||
always_include_ids=always_include_ids,
|
||||
retention_policy=retention_policies[event.room_id],
|
||||
state=state_after_event,
|
||||
@@ -255,6 +262,46 @@ async def filter_and_transform_events_for_client(
|
||||
return cloned
|
||||
|
||||
# Check each event: gives an iterable of None or (a modified) EventBase.
|
||||
event_map = {ev.event_id: ev for ev in events}
|
||||
async def walk_relations(root: EventBase, senders: set[str]) -> set[str]:
|
||||
senders.add(root.sender)
|
||||
logger.debug("Walking relations of %s (senders: %r)", root.event_id, senders)
|
||||
relations = root.unsigned.get("m.relates_to")
|
||||
if not isinstance(relations, dict):
|
||||
logger.debug("No more relations to walk (no relates_to)")
|
||||
return senders
|
||||
reply_to = relations.get("m.in_reply_to")
|
||||
if not isinstance(reply_to, dict):
|
||||
logger.debug("No more relations to walk (no in_reply_to)")
|
||||
return senders
|
||||
reply_id = reply_to.get("event_id")
|
||||
if not isinstance(reply_id, str):
|
||||
logger.debug("No more relations to walk (no event_id)")
|
||||
return senders
|
||||
reply = event_map.get(
|
||||
reply_id,
|
||||
await storage.main.get_event(
|
||||
reply_id,
|
||||
redact_behaviour=EventRedactBehaviour.as_is,
|
||||
allow_rejected=True,
|
||||
allow_none=True
|
||||
)
|
||||
)
|
||||
if reply is None:
|
||||
logger.debug("No more relations to walk (reply to unknown event)")
|
||||
return senders
|
||||
# If this reply is already in sender_trees, we've already walked it, and can
|
||||
# return now.
|
||||
if reply in sender_trees:
|
||||
logger.debug("Already seen reply, no more relations to walk.")
|
||||
return senders
|
||||
return await walk_relations(reply, senders)
|
||||
|
||||
for ev in events:
|
||||
logger.debug("Preparing to walk relations of proposed event %s", ev.event_id)
|
||||
sender_trees.setdefault(ev.event_id, set())
|
||||
await walk_relations(ev, sender_trees[ev.event_id])
|
||||
|
||||
filtered_events = map(allowed, events)
|
||||
|
||||
# Turn it into a list and remove None entries before returning.
|
||||
|
||||
@@ -32,7 +32,10 @@ from synapse.api.constants import (
|
||||
ReceiptTypes,
|
||||
RelationTypes,
|
||||
)
|
||||
from synapse.rest.client import devices, knock, login, read_marker, receipts, room, sync
|
||||
from synapse.rest.client import (
|
||||
account_data, devices, knock, login, read_marker,
|
||||
receipts, room, sync,
|
||||
)
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util.clock import Clock
|
||||
@@ -1208,3 +1211,105 @@ class SyncCancellationTestCase(unittest.HomeserverTestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(200, channel.code, msg=channel.result["body"])
|
||||
|
||||
class StarnapseSyncIgnoreSenderTreeTestCase(unittest.HomeserverTestCase):
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets,
|
||||
login.register_servlets,
|
||||
sync.register_servlets,
|
||||
room.register_servlets,
|
||||
account_data.register_servlets,
|
||||
]
|
||||
|
||||
def test_initial_sync(self) -> None:
|
||||
"""Tests that a reply to an ignored user is omitted from sync"""
|
||||
alice = self.register_user("alice", "password")
|
||||
bob = self.register_user("bob", "password")
|
||||
charlie = self.register_user("charlie", "password")
|
||||
alice_token = self.login("alice", "password")
|
||||
bob_token = self.login("bob", "password")
|
||||
charlie_token = self.login("charlie", "password")
|
||||
|
||||
# Create a room and join everyone to it
|
||||
room_id = self.helper.create_room_as(alice, tok=alice_token)
|
||||
self.helper.join(room_id, bob, tok=bob_token)
|
||||
self.helper.join(room_id, charlie, tok=charlie_token)
|
||||
|
||||
# Alice ignores bob
|
||||
self.helper.set_account_data(
|
||||
alice,
|
||||
"m.ignored_user_list",
|
||||
{
|
||||
bob: {}
|
||||
},
|
||||
tok=alice_token
|
||||
)
|
||||
|
||||
# Alice sends a message
|
||||
alice_msg = self.helper.send(room_id, alice, tok=alice_token)["event_id"]
|
||||
# And Bob replies to it
|
||||
bob_msg = self.helper.send_event(
|
||||
room_id,
|
||||
bob,
|
||||
{
|
||||
"msgtype": "m.text",
|
||||
"body": f"Hello {alice} from Bob!",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": alice_msg,
|
||||
}
|
||||
}
|
||||
},
|
||||
tok=bob_token
|
||||
)["event_id"]
|
||||
# And then Charlie replies to Bob
|
||||
charlie_msg = self.helper.send_event(
|
||||
room_id,
|
||||
bob,
|
||||
{
|
||||
"msgtype": "m.text",
|
||||
"body": f"Hello {bob} from Charlie!",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": bob_msg,
|
||||
}
|
||||
}
|
||||
},
|
||||
tok=charlie_token
|
||||
)["event_id"]
|
||||
# Because Alice blocked Bob, she probably doesn't want to see Charlie's reply
|
||||
# to his message either. Neither Bob nor Charlie's message should show up in
|
||||
# the next sync.
|
||||
|
||||
# However, we want to make sure a reply from Charlie to Alice doesn't
|
||||
# get dropped.
|
||||
charlie_msg2 = self.helper.send_event(
|
||||
room_id,
|
||||
bob,
|
||||
{
|
||||
"msgtype": "m.text",
|
||||
"body": f"Hello {alice} from Charlie!",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": alice_msg,
|
||||
}
|
||||
}
|
||||
},
|
||||
tok=charlie_token
|
||||
)["event_id"]
|
||||
charlie_msg3 = self.helper.send(room_id, charlie, tok=charlie_token)["event_id"]
|
||||
|
||||
|
||||
# Request an initial sync
|
||||
channel = self.make_request("GET", "/sync", access_token=alice_token)
|
||||
self.assertEqual(channel.code, 200, channel.json_body)
|
||||
|
||||
# Check for those one time key counts
|
||||
timeline = channel.json_body["rooms"]["join"][room_id]["timeline"]["events"]
|
||||
timeline_map = {e["event_id"]: e for e in timeline if e.get("state_key") is None}
|
||||
print("\nTimeline map: %s\nAlice: %s\nBob: %s\nCharlie: %s %s %s" % (json.dumps(timeline_map, indent=4), alice_msg, bob_msg, charlie_msg, charlie_msg2, charlie_msg3))
|
||||
assert bob_msg not in timeline_map, "Bob bypassed ignore list"
|
||||
assert charlie_msg not in timeline_map, "Charlie's reply to bob appeared"
|
||||
assert alice_msg in timeline_map, "Alice's message should be in the timeline"
|
||||
assert charlie_msg2 in timeline_map, "Charlie's reply to alice message should be in the timeline"
|
||||
assert charlie_msg3 in timeline_map, "Charlie's unrelated message should be in the timeline"
|
||||
|
||||
@@ -1022,3 +1022,26 @@ class RestHelper:
|
||||
access_token=tok,
|
||||
)
|
||||
assert channel.code == HTTPStatus.OK, channel.text_body
|
||||
|
||||
def set_account_data(
|
||||
self,
|
||||
user_id: str,
|
||||
type: str,
|
||||
content: dict,
|
||||
room_id: str | None = None,
|
||||
*,
|
||||
tok: str
|
||||
) -> None:
|
||||
if room_id is None:
|
||||
path = f"/user/{user_id}/account_data/{type}"
|
||||
else:
|
||||
path = f"/user/{user_id}/rooms/{room_id}/account_data/{type}"
|
||||
channel = make_request(
|
||||
self.reactor,
|
||||
self.site,
|
||||
method="PUT",
|
||||
path=path,
|
||||
content=content,
|
||||
access_token=tok,
|
||||
)
|
||||
assert channel.code == HTTPStatus.OK, channel.text_body
|
||||
|
||||
Reference in New Issue
Block a user