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

Commit d73bf18

Browse files
authored
Server notices: Dissociate room creation/lookup from invite (#7199)
Fixes #6815 Before figuring out whether we should alert a user on MAU, we call get_notice_room_for_user to get some info on the existing server notices room for this user. This function, if the room doesn't exist, creates it and invites the user in it. This means that, if we decide later that no server notice is needed, the user gets invited in a room with no message in it. This happens at every restart of the server, since the room ID returned by get_notice_room_for_user is cached. This PR fixes that by moving the inviting bit to a dedicated function, that's only called when the server actually needs to send a notice to the user. A potential issue with this approach is that the room that's created by get_notice_room_for_user doesn't match how that same function looks for an existing room (i.e. it creates a room that doesn't have an invite or a join for the current user in it, so it could lead to a new room being created each time a user syncs), but I'm not sure this is a problem given it's cached until the server restarts, so that function won't run very often. It also renames get_notice_room_for_user into get_or_create_notice_room_for_user to make what it does clearer.
1 parent 694d8be commit d73bf18

File tree

4 files changed

+154
-22
lines changed

4 files changed

+154
-22
lines changed

changelog.d/7199.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug that could cause a user to be invited to a server notices (aka System Alerts) room without any notice being sent.

synapse/server_notices/resource_limits_server_notices.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ def maybe_send_server_notice_to_user(self, user_id):
8080
# In practice, not sure we can ever get here
8181
return
8282

83-
room_id = yield self._server_notices_manager.get_notice_room_for_user(user_id)
83+
room_id = yield self._server_notices_manager.get_or_create_notice_room_for_user(
84+
user_id
85+
)
8486

8587
if not room_id:
8688
logger.warning("Failed to get server notices room")

synapse/server_notices/server_notices_manager.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from twisted.internet import defer
1818

1919
from synapse.api.constants import EventTypes, Membership, RoomCreationPreset
20-
from synapse.types import create_requester
20+
from synapse.types import UserID, create_requester
2121
from synapse.util.caches.descriptors import cachedInlineCallbacks
2222

2323
logger = logging.getLogger(__name__)
@@ -36,10 +36,12 @@ def __init__(self, hs):
3636
self._store = hs.get_datastore()
3737
self._config = hs.config
3838
self._room_creation_handler = hs.get_room_creation_handler()
39+
self._room_member_handler = hs.get_room_member_handler()
3940
self._event_creation_handler = hs.get_event_creation_handler()
4041
self._is_mine_id = hs.is_mine_id
4142

4243
self._notifier = hs.get_notifier()
44+
self.server_notices_mxid = self._config.server_notices_mxid
4345

4446
def is_enabled(self):
4547
"""Checks if server notices are enabled on this server.
@@ -66,7 +68,8 @@ def send_notice(
6668
Returns:
6769
Deferred[FrozenEvent]
6870
"""
69-
room_id = yield self.get_notice_room_for_user(user_id)
71+
room_id = yield self.get_or_create_notice_room_for_user(user_id)
72+
yield self.maybe_invite_user_to_room(user_id, room_id)
7073

7174
system_mxid = self._config.server_notices_mxid
7275
requester = create_requester(system_mxid)
@@ -89,10 +92,11 @@ def send_notice(
8992
return res
9093

9194
@cachedInlineCallbacks()
92-
def get_notice_room_for_user(self, user_id):
95+
def get_or_create_notice_room_for_user(self, user_id):
9396
"""Get the room for notices for a given user
9497
95-
If we have not yet created a notice room for this user, create it
98+
If we have not yet created a notice room for this user, create it, but don't
99+
invite the user to it.
96100
97101
Args:
98102
user_id (str): complete user id for the user we want a room for
@@ -108,18 +112,21 @@ def get_notice_room_for_user(self, user_id):
108112
rooms = yield self._store.get_rooms_for_local_user_where_membership_is(
109113
user_id, [Membership.INVITE, Membership.JOIN]
110114
)
111-
system_mxid = self._config.server_notices_mxid
112115
for room in rooms:
113116
# it's worth noting that there is an asymmetry here in that we
114117
# expect the user to be invited or joined, but the system user must
115118
# be joined. This is kinda deliberate, in that if somebody somehow
116119
# manages to invite the system user to a room, that doesn't make it
117120
# the server notices room.
118121
user_ids = yield self._store.get_users_in_room(room.room_id)
119-
if system_mxid in user_ids:
122+
if self.server_notices_mxid in user_ids:
120123
# we found a room which our user shares with the system notice
121124
# user
122-
logger.info("Using room %s", room.room_id)
125+
logger.info(
126+
"Using existing server notices room %s for user %s",
127+
room.room_id,
128+
user_id,
129+
)
123130
return room.room_id
124131

125132
# apparently no existing notice room: create a new one
@@ -138,14 +145,13 @@ def get_notice_room_for_user(self, user_id):
138145
"avatar_url": self._config.server_notices_mxid_avatar_url,
139146
}
140147

141-
requester = create_requester(system_mxid)
148+
requester = create_requester(self.server_notices_mxid)
142149
info = yield self._room_creation_handler.create_room(
143150
requester,
144151
config={
145152
"preset": RoomCreationPreset.PRIVATE_CHAT,
146153
"name": self._config.server_notices_room_name,
147154
"power_level_content_override": {"users_default": -10},
148-
"invite": (user_id,),
149155
},
150156
ratelimit=False,
151157
creator_join_profile=join_profile,
@@ -159,3 +165,30 @@ def get_notice_room_for_user(self, user_id):
159165

160166
logger.info("Created server notices room %s for %s", room_id, user_id)
161167
return room_id
168+
169+
@defer.inlineCallbacks
170+
def maybe_invite_user_to_room(self, user_id: str, room_id: str):
171+
"""Invite the given user to the given server room, unless the user has already
172+
joined or been invited to it.
173+
174+
Args:
175+
user_id: The ID of the user to invite.
176+
room_id: The ID of the room to invite the user to.
177+
"""
178+
requester = create_requester(self.server_notices_mxid)
179+
180+
# Check whether the user has already joined or been invited to this room. If
181+
# that's the case, there is no need to re-invite them.
182+
joined_rooms = yield self._store.get_rooms_for_local_user_where_membership_is(
183+
user_id, [Membership.INVITE, Membership.JOIN]
184+
)
185+
for room in joined_rooms:
186+
if room.room_id == room_id:
187+
return
188+
189+
yield self._room_member_handler.update_membership(
190+
requester=requester,
191+
target=UserID.from_string(user_id),
192+
room_id=room_id,
193+
action="invite",
194+
)

tests/server_notices/test_resource_limits_server_notices.py

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919

2020
from synapse.api.constants import EventTypes, LimitBlockingTypes, ServerNoticeMsgType
2121
from synapse.api.errors import ResourceLimitError
22+
from synapse.rest import admin
23+
from synapse.rest.client.v1 import login, room
24+
from synapse.rest.client.v2_alpha import sync
2225
from synapse.server_notices.resource_limits_server_notices import (
2326
ResourceLimitsServerNotices,
2427
)
@@ -67,7 +70,7 @@ def prepare(self, reactor, clock, hs):
6770
# self.server_notices_mxid_avatar_url = None
6871
# self.server_notices_room_name = "Server Notices"
6972

70-
self._rlsn._server_notices_manager.get_notice_room_for_user = Mock(
73+
self._rlsn._server_notices_manager.get_or_create_notice_room_for_user = Mock(
7174
returnValue=""
7275
)
7376
self._rlsn._store.add_tag_to_room = Mock()
@@ -215,6 +218,26 @@ def test_maybe_send_server_notice_when_alerting_suppressed_room_blocked(self):
215218

216219

217220
class TestResourceLimitsServerNoticesWithRealRooms(unittest.HomeserverTestCase):
221+
servlets = [
222+
admin.register_servlets,
223+
login.register_servlets,
224+
room.register_servlets,
225+
sync.register_servlets,
226+
]
227+
228+
def default_config(self):
229+
c = super().default_config()
230+
c["server_notices"] = {
231+
"system_mxid_localpart": "server",
232+
"system_mxid_display_name": None,
233+
"system_mxid_avatar_url": None,
234+
"room_name": "Test Server Notice Room",
235+
}
236+
c["limit_usage_by_mau"] = True
237+
c["max_mau_value"] = 5
238+
c["admin_contact"] = "mailto:[email protected]"
239+
return c
240+
218241
def prepare(self, reactor, clock, hs):
219242
self.store = self.hs.get_datastore()
220243
self.server_notices_sender = self.hs.get_server_notices_sender()
@@ -228,18 +251,8 @@ def prepare(self, reactor, clock, hs):
228251
if not isinstance(self._rlsn, ResourceLimitsServerNotices):
229252
raise Exception("Failed to find reference to ResourceLimitsServerNotices")
230253

231-
self.hs.config.limit_usage_by_mau = True
232-
self.hs.config.hs_disabled = False
233-
self.hs.config.max_mau_value = 5
234-
self.hs.config.server_notices_mxid = "@server:test"
235-
self.hs.config.server_notices_mxid_display_name = None
236-
self.hs.config.server_notices_mxid_avatar_url = None
237-
self.hs.config.server_notices_room_name = "Test Server Notice Room"
238-
239254
self.user_id = "@user_id:test"
240255

241-
self.hs.config.admin_contact = "mailto:[email protected]"
242-
243256
def test_server_notice_only_sent_once(self):
244257
self.store.get_monthly_active_count = Mock(return_value=1000)
245258

@@ -253,7 +266,7 @@ def test_server_notice_only_sent_once(self):
253266
# Now lets get the last load of messages in the service notice room and
254267
# check that there is only one server notice
255268
room_id = self.get_success(
256-
self.server_notices_manager.get_notice_room_for_user(self.user_id)
269+
self.server_notices_manager.get_or_create_notice_room_for_user(self.user_id)
257270
)
258271

259272
token = self.get_success(self.event_source.get_current_token())
@@ -273,3 +286,86 @@ def test_server_notice_only_sent_once(self):
273286
count += 1
274287

275288
self.assertEqual(count, 1)
289+
290+
def test_no_invite_without_notice(self):
291+
"""Tests that a user doesn't get invited to a server notices room without a
292+
server notice being sent.
293+
294+
The scenario for this test is a single user on a server where the MAU limit
295+
hasn't been reached (since it's the only user and the limit is 5), so users
296+
shouldn't receive a server notice.
297+
"""
298+
self.register_user("user", "password")
299+
tok = self.login("user", "password")
300+
301+
request, channel = self.make_request("GET", "/sync?timeout=0", access_token=tok)
302+
self.render(request)
303+
304+
invites = channel.json_body["rooms"]["invite"]
305+
self.assertEqual(len(invites), 0, invites)
306+
307+
def test_invite_with_notice(self):
308+
"""Tests that, if the MAU limit is hit, the server notices user invites each user
309+
to a room in which it has sent a notice.
310+
"""
311+
user_id, tok, room_id = self._trigger_notice_and_join()
312+
313+
# Sync again to retrieve the events in the room, so we can check whether this
314+
# room has a notice in it.
315+
request, channel = self.make_request("GET", "/sync?timeout=0", access_token=tok)
316+
self.render(request)
317+
318+
# Scan the events in the room to search for a message from the server notices
319+
# user.
320+
events = channel.json_body["rooms"]["join"][room_id]["timeline"]["events"]
321+
notice_in_room = False
322+
for event in events:
323+
if (
324+
event["type"] == EventTypes.Message
325+
and event["sender"] == self.hs.config.server_notices_mxid
326+
):
327+
notice_in_room = True
328+
329+
self.assertTrue(notice_in_room, "No server notice in room")
330+
331+
def _trigger_notice_and_join(self):
332+
"""Creates enough active users to hit the MAU limit and trigger a system notice
333+
about it, then joins the system notices room with one of the users created.
334+
335+
Returns:
336+
user_id (str): The ID of the user that joined the room.
337+
tok (str): The access token of the user that joined the room.
338+
room_id (str): The ID of the room that's been joined.
339+
"""
340+
user_id = None
341+
tok = None
342+
invites = []
343+
344+
# Register as many users as the MAU limit allows.
345+
for i in range(self.hs.config.max_mau_value):
346+
localpart = "user%d" % i
347+
user_id = self.register_user(localpart, "password")
348+
tok = self.login(localpart, "password")
349+
350+
# Sync with the user's token to mark the user as active.
351+
request, channel = self.make_request(
352+
"GET", "/sync?timeout=0", access_token=tok,
353+
)
354+
self.render(request)
355+
356+
# Also retrieves the list of invites for this user. We don't care about that
357+
# one except if we're processing the last user, which should have received an
358+
# invite to a room with a server notice about the MAU limit being reached.
359+
# We could also pick another user and sync with it, which would return an
360+
# invite to a system notices room, but it doesn't matter which user we're
361+
# using so we use the last one because it saves us an extra sync.
362+
invites = channel.json_body["rooms"]["invite"]
363+
364+
# Make sure we have an invite to process.
365+
self.assertEqual(len(invites), 1, invites)
366+
367+
# Join the room.
368+
room_id = list(invites.keys())[0]
369+
self.helper.join(room=room_id, user=user_id, tok=tok)
370+
371+
return user_id, tok, room_id

0 commit comments

Comments
 (0)