Add the ability to exclude remote users in user directory search results (#18300)

This change adds a new configuration
`user_directory.exclude_remote_users`, which defaults to False.
When set to True, remote users will not appear in user directory search
results.

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct
(run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
This commit is contained in:
Will Lewis 2025-05-02 15:38:02 +01:00 committed by GitHub
parent b8146d4b03
commit fe8bb620de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 4 deletions

View File

@ -0,0 +1 @@
Add config option `user_directory.exclude_remote_users` which, when enabled, excludes remote users from user directory search results.

View File

@ -4095,6 +4095,7 @@ This option has the following sub-options:
* `prefer_local_users`: Defines whether to prefer local users in search query results.
If set to true, local users are more likely to appear above remote users when searching the
user directory. Defaults to false.
* `exclude_remote_users`: If set to true, the search will only return local users. Defaults to false.
* `show_locked_users`: Defines whether to show locked users in search query results. Defaults to false.
Example configuration:
@ -4103,6 +4104,7 @@ user_directory:
enabled: false
search_all_users: true
prefer_local_users: true
exclude_remote_users: false
show_locked_users: true
```
---

View File

@ -38,6 +38,9 @@ class UserDirectoryConfig(Config):
self.user_directory_search_all_users = user_directory_config.get(
"search_all_users", False
)
self.user_directory_exclude_remote_users = user_directory_config.get(
"exclude_remote_users", False
)
self.user_directory_search_prefer_local_users = user_directory_config.get(
"prefer_local_users", False
)

View File

@ -108,6 +108,9 @@ class UserDirectoryHandler(StateDeltasHandler):
self.is_mine_id = hs.is_mine_id
self.update_user_directory = hs.config.worker.should_update_user_directory
self.search_all_users = hs.config.userdirectory.user_directory_search_all_users
self.exclude_remote_users = (
hs.config.userdirectory.user_directory_exclude_remote_users
)
self.show_locked_users = hs.config.userdirectory.show_locked_users
self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker
self._hs = hs

View File

@ -1037,11 +1037,11 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
}
"""
join_args: Tuple[str, ...] = (user_id,)
if self.hs.config.userdirectory.user_directory_search_all_users:
join_args = (user_id,)
where_clause = "user_id != ?"
else:
join_args = (user_id,)
where_clause = """
(
EXISTS (select 1 from users_in_public_rooms WHERE user_id = t.user_id)
@ -1055,6 +1055,14 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
if not show_locked_users:
where_clause += " AND (u.locked IS NULL OR u.locked = FALSE)"
# Adjust the JOIN type based on the exclude_remote_users flag (the users
# table only contains local users so an inner join is a good way to
# to exclude remote users)
if self.hs.config.userdirectory.user_directory_exclude_remote_users:
join_type = "JOIN"
else:
join_type = "LEFT JOIN"
# We allow manipulating the ranking algorithm by injecting statements
# based on config options.
additional_ordering_statements = []
@ -1086,7 +1094,7 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
SELECT d.user_id AS user_id, display_name, avatar_url
FROM matching_users as t
INNER JOIN user_directory AS d USING (user_id)
LEFT JOIN users AS u ON t.user_id = u.name
%(join_type)s users AS u ON t.user_id = u.name
WHERE
%(where_clause)s
ORDER BY
@ -1115,6 +1123,7 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
""" % {
"where_clause": where_clause,
"order_case_statements": " ".join(additional_ordering_statements),
"join_type": join_type,
}
args = (
(full_query,)
@ -1142,7 +1151,7 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
SELECT d.user_id AS user_id, display_name, avatar_url
FROM user_directory_search as t
INNER JOIN user_directory AS d USING (user_id)
LEFT JOIN users AS u ON t.user_id = u.name
%(join_type)s users AS u ON t.user_id = u.name
WHERE
%(where_clause)s
AND value MATCH ?
@ -1155,6 +1164,7 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
""" % {
"where_clause": where_clause,
"order_statements": " ".join(additional_ordering_statements),
"join_type": join_type,
}
args = join_args + (search_query,) + ordering_arguments + (limit + 1,)
else:

View File

@ -992,6 +992,67 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
[self.assertIn(user, local_users) for user in received_user_id_ordering[:3]]
[self.assertIn(user, remote_users) for user in received_user_id_ordering[3:]]
@override_config(
{
"user_directory": {
"enabled": True,
"search_all_users": True,
"exclude_remote_users": True,
}
}
)
def test_exclude_remote_users(self) -> None:
"""Tests that only local users are returned when
user_directory.exclude_remote_users is True.
"""
# Create a room and few users to test the directory with
searching_user = self.register_user("searcher", "password")
searching_user_tok = self.login("searcher", "password")
room_id = self.helper.create_room_as(
searching_user,
room_version=RoomVersions.V1.identifier,
tok=searching_user_tok,
)
# Create a few local users and join them to the room
local_user_1 = self.register_user("user_xxxxx", "password")
local_user_2 = self.register_user("user_bbbbb", "password")
local_user_3 = self.register_user("user_zzzzz", "password")
self._add_user_to_room(room_id, RoomVersions.V1, local_user_1)
self._add_user_to_room(room_id, RoomVersions.V1, local_user_2)
self._add_user_to_room(room_id, RoomVersions.V1, local_user_3)
# Create a few "remote" users and join them to the room
remote_user_1 = "@user_aaaaa:remote_server"
remote_user_2 = "@user_yyyyy:remote_server"
remote_user_3 = "@user_ccccc:remote_server"
self._add_user_to_room(room_id, RoomVersions.V1, remote_user_1)
self._add_user_to_room(room_id, RoomVersions.V1, remote_user_2)
self._add_user_to_room(room_id, RoomVersions.V1, remote_user_3)
local_users = [local_user_1, local_user_2, local_user_3]
remote_users = [remote_user_1, remote_user_2, remote_user_3]
# The local searching user searches for the term "user", which other users have
# in their user id
results = self.get_success(
self.handler.search_users(searching_user, "user", 20)
)["results"]
received_user_ids = [result["user_id"] for result in results]
for user in local_users:
self.assertIn(
user, received_user_ids, f"Local user {user} not found in results"
)
for user in remote_users:
self.assertNotIn(
user, received_user_ids, f"Remote user {user} should not be in results"
)
def _add_user_to_room(
self,
room_id: str,