From 43d2a88e30304aec179993cc199e228f114ec20f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Sun, 18 Oct 2026 21:34:16 +0000 Subject: [PATCH] Read a joining local user's profile in one query A join without profile data read the user's display name and then their avatar URL, two transactions. For a local user with neither in the content, `get_profileinfo` now reads both at once. A remote user, or content that has one of the two, takes the same path as before. If the user has no profile row, the first read used to fail with a 404, logged, and leave both out. Now both are left out without the log line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PVLsNh6akt4ifEizY4D1ta --- synapse/handlers/room_member.py | 28 ++++++++++++++++++++-------- tests/rest/client/test_rooms.py | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index ecb4306504..bcc9d87be3 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -823,14 +823,26 @@ class RoomMemberHandler(metaclass=abc.ABCMeta): profile = self.profile_handler try: - if "displayname" not in content: - displayname = await profile.get_displayname(target) - if displayname is not None: - content["displayname"] = displayname - if "avatar_url" not in content: - avatar_url = await profile.get_avatar_url(target) - if avatar_url is not None: - content["avatar_url"] = avatar_url + if ( + "displayname" not in content + and "avatar_url" not in content + and self.hs.is_mine(target) + ): + # Both in one query. + info = await self.store.get_profileinfo(target) + if info.display_name is not None: + content["displayname"] = info.display_name + if info.avatar_url is not None: + content["avatar_url"] = info.avatar_url + else: + if "displayname" not in content: + displayname = await profile.get_displayname(target) + if displayname is not None: + content["displayname"] = displayname + if "avatar_url" not in content: + avatar_url = await profile.get_avatar_url(target) + if avatar_url is not None: + content["avatar_url"] = avatar_url except Exception as e: logger.info("Failed to get profile information for %r: %s", target, e) diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index d9602d961f..3bff1a6d34 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -3446,6 +3446,24 @@ class PerRoomProfilesForbiddenTestCase(unittest.HomeserverTestCase): self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + def test_join_has_the_global_profile(self) -> None: + """A join without profile data gets the user's display name and + avatar.""" + channel = self.make_request( + "PUT", + "/_matrix/client/r0/profile/%s/avatar_url" % (self.user_id,), + {"avatar_url": "mxc://test/avatar"}, + access_token=self.tok, + ) + self.assertEqual(channel.code, HTTPStatus.OK, channel.result) + + room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + member = self.helper.get_state( + room_id, "m.room.member", self.tok, state_key=self.user_id + ) + self.assertEqual(member["displayname"], self.displayname) + self.assertEqual(member["avatar_url"], "mxc://test/avatar") + def test_per_room_profile_forbidden(self) -> None: request_data = {"membership": "join", "displayname": "other test user"} channel = self.make_request(