def setUp(self): self.hs = yield setup_test_homeserver() self.store = UserDirectoryStore(None, self.hs) # alice and bob are both in !room_id. bobby is not but shares # a homeserver with alice. yield self.store.add_profiles_to_user_dir( "!room:id", { ALICE: ProfileInfo(None, "alice"), BOB: ProfileInfo(None, "bob"), BOBBY: ProfileInfo(None, "bobby") }, ) yield self.store.add_users_to_public_room( "!room:id", [ALICE, BOB], ) yield self.store.add_users_who_share_room( "!room:id", False, ( (ALICE, BOB), (BOB, ALICE), ), )
def test_ignore_display_names_with_null_codepoints(self) -> None: MXC_DUMMY = "mxc://dummy" # Alice creates a public room. alice = self.register_user("alice", "pass") # Alice has a user directory entry to start with. self.assertIn( alice, self.get_success( self.user_dir_helper.get_profiles_in_user_directory()), ) # Alice changes her name to include a null codepoint. self.get_success( self.hs.get_user_directory_handler().handle_local_profile_change( alice, ProfileInfo( display_name="abcd\u0000efgh", avatar_url=MXC_DUMMY, ), )) # Alice's profile should be updated with the new avatar, but no display name. self.assertEqual( self.get_success( self.user_dir_helper.get_profiles_in_user_directory()), {alice: ProfileInfo(display_name=None, avatar_url=MXC_DUMMY)}, )
def test_handle_local_profile_change_with_support_user(self) -> None: support_user_id = "@support:test" self.get_success( self.store.register_user( user_id=support_user_id, password_hash=None, user_type=UserTypes.SUPPORT ) ) regular_user_id = "@regular:test" self.get_success( self.store.register_user(user_id=regular_user_id, password_hash=None) ) self.get_success( self.handler.handle_local_profile_change( support_user_id, ProfileInfo("I love support me", None) ) ) profile = self.get_success(self.store.get_user_in_directory(support_user_id)) self.assertIsNone(profile) display_name = "display_name" profile_info = ProfileInfo(avatar_url="avatar_url", display_name=display_name) self.get_success( self.handler.handle_local_profile_change(regular_user_id, profile_info) ) profile = self.get_success(self.store.get_user_in_directory(regular_user_id)) self.assertTrue(profile["display_name"] == display_name)
def test_handle_local_profile_change_with_support_user(self): support_user_id = "@support:test" self.get_success( self.store.register( user_id=support_user_id, token="123", password_hash=None, user_type=UserTypes.SUPPORT, ) ) self.get_success( self.handler.handle_local_profile_change(support_user_id, None) ) profile = self.get_success(self.store.get_user_in_directory(support_user_id)) self.assertTrue(profile is None) display_name = 'display_name' profile_info = ProfileInfo(avatar_url='avatar_url', display_name=display_name) regular_user_id = '@regular:test' self.get_success( self.handler.handle_local_profile_change(regular_user_id, profile_info) ) profile = self.get_success(self.store.get_user_in_directory(regular_user_id)) self.assertTrue(profile['display_name'] == display_name)
def _get_joined_profiles_from_event_ids(self, event_ids): """For given set of member event_ids check if they point to a join event and if so return the associated user and profile info. Args: event_ids (Iterable[str]): The member event IDs to lookup Returns: Deferred[dict[str, Tuple[str, ProfileInfo]|None]]: Map from event ID to `user_id` and ProfileInfo (or None if not join event). """ rows = yield self._simple_select_many_batch( table="room_memberships", column="event_id", iterable=event_ids, retcols=("user_id", "display_name", "avatar_url", "event_id"), keyvalues={"membership": Membership.JOIN}, batch_size=500, desc="_get_membership_from_event_ids", ) return { row["event_id"]: ( row["user_id"], ProfileInfo(avatar_url=row["avatar_url"], display_name=row["display_name"]), ) for row in rows }
def test_per_room_profile_doesnt_alter_directory_entry(self) -> None: alice = self.register_user("alice", "pass") alice_token = self.login(alice, "pass") bob = self.register_user("bob", "pass") # Alice should have a user directory entry created at registration. users = self.get_success(self.user_dir_helper.get_profiles_in_user_directory()) self.assertEqual( users[alice], ProfileInfo(display_name="alice", avatar_url=None) ) # Alice makes a room for herself. room = self.helper.create_room_as(alice, is_public=True, tok=alice_token) # Alice sets a nickname unique to that room. self.helper.send_state( room, "m.room.member", { "displayname": "Freddy Mercury", "membership": "join", }, alice_token, state_key=alice, ) # Alice's display name remains the same in the user directory. search_result = self.get_success(self.handler.search_users(bob, alice, 10)) self.assertEqual( search_result["results"], [{"display_name": "alice", "avatar_url": None, "user_id": alice}], 0, )
def test_handle_local_profile_change_with_deactivated_user(self): # create user r_user_id = "@regular:test" self.get_success( self.store.register_user(user_id=r_user_id, password_hash=None)) # update profile display_name = "Regular User" profile_info = ProfileInfo(avatar_url="avatar_url", display_name=display_name) self.get_success( self.handler.handle_local_profile_change(r_user_id, profile_info)) # profile is in directory profile = self.get_success(self.store.get_user_in_directory(r_user_id)) self.assertTrue(profile["display_name"] == display_name) # deactivate user self.get_success( self.store.set_user_deactivated_status(r_user_id, True)) self.get_success(self.handler.handle_user_deactivated(r_user_id)) # profile is not in directory profile = self.get_success(self.store.get_user_in_directory(r_user_id)) self.assertTrue(profile is None) # update profile after deactivation self.get_success( self.handler.handle_local_profile_change(r_user_id, profile_info)) # profile is furthermore not in directory profile = self.get_success(self.store.get_user_in_directory(r_user_id)) self.assertTrue(profile is None)
def get_profileinfo(self, user_localpart): try: profile = yield self._simple_select_one( table="profiles", keyvalues={"user_id": user_localpart}, retcols=("displayname", "avatar_url"), desc="get_profileinfo", ) except StoreError as e: if e.code == 404: # no match return ProfileInfo(None, None) else: raise return ProfileInfo(avatar_url=profile["avatar_url"], display_name=profile["displayname"])
def _get_users_in_room_with_profiles(txn) -> Dict[str, ProfileInfo]: sql = """ SELECT user_id, display_name, avatar_url FROM room_memberships WHERE room_id = ? AND membership = ? """ txn.execute(sql, (room_id, Membership.JOIN)) return {r[0]: ProfileInfo(display_name=r[1], avatar_url=r[2]) for r in txn}
def _get_users_in_room_with_profiles(txn) -> Dict[str, ProfileInfo]: sql = """ SELECT state_key, display_name, avatar_url FROM room_memberships as m INNER JOIN current_state_events as c ON m.event_id = c.event_id AND m.room_id = c.room_id AND m.user_id = c.state_key WHERE c.type = 'm.room.member' AND c.room_id = ? AND m.membership = ? """ txn.execute(sql, (room_id, Membership.JOIN)) return {r[0]: ProfileInfo(display_name=r[1], avatar_url=r[2]) for r in txn}
def get_profileinfo(self, user_localpart): try: profile = yield self._simple_select_one( table="profiles", keyvalues={"user_id": user_localpart}, retcols=("profiles.displayname", "profiles.avatar_url", "profiles.dob"), desc="get_profileinfo", ) except StoreError as e: if e.code == 404: # no match defer.returnValue(ProfileInfo(None, None)) return else: raise defer.returnValue( ProfileInfo( avatar_url=profile['avatar_url'], display_name=profile['displayname'], dob=profile['dob'], ))
async def get_profiles_in_user_directory(self) -> Dict[str, ProfileInfo]: """Fetch users and their profiles from the `user_directory` table. This is useful when we want to inspect display names and avatars. It's almost the entire contents of the `user_directory` table: the only thing missing is an unused room_id column. """ rows = await self.store.db_pool.simple_select_list( "user_directory", None, ("user_id", "display_name", "avatar_url"), ) return { row["user_id"]: ProfileInfo(display_name=row["display_name"], avatar_url=row["avatar_url"]) for row in rows }
def test_handle_local_profile_change_with_appservice_sender(self) -> None: # profile is not in directory profile = self.get_success( self.store.get_user_in_directory(self.appservice.sender)) self.assertIsNone(profile) # update profile profile_info = ProfileInfo(avatar_url="avatar_url", display_name="4L1c3") self.get_success( self.handler.handle_local_profile_change(self.appservice.sender, profile_info)) # profile is still not in directory profile = self.get_success( self.store.get_user_in_directory(self.appservice.sender)) self.assertIsNone(profile)
def test_population_conceals_private_nickname(self) -> None: # Make a private room, and set a nickname within user = self.register_user("aaaa", "pass") user_token = self.login(user, "pass") private_room = self.helper.create_room_as(user, is_public=False, tok=user_token) self.helper.send_state( private_room, EventTypes.Member, state_key=user, body={ "membership": Membership.JOIN, "displayname": "BBBB" }, tok=user_token, ) # Rebuild the user directory. Make the rescan of the `users` table a no-op # so we only see the effect of scanning the `room_memberships` table. async def mocked_process_users(*args: Any, **kwargs: Any) -> int: await self.store.db_pool.updates._end_background_update( "populate_user_directory_process_users") return 1 with mock.patch.dict( self.store.db_pool.updates._background_update_handlers, populate_user_directory_process_users=_BackgroundUpdateHandler( mocked_process_users, ), ): self._purge_and_rebuild_user_dir() # Local users are ignored by the scan over rooms users = self.get_success( self.user_dir_helper.get_profiles_in_user_directory()) self.assertEqual(users, {}) # Do a full rebuild including the scan over the `users` table. The local # user should appear with their profile name. self._purge_and_rebuild_user_dir() users = self.get_success( self.user_dir_helper.get_profiles_in_user_directory()) self.assertEqual( users, {user: ProfileInfo(display_name="aaaa", avatar_url=None)})
def test_handle_local_profile_change_with_appservice_user(self) -> None: # create user as_user_id = self.register_appservice_user( "as_user_alice", self.appservice.token ) # profile is not in directory profile = self.get_success(self.store.get_user_in_directory(as_user_id)) self.assertIsNone(profile) # update profile profile_info = ProfileInfo(avatar_url="avatar_url", display_name="4L1c3") self.get_success( self.handler.handle_local_profile_change(as_user_id, profile_info) ) # profile is still not in directory profile = self.get_success(self.store.get_user_in_directory(as_user_id)) self.assertIsNone(profile)
def _get_joined_users_from_context( self, room_id, state_group, current_state_ids, cache_context, event=None, context=None, ): # We don't use `state_group`, it's there so that we can cache based # on it. However, it's important that it's never None, since two current_states # with a state_group of None are likely to be different. # See bulk_get_push_rules_for_room for how we work around this. assert state_group is not None users_in_room = {} member_event_ids = [ e_id for key, e_id in iteritems(current_state_ids) if key[0] == EventTypes.Member ] if context is not None: # If we have a context with a delta from a previous state group, # check if we also have the result from the previous group in cache. # If we do then we can reuse that result and simply update it with # any membership changes in `delta_ids` if context.prev_group and context.delta_ids: prev_res = self._get_joined_users_from_context.cache.get( (room_id, context.prev_group), None) if prev_res and isinstance(prev_res, dict): users_in_room = dict(prev_res) member_event_ids = [ e_id for key, e_id in iteritems(context.delta_ids) if key[0] == EventTypes.Member ] for etype, state_key in context.delta_ids: users_in_room.pop(state_key, None) # We check if we have any of the member event ids in the event cache # before we ask the DB # We don't update the event cache hit ratio as it completely throws off # the hit ratio counts. After all, we don't populate the cache if we # miss it here event_map = self._get_events_from_cache(member_event_ids, allow_rejected=False, update_metrics=False) missing_member_event_ids = [] for event_id in member_event_ids: ev_entry = event_map.get(event_id) if ev_entry: if ev_entry.event.membership == Membership.JOIN: users_in_room[to_ascii( ev_entry.event.state_key)] = ProfileInfo( display_name=to_ascii( ev_entry.event.content.get( "displayname", None)), avatar_url=to_ascii( ev_entry.event.content.get("avatar_url", None)), ) else: missing_member_event_ids.append(event_id) if missing_member_event_ids: event_to_memberships = yield self._get_joined_profiles_from_event_ids( missing_member_event_ids) users_in_room.update( (row for row in event_to_memberships.values() if row)) if event is not None and event.type == EventTypes.Member: if event.membership == Membership.JOIN: if event.event_id in member_event_ids: users_in_room[to_ascii(event.state_key)] = ProfileInfo( display_name=to_ascii( event.content.get("displayname", None)), avatar_url=to_ascii( event.content.get("avatar_url", None)), ) return users_in_room
async def _handle_deltas(self, deltas): """Called with the state deltas to process """ for delta in deltas: typ = delta["type"] state_key = delta["state_key"] room_id = delta["room_id"] event_id = delta["event_id"] prev_event_id = delta["prev_event_id"] logger.debug("Handling: %r %r, %s", typ, state_key, event_id) # For join rule and visibility changes we need to check if the room # may have become public or not and add/remove the users in said room if typ in (EventTypes.RoomHistoryVisibility, EventTypes.JoinRules): await self._handle_room_publicity_change( room_id, prev_event_id, event_id, typ) elif typ == EventTypes.Member: change = await self._get_key_change( prev_event_id, event_id, key_name="membership", public_value=Membership.JOIN, ) if change is False: # Need to check if the server left the room entirely, if so # we might need to remove all the users in that room is_in_room = await self.store.is_host_joined( room_id, self.server_name) if not is_in_room: logger.debug("Server left room: %r", room_id) # Fetch all the users that we marked as being in user # directory due to being in the room and then check if # need to remove those users or not user_ids = await self.store.get_users_in_dir_due_to_room( room_id) for user_id in user_ids: await self._handle_remove_user(room_id, user_id) return else: logger.debug("Server is still in room: %r", room_id) is_support = await self.store.is_support_user(state_key) if not is_support: if change is None: # Handle any profile changes await self._handle_profile_change( state_key, room_id, prev_event_id, event_id) continue if change: # The user joined event = await self.store.get_event(event_id, allow_none=True) profile = ProfileInfo( avatar_url=event.content.get("avatar_url"), display_name=event.content.get("displayname"), ) await self._handle_new_user(room_id, state_key, profile) else: # The user left await self._handle_remove_user(room_id, state_key) else: logger.debug("Ignoring irrelevant type: %r", typ)