Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 04e79e6

Browse files
authored
Add config option to forget rooms automatically when users leave them (#15224)
This is largely based off the stats and user directory updater code. Signed-off-by: Sean Quah <seanq@matrix.org>
1 parent 0e8aa2a commit 04e79e6

File tree

9 files changed

+259
-47
lines changed

9 files changed

+259
-47
lines changed

changelog.d/15224.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `forget_rooms_on_leave` config option to automatically forget rooms when users leave them or are removed from them.

docs/usage/configuration/config_documentation.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3699,6 +3699,16 @@ default_power_level_content_override:
36993699
trusted_private_chat: null
37003700
public_chat: null
37013701
```
3702+
---
3703+
### `forget_rooms_on_leave`
3704+
3705+
Set to true to automatically forget rooms for users when they leave them, either
3706+
normally or via a kick or ban. Defaults to false.
3707+
3708+
Example configuration:
3709+
```yaml
3710+
forget_rooms_on_leave: false
3711+
```
37023712

37033713
---
37043714
## Opentracing

synapse/config/room.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,7 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
7575
% preset
7676
)
7777
# We validate the actual overrides when we try to apply them.
78+
79+
# When enabled, users will forget rooms when they leave them, either via a
80+
# leave, kick or ban.
81+
self.forget_on_leave = config.get("forget_rooms_on_leave", False)

synapse/handlers/room_member.py

Lines changed: 154 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717
import random
1818
from http import HTTPStatus
19-
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple
19+
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple
2020

2121
from synapse import types
2222
from synapse.api.constants import (
@@ -38,7 +38,10 @@
3838
from synapse.events import EventBase
3939
from synapse.events.snapshot import EventContext
4040
from synapse.handlers.profile import MAX_AVATAR_URL_LEN, MAX_DISPLAYNAME_LEN
41+
from synapse.handlers.state_deltas import MatchChange, StateDeltasHandler
4142
from synapse.logging import opentracing
43+
from synapse.metrics import event_processing_positions
44+
from synapse.metrics.background_process_metrics import run_as_background_process
4245
from synapse.module_api import NOT_SPAM
4346
from synapse.types import (
4447
JsonDict,
@@ -280,9 +283,25 @@ async def _user_left_room(self, target: UserID, room_id: str) -> None:
280283
"""
281284
raise NotImplementedError()
282285

283-
@abc.abstractmethod
284286
async def forget(self, user: UserID, room_id: str) -> None:
285-
raise NotImplementedError()
287+
user_id = user.to_string()
288+
289+
member = await self._storage_controllers.state.get_current_state_event(
290+
room_id=room_id, event_type=EventTypes.Member, state_key=user_id
291+
)
292+
membership = member.membership if member else None
293+
294+
if membership is not None and membership not in [
295+
Membership.LEAVE,
296+
Membership.BAN,
297+
]:
298+
raise SynapseError(400, "User %s in room %s" % (user_id, room_id))
299+
300+
# In normal case this call is only required if `membership` is not `None`.
301+
# But: After the last member had left the room, the background update
302+
# `_background_remove_left_rooms` is deleting rows related to this room from
303+
# the table `current_state_events` and `get_current_state_events` is `None`.
304+
await self.store.forget(user_id, room_id)
286305

287306
async def ratelimit_multiple_invites(
288307
self,
@@ -2046,25 +2065,141 @@ async def _user_left_room(self, target: UserID, room_id: str) -> None:
20462065
"""Implements RoomMemberHandler._user_left_room"""
20472066
user_left_room(self.distributor, target, room_id)
20482067

2049-
async def forget(self, user: UserID, room_id: str) -> None:
2050-
user_id = user.to_string()
20512068

2052-
member = await self._storage_controllers.state.get_current_state_event(
2053-
room_id=room_id, event_type=EventTypes.Member, state_key=user_id
2054-
)
2055-
membership = member.membership if member else None
2069+
class RoomForgetterHandler(StateDeltasHandler):
2070+
"""Forgets rooms when they are left, when enabled in the homeserver config.
20562071
2057-
if membership is not None and membership not in [
2058-
Membership.LEAVE,
2059-
Membership.BAN,
2060-
]:
2061-
raise SynapseError(400, "User %s in room %s" % (user_id, room_id))
2072+
For the purposes of this feature, kicks, bans and "leaves" via state resolution
2073+
weirdness are all considered to be leaves.
20622074
2063-
# In normal case this call is only required if `membership` is not `None`.
2064-
# But: After the last member had left the room, the background update
2065-
# `_background_remove_left_rooms` is deleting rows related to this room from
2066-
# the table `current_state_events` and `get_current_state_events` is `None`.
2067-
await self.store.forget(user_id, room_id)
2075+
Derived from `StatsHandler` and `UserDirectoryHandler`.
2076+
"""
2077+
2078+
def __init__(self, hs: "HomeServer"):
2079+
super().__init__(hs)
2080+
2081+
self._hs = hs
2082+
self._store = hs.get_datastores().main
2083+
self._storage_controllers = hs.get_storage_controllers()
2084+
self._clock = hs.get_clock()
2085+
self._notifier = hs.get_notifier()
2086+
self._room_member_handler = hs.get_room_member_handler()
2087+
2088+
# The current position in the current_state_delta stream
2089+
self.pos: Optional[int] = None
2090+
2091+
# Guard to ensure we only process deltas one at a time
2092+
self._is_processing = False
2093+
2094+
if hs.config.worker.run_background_tasks:
2095+
self._notifier.add_replication_callback(self.notify_new_event)
2096+
2097+
# We kick this off to pick up outstanding work from before the last restart.
2098+
self._clock.call_later(0, self.notify_new_event)
2099+
2100+
def notify_new_event(self) -> None:
2101+
"""Called when there may be more deltas to process"""
2102+
if self._is_processing:
2103+
return
2104+
2105+
self._is_processing = True
2106+
2107+
async def process() -> None:
2108+
try:
2109+
await self._unsafe_process()
2110+
finally:
2111+
self._is_processing = False
2112+
2113+
run_as_background_process("room_forgetter.notify_new_event", process)
2114+
2115+
async def _unsafe_process(self) -> None:
2116+
# If self.pos is None then means we haven't fetched it from DB
2117+
if self.pos is None:
2118+
self.pos = await self._store.get_room_forgetter_stream_pos()
2119+
room_max_stream_ordering = self._store.get_room_max_stream_ordering()
2120+
if self.pos > room_max_stream_ordering:
2121+
# apparently, we've processed more events than exist in the database!
2122+
# this can happen if events are removed with history purge or similar.
2123+
logger.warning(
2124+
"Event stream ordering appears to have gone backwards (%i -> %i): "
2125+
"rewinding room forgetter processor",
2126+
self.pos,
2127+
room_max_stream_ordering,
2128+
)
2129+
self.pos = room_max_stream_ordering
2130+
2131+
if not self._hs.config.room.forget_on_leave:
2132+
# Update the processing position, so that if the server admin turns the
2133+
# feature on at a later date, we don't decide to forget every room that
2134+
# has ever been left in the past.
2135+
self.pos = self._store.get_room_max_stream_ordering()
2136+
await self._store.update_room_forgetter_stream_pos(self.pos)
2137+
return
2138+
2139+
# Loop round handling deltas until we're up to date
2140+
2141+
while True:
2142+
# Be sure to read the max stream_ordering *before* checking if there are any outstanding
2143+
# deltas, since there is otherwise a chance that we could miss updates which arrive
2144+
# after we check the deltas.
2145+
room_max_stream_ordering = self._store.get_room_max_stream_ordering()
2146+
if self.pos == room_max_stream_ordering:
2147+
break
2148+
2149+
logger.debug(
2150+
"Processing room forgetting %s->%s", self.pos, room_max_stream_ordering
2151+
)
2152+
(
2153+
max_pos,
2154+
deltas,
2155+
) = await self._storage_controllers.state.get_current_state_deltas(
2156+
self.pos, room_max_stream_ordering
2157+
)
2158+
2159+
logger.debug("Handling %d state deltas", len(deltas))
2160+
await self._handle_deltas(deltas)
2161+
2162+
self.pos = max_pos
2163+
2164+
# Expose current event processing position to prometheus
2165+
event_processing_positions.labels("room_forgetter").set(max_pos)
2166+
2167+
await self._store.update_room_forgetter_stream_pos(max_pos)
2168+
2169+
async def _handle_deltas(self, deltas: List[Dict[str, Any]]) -> None:
2170+
"""Called with the state deltas to process"""
2171+
for delta in deltas:
2172+
typ = delta["type"]
2173+
state_key = delta["state_key"]
2174+
room_id = delta["room_id"]
2175+
event_id = delta["event_id"]
2176+
prev_event_id = delta["prev_event_id"]
2177+
2178+
if typ != EventTypes.Member:
2179+
continue
2180+
2181+
if not self._hs.is_mine_id(state_key):
2182+
continue
2183+
2184+
change = await self._get_key_change(
2185+
prev_event_id,
2186+
event_id,
2187+
key_name="membership",
2188+
public_value=Membership.JOIN,
2189+
)
2190+
is_leave = change is MatchChange.now_false
2191+
2192+
if is_leave:
2193+
try:
2194+
await self._room_member_handler.forget(
2195+
UserID.from_string(state_key), room_id
2196+
)
2197+
except SynapseError as e:
2198+
if e.code == 400:
2199+
# The user is back in the room.
2200+
pass
2201+
else:
2202+
raise
20682203

20692204

20702205
def get_users_which_can_issue_invite(auth_events: StateMap[EventBase]) -> List[str]:

synapse/handlers/room_member_worker.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,3 @@ async def _user_left_room(self, target: UserID, room_id: str) -> None:
137137
await self._notify_change_client(
138138
user_id=target.to_string(), room_id=room_id, change="left"
139139
)
140-
141-
async def forget(self, target: UserID, room_id: str) -> None:
142-
raise RuntimeError("Cannot forget rooms on workers.")

synapse/server.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,11 @@
9494
)
9595
from synapse.handlers.room_batch import RoomBatchHandler
9696
from synapse.handlers.room_list import RoomListHandler
97-
from synapse.handlers.room_member import RoomMemberHandler, RoomMemberMasterHandler
97+
from synapse.handlers.room_member import (
98+
RoomForgetterHandler,
99+
RoomMemberHandler,
100+
RoomMemberMasterHandler,
101+
)
98102
from synapse.handlers.room_member_worker import RoomMemberWorkerHandler
99103
from synapse.handlers.room_summary import RoomSummaryHandler
100104
from synapse.handlers.search import SearchHandler
@@ -233,6 +237,7 @@ class HomeServer(metaclass=abc.ABCMeta):
233237
"message",
234238
"pagination",
235239
"profile",
240+
"room_forgetter",
236241
"stats",
237242
]
238243

@@ -847,6 +852,10 @@ def get_account_handler(self) -> AccountHandler:
847852
def get_push_rules_handler(self) -> PushRulesHandler:
848853
return PushRulesHandler(self)
849854

855+
@cache_in_self
856+
def get_room_forgetter_handler(self) -> RoomForgetterHandler:
857+
return RoomForgetterHandler(self)
858+
850859
@cache_in_self
851860
def get_outbound_redis_connection(self) -> "ConnectionHandler":
852861
"""

synapse/storage/databases/main/roommember.py

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class EventIdMembership:
8282
membership: str
8383

8484

85-
class RoomMemberWorkerStore(EventsWorkerStore):
85+
class RoomMemberWorkerStore(EventsWorkerStore, CacheInvalidationWorkerStore):
8686
def __init__(
8787
self,
8888
database: DatabasePool,
@@ -1372,6 +1372,50 @@ def _is_local_host_in_room_ignoring_users_txn(
13721372
_is_local_host_in_room_ignoring_users_txn,
13731373
)
13741374

1375+
async def forget(self, user_id: str, room_id: str) -> None:
1376+
"""Indicate that user_id wishes to discard history for room_id."""
1377+
1378+
def f(txn: LoggingTransaction) -> None:
1379+
self.db_pool.simple_update_txn(
1380+
txn,
1381+
table="room_memberships",
1382+
keyvalues={"user_id": user_id, "room_id": room_id},
1383+
updatevalues={"forgotten": 1},
1384+
)
1385+
1386+
self._invalidate_cache_and_stream(txn, self.did_forget, (user_id, room_id))
1387+
self._invalidate_cache_and_stream(
1388+
txn, self.get_forgotten_rooms_for_user, (user_id,)
1389+
)
1390+
1391+
await self.db_pool.runInteraction("forget_membership", f)
1392+
1393+
async def get_room_forgetter_stream_pos(self) -> int:
1394+
"""Get the stream position of the background process to forget rooms when left
1395+
by users.
1396+
"""
1397+
return await self.db_pool.simple_select_one_onecol(
1398+
table="room_forgetter_stream_pos",
1399+
keyvalues={},
1400+
retcol="stream_id",
1401+
desc="room_forgetter_stream_pos",
1402+
)
1403+
1404+
async def update_room_forgetter_stream_pos(self, stream_id: int) -> None:
1405+
"""Update the stream position of the background process to forget rooms when
1406+
left by users.
1407+
1408+
Must only be used by the worker running the background process.
1409+
"""
1410+
assert self.hs.config.worker.run_background_tasks
1411+
1412+
await self.db_pool.simple_update_one(
1413+
table="room_forgetter_stream_pos",
1414+
keyvalues={},
1415+
updatevalues={"stream_id": stream_id},
1416+
desc="room_forgetter_stream_pos",
1417+
)
1418+
13751419

13761420
class RoomMemberBackgroundUpdateStore(SQLBaseStore):
13771421
def __init__(
@@ -1553,29 +1597,6 @@ def __init__(
15531597
):
15541598
super().__init__(database, db_conn, hs)
15551599

1556-
async def forget(self, user_id: str, room_id: str) -> None:
1557-
"""Indicate that user_id wishes to discard history for room_id."""
1558-
1559-
def f(txn: LoggingTransaction) -> None:
1560-
sql = (
1561-
"UPDATE"
1562-
" room_memberships"
1563-
" SET"
1564-
" forgotten = 1"
1565-
" WHERE"
1566-
" user_id = ?"
1567-
" AND"
1568-
" room_id = ?"
1569-
)
1570-
txn.execute(sql, (user_id, room_id))
1571-
1572-
self._invalidate_cache_and_stream(txn, self.did_forget, (user_id, room_id))
1573-
self._invalidate_cache_and_stream(
1574-
txn, self.get_forgotten_rooms_for_user, (user_id,)
1575-
)
1576-
1577-
await self.db_pool.runInteraction("forget_membership", f)
1578-
15791600

15801601
def extract_heroes_from_room_summary(
15811602
details: Mapping[str, MemberSummary], me: str
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* Copyright 2023 The Matrix.org Foundation C.I.C
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
CREATE TABLE room_forgetter_stream_pos (
17+
Lock CHAR(1) NOT NULL DEFAULT 'X' UNIQUE, -- Makes sure this table only has one row.
18+
stream_id BIGINT NOT NULL,
19+
CHECK (Lock='X')
20+
);
21+
22+
INSERT INTO room_forgetter_stream_pos (
23+
stream_id
24+
) SELECT COALESCE(MAX(stream_ordering), 0) from events;

0 commit comments

Comments
 (0)