mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 11:17:51 +00:00
Changes to a member's `member_info` entry that no event in the room anchors (erasure, and MSC4430 link verification) were only delivered when the member next appeared in a response. Give them a stream so that syncing clients are woken and told. * `StreamToken.member_info_key`: a trailing `MultiWriterStreamToken` component (`StreamKeyType.MEMBER_INFO`); tokens without it still parse. * `msc4428_member_info_changes` (delta 95/01) behind a multi-writer id generator with no fixed writer list, as for cache invalidations: any worker may write (erasure runs wherever deactivation is handled). `MemberInfoChangesStream` replicates rows; workers wake room members. * Retention: rows older than 30 days are pruned hourly on the background worker and the prune point recorded; a since token older than that is sent the full map per room instead of deltas. * Producer: `MemberInfoHandler.on_user_erased`, from `deactivate_account`, one row per room the user has any membership in. * Consumers: sync v2 reads the changes for all joined rooms once per sync and includes a room with an empty timeline when it has any; sliding sync includes such rooms when choosing what to send and resends the full map for rooms it last sent at an older position. The stream exists always, like every stream, but nothing writes to it unless `msc4428_enabled` is set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
#
|
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
#
|
|
# Copyright (C) 2026 Element Creations Ltd
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# See the GNU Affero General Public License for more details:
|
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
|
#
|
|
|
|
from twisted.internet.testing import MemoryReactor
|
|
|
|
from synapse.api.constants import MemberInfo
|
|
from synapse.rest import admin
|
|
from synapse.rest.client import login, room, sync
|
|
from synapse.server import HomeServer
|
|
from synapse.types import JsonDict
|
|
from synapse.util.clock import Clock
|
|
|
|
from tests.replication._base import BaseMultiWorkerStreamTestCase
|
|
from tests.server import make_request
|
|
|
|
|
|
class MemberInfoWorkerTestCase(BaseMultiWorkerStreamTestCase):
|
|
"""MSC4428: a `/sync` served by a worker sees an erasure recorded on the
|
|
main process, with nothing in the room to anchor it."""
|
|
|
|
servlets = [
|
|
admin.register_servlets,
|
|
login.register_servlets,
|
|
room.register_servlets,
|
|
sync.register_servlets,
|
|
]
|
|
|
|
def default_config(self) -> JsonDict:
|
|
config = super().default_config()
|
|
config["experimental_features"] = {"msc4428_enabled": True}
|
|
return config
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.store = hs.get_datastores().main
|
|
self.alice = self.register_user("alice", "pass")
|
|
self.alice_tok = self.login("alice", "pass")
|
|
self.bob = self.register_user("bob", "pass")
|
|
self.bob_tok = self.login("bob", "pass")
|
|
self.room_id = self.helper.create_room_as(self.alice, tok=self.alice_tok)
|
|
self.helper.join(self.room_id, self.bob, tok=self.bob_tok)
|
|
|
|
def test_erasure_reaches_sync_on_worker(self) -> None:
|
|
worker_hs = self.make_worker_hs("synapse.app.generic_worker")
|
|
site = self._hs_to_site[worker_hs]
|
|
|
|
channel = make_request(
|
|
self.reactor, site, "GET", "/sync", access_token=self.alice_tok
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.json_body)
|
|
self.assertNotIn(
|
|
MemberInfo.FIELD, channel.json_body["rooms"]["join"][self.room_id]
|
|
)
|
|
since = channel.json_body["next_batch"]
|
|
|
|
# Erase Bob on the main process, without an event in the room.
|
|
self.get_success(self.store.mark_user_erased(self.bob))
|
|
self.get_success(self.hs.get_member_info_handler().on_user_erased(self.bob))
|
|
self.replicate()
|
|
|
|
channel = make_request(
|
|
self.reactor,
|
|
site,
|
|
"GET",
|
|
f"/sync?since={since}",
|
|
access_token=self.alice_tok,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.json_body)
|
|
room = channel.json_body["rooms"]["join"][self.room_id]
|
|
self.assertEqual(room["timeline"]["events"], [])
|
|
self.assertEqual(room[MemberInfo.FIELD], {self.bob: {MemberInfo.USER_ID: None}})
|