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),
            ),
        )
示例#2
0
    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)
示例#4
0
    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)
示例#5
0
    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,
        )
示例#7
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)
示例#8
0
    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"])
示例#9
0
        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}
示例#10
0
        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}
示例#11
0
    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'],
            ))
示例#12
0
    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
        }
示例#13
0
    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)
示例#14
0
    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)
示例#16
0
    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
示例#17
0
    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)