Skip to content

Commit eedab12

Browse files
authored
forward requester id to check username for spam callbacks (#17916)
1 parent 483602e commit eedab12

File tree

6 files changed

+70
-7
lines changed

6 files changed

+70
-7
lines changed

changelog.d/17916.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Module developers will have access to user id of requester when adding `check_username_for_spam` callbacks to `spam_checker_module_callbacks`. Contributed by Wilson@Pangea.chat.

docs/modules/spam_checker_callbacks.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ this callback.
245245
_First introduced in Synapse v1.37.0_
246246

247247
```python
248-
async def check_username_for_spam(user_profile: synapse.module_api.UserProfile) -> bool
248+
async def check_username_for_spam(user_profile: synapse.module_api.UserProfile, requester_id: str) -> bool
249249
```
250250

251251
Called when computing search results in the user directory. The module must return a
@@ -264,6 +264,8 @@ The profile is represented as a dictionary with the following keys:
264264
The module is given a copy of the original dictionary, so modifying it from within the
265265
module cannot modify a user's profile when included in user directory search results.
266266

267+
The requester_id parameter is the ID of the user that called the user directory API.
268+
267269
If multiple modules implement this callback, they will be considered in order. If a
268270
callback returns `False`, Synapse falls through to the next one. The value of the first
269271
callback that does not return `False` will be used. If this happens, Synapse will not call

docs/spam_checker.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ class ExampleSpamChecker:
7272
async def user_may_publish_room(self, userid, room_id):
7373
return True # allow publishing of all rooms
7474

75-
async def check_username_for_spam(self, user_profile):
76-
return False # allow all usernames
75+
async def check_username_for_spam(self, user_profile, requester_id):
76+
return False # allow all usernames regardless of requester
7777

7878
async def check_registration_for_spam(
7979
self,

synapse/handlers/user_directory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ async def search_users(
161161
non_spammy_users = []
162162
for user in results["results"]:
163163
if not await self._spam_checker_module_callbacks.check_username_for_spam(
164-
user
164+
user, user_id
165165
):
166166
non_spammy_users.append(user)
167167
results["results"] = non_spammy_users

synapse/module_api/callbacks/spamchecker_callbacks.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
Optional,
3232
Tuple,
3333
Union,
34+
cast,
3435
)
3536

3637
# `Literal` appears with Python 3.8.
@@ -168,7 +169,10 @@
168169
]
169170
],
170171
]
171-
CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]]
172+
CHECK_USERNAME_FOR_SPAM_CALLBACK = Union[
173+
Callable[[UserProfile], Awaitable[bool]],
174+
Callable[[UserProfile, str], Awaitable[bool]],
175+
]
172176
LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[
173177
[
174178
Optional[dict],
@@ -716,7 +720,9 @@ async def user_may_publish_room(
716720

717721
return self.NOT_SPAM
718722

719-
async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
723+
async def check_username_for_spam(
724+
self, user_profile: UserProfile, requester_id: str
725+
) -> bool:
720726
"""Checks if a user ID or display name are considered "spammy" by this server.
721727
722728
If the server considers a username spammy, then it will not be included in
@@ -727,15 +733,33 @@ async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
727733
* user_id
728734
* display_name
729735
* avatar_url
736+
requester_id: The user ID of the user making the user directory search request.
730737
731738
Returns:
732739
True if the user is spammy.
733740
"""
734741
for callback in self._check_username_for_spam_callbacks:
735742
with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
743+
checker_args = inspect.signature(callback)
736744
# Make a copy of the user profile object to ensure the spam checker cannot
737745
# modify it.
738-
res = await delay_cancellation(callback(user_profile.copy()))
746+
# Also ensure backwards compatibility with spam checker callbacks
747+
# that don't expect the requester_id argument.
748+
if len(checker_args.parameters) == 2:
749+
callback_with_requester_id = cast(
750+
Callable[[UserProfile, str], Awaitable[bool]], callback
751+
)
752+
res = await delay_cancellation(
753+
callback_with_requester_id(user_profile.copy(), requester_id)
754+
)
755+
else:
756+
callback_without_requester_id = cast(
757+
Callable[[UserProfile], Awaitable[bool]], callback
758+
)
759+
res = await delay_cancellation(
760+
callback_without_requester_id(user_profile.copy())
761+
)
762+
739763
if res:
740764
return True
741765

tests/handlers/test_user_directory.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ def test_spam_checker(self) -> None:
796796
s = self.get_success(self.handler.search_users(u1, "user2", 10))
797797
self.assertEqual(len(s["results"]), 1)
798798

799+
# Kept old spam checker without `requester_id` tests for backwards compatibility.
799800
async def allow_all(user_profile: UserProfile) -> bool:
800801
# Allow all users.
801802
return False
@@ -809,6 +810,7 @@ async def allow_all(user_profile: UserProfile) -> bool:
809810
s = self.get_success(self.handler.search_users(u1, "user2", 10))
810811
self.assertEqual(len(s["results"]), 1)
811812

813+
# Kept old spam checker without `requester_id` tests for backwards compatibility.
812814
# Configure a spam checker that filters all users.
813815
async def block_all(user_profile: UserProfile) -> bool:
814816
# All users are spammy.
@@ -820,6 +822,40 @@ async def block_all(user_profile: UserProfile) -> bool:
820822
s = self.get_success(self.handler.search_users(u1, "user2", 10))
821823
self.assertEqual(len(s["results"]), 0)
822824

825+
async def allow_all_expects_requester_id(
826+
user_profile: UserProfile, requester_id: str
827+
) -> bool:
828+
self.assertEqual(requester_id, u1)
829+
# Allow all users.
830+
return False
831+
832+
# Configure a spam checker that does not filter any users.
833+
spam_checker = self.hs.get_module_api_callbacks().spam_checker
834+
spam_checker._check_username_for_spam_callbacks = [
835+
allow_all_expects_requester_id
836+
]
837+
838+
# The results do not change:
839+
# We get one search result when searching for user2 by user1.
840+
s = self.get_success(self.handler.search_users(u1, "user2", 10))
841+
self.assertEqual(len(s["results"]), 1)
842+
843+
# Configure a spam checker that filters all users.
844+
async def block_all_expects_requester_id(
845+
user_profile: UserProfile, requester_id: str
846+
) -> bool:
847+
self.assertEqual(requester_id, u1)
848+
# All users are spammy.
849+
return True
850+
851+
spam_checker._check_username_for_spam_callbacks = [
852+
block_all_expects_requester_id
853+
]
854+
855+
# User1 now gets no search results for any of the other users.
856+
s = self.get_success(self.handler.search_users(u1, "user2", 10))
857+
self.assertEqual(len(s["results"]), 0)
858+
823859
@override_config(
824860
{
825861
"spam_checker": {

0 commit comments

Comments
 (0)