From 48bf78a855c5bb9c4db881dabaca221cedb31549 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 2 Nov 2023 14:39:06 +0100 Subject: [PATCH] Fix getting and using participants --- .../src/telethon/_impl/client/client/bots.py | 4 - .../src/telethon/_impl/client/client/chats.py | 16 ++- .../telethon/_impl/client/types/__init__.py | 2 + .../_impl/client/types/admin_right.py | 104 ++++++++++++++++++ .../_impl/client/types/participant.py | 100 ++++++++++++++++- client/src/telethon/types/__init__.py | 2 + .../{test_parsers.py => parsers_test.py} | 0 client/tests/types_test.py | 14 +++ 8 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 client/src/telethon/_impl/client/types/admin_right.py rename client/tests/{test_parsers.py => parsers_test.py} (100%) create mode 100644 client/tests/types_test.py diff --git a/client/src/telethon/_impl/client/client/bots.py b/client/src/telethon/_impl/client/client/bots.py index 3a17db9e..f21fefc3 100644 --- a/client/src/telethon/_impl/client/client/bots.py +++ b/client/src/telethon/_impl/client/client/bots.py @@ -10,10 +10,6 @@ if TYPE_CHECKING: class InlineResults(metaclass=NoPublicConstructor): - """ - :final: - """ - def __init__( self, client: Client, diff --git a/client/src/telethon/_impl/client/client/chats.py b/client/src/telethon/_impl/client/client/chats.py index a8a7b2a2..4adef002 100644 --- a/client/src/telethon/_impl/client/client/chats.py +++ b/client/src/telethon/_impl/client/client/chats.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Set from ...tl import abcs, functions, types from ..types import AsyncList, ChatLike, File, Participant, RecentAction, build_chat_map @@ -21,6 +21,7 @@ class ParticipantList(AsyncList[Participant]): self._chat = chat self._peer: Optional[abcs.InputPeer] = None self._offset = 0 + self._seen: Set[int] = set() async def _fetch_next(self) -> None: if self._peer is None: @@ -45,10 +46,17 @@ class ParticipantList(AsyncList[Participant]): chat_map = build_chat_map(result.users, result.chats) - self._buffer.extend( - Participant._from_raw_channel(p, chat_map) for p in result.participants - ) + seen_count = len(self._seen) + for p in result.participants: + part = Participant._from_raw_channel(p, chat_map) + pid = part._peer_id() + if pid not in self._seen: + self._seen.add(pid) + self._buffer.append(part) + self._total = result.count + self._offset += len(result.participants) + self._done = len(self._seen) == seen_count elif isinstance(self._peer, types.InputPeerChat): result = await self._client( diff --git a/client/src/telethon/_impl/client/types/__init__.py b/client/src/telethon/_impl/client/types/__init__.py index 6e03e6ef..9ceda3e0 100644 --- a/client/src/telethon/_impl/client/types/__init__.py +++ b/client/src/telethon/_impl/client/types/__init__.py @@ -1,3 +1,4 @@ +from .admin_right import AdminRight from .async_list import AsyncList from .callback_answer import CallbackAnswer from .chat import ( @@ -22,6 +23,7 @@ from .password_token import PasswordToken from .recent_action import RecentAction __all__ = [ + "AdminRight", "AsyncList", "CallbackAnswer", "Channel", diff --git a/client/src/telethon/_impl/client/types/admin_right.py b/client/src/telethon/_impl/client/types/admin_right.py new file mode 100644 index 00000000..5b1dd8ce --- /dev/null +++ b/client/src/telethon/_impl/client/types/admin_right.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from enum import Enum +from typing import Set + +from ...tl import abcs, types + + +class AdminRight(Enum): + """ + A right that can be granted to a chat's administrator. + + .. note:: + + The specific values of the enumeration are not covered by `semver `_. + They also may do nothing in future updates if Telegram decides to change them. + """ + + CHANGE_INFO = "change_info" + """Allows editing the description in a group or channel.""" + + POST_MESSAGES = "post_messages" + """Allows sending messages in a broadcast channel.""" + + EDIT_MESSAGES = "edit_messages" + """Allows editing messages in a group or channel.""" + + DELETE_MESSAGES = "delete_messages" + """Allows deleting messages in a group or channel.""" + + BAN_USERS = "ban_users" + """Allows setting the banned rights of other users in a group or channel.""" + + INVITE_USERS = "invite_users" + """Allows inviting other users to the group or channel.""" + + PIN_MESSAGES = "pin_messages" + """Allows pinning a message to the group or channel.""" + + MANAGE_ADMINS = "add_admins" + """Allows setting the same or less administrator rights to other users in the group or channel.""" + + REMAIN_ANONYMOUS = "anonymous" + """Allows the administrator to remain anonymous.""" + + MANAGE_CALLS = "manage_call" + """Allows managing group or channel calls.""" + + OTHER = "other" + """Unspecified.""" + + MANAGE_TOPICS = "manage_topics" + """Allows managing the topics in a group.""" + + POST_STORIES = "post_stories" + """Allows posting stories in a channel.""" + + EDIT_STORIES = "edit_stories" + """Allows editing stories in a channel.""" + + DELETE_STORIES = "delete_stories" + """Allows deleting stories in a channel.""" + + @classmethod + def _from_raw(cls, rights: abcs.ChatAdminRights) -> Set[AdminRight]: + assert isinstance(rights, types.ChatAdminRights) + all_rights = ( + cls.CHANGE_INFO if rights.change_info else None, + cls.POST_MESSAGES if rights.post_messages else None, + cls.EDIT_MESSAGES if rights.edit_messages else None, + cls.DELETE_MESSAGES if rights.delete_messages else None, + cls.BAN_USERS if rights.ban_users else None, + cls.INVITE_USERS if rights.invite_users else None, + cls.PIN_MESSAGES if rights.pin_messages else None, + cls.MANAGE_ADMINS if rights.add_admins else None, + cls.REMAIN_ANONYMOUS if rights.anonymous else None, + cls.MANAGE_CALLS if rights.manage_call else None, + cls.OTHER if rights.other else None, + cls.MANAGE_TOPICS if rights.manage_topics else None, + cls.POST_STORIES if rights.post_stories else None, + cls.EDIT_STORIES if rights.edit_stories else None, + cls.DELETE_STORIES if rights.delete_stories else None, + ) + return set(filter(None, iter(all_rights))) + + @classmethod + def _chat_rights(cls) -> Set[AdminRight]: + return { + cls.CHANGE_INFO, + cls.POST_MESSAGES, + cls.EDIT_MESSAGES, + cls.DELETE_MESSAGES, + cls.BAN_USERS, + cls.INVITE_USERS, + cls.PIN_MESSAGES, + cls.MANAGE_ADMINS, + cls.REMAIN_ANONYMOUS, + cls.MANAGE_CALLS, + cls.OTHER, + cls.MANAGE_TOPICS, + cls.POST_STORIES, + cls.EDIT_STORIES, + cls.DELETE_STORIES, + } diff --git a/client/src/telethon/_impl/client/types/participant.py b/client/src/telethon/_impl/client/types/participant.py index 4ec09a06..8d6360bf 100644 --- a/client/src/telethon/_impl/client/types/participant.py +++ b/client/src/telethon/_impl/client/types/participant.py @@ -1,7 +1,8 @@ -from typing import Dict, Self, Union +from typing import Dict, Optional, Self, Set, Union from ...tl import abcs, types -from .chat import Chat +from .admin_right import AdminRight +from .chat import Chat, User, peer_id from .meta import NoPublicConstructor @@ -66,3 +67,98 @@ class Participant(metaclass=NoPublicConstructor): return cls._create(participant, chat_map) else: raise RuntimeError("unexpected case") + + def _peer_id(self) -> int: + if isinstance( + self._raw, + ( + types.ChannelParticipant, + types.ChannelParticipantSelf, + types.ChannelParticipantCreator, + types.ChannelParticipantAdmin, + types.ChatParticipant, + types.ChatParticipantCreator, + types.ChatParticipantAdmin, + ), + ): + return self._raw.user_id + elif isinstance( + self._raw, (types.ChannelParticipantBanned, types.ChannelParticipantLeft) + ): + return peer_id(self._raw.peer) + else: + raise RuntimeError("unexpected case") + + @property + def user(self) -> Optional[User]: + """ + The user participant that is currently present in the chat. + + This will be :data:`None` if the participant was instead :attr:`banned` or has :attr:`left`. + """ + if isinstance( + self._raw, + ( + types.ChannelParticipant, + types.ChannelParticipantSelf, + types.ChannelParticipantCreator, + types.ChannelParticipantAdmin, + types.ChatParticipant, + types.ChatParticipantCreator, + types.ChatParticipantAdmin, + ), + ): + user = self._chat_map[self._raw.user_id] + assert isinstance(user, User) + return user + else: + return None + + @property + def banned(self) -> Optional[Chat]: + """ + The banned participant. + + This will usually be a :class:`User`. + """ + if isinstance(self._raw, types.ChannelParticipantBanned): + return self._chat_map[peer_id(self._raw.peer)] + else: + return None + + @property + def left(self) -> Optional[Chat]: + """ + The participant that has left the group. + + This will usually be a :class:`User`. + """ + if isinstance(self._raw, types.ChannelParticipantLeft): + return self._chat_map[peer_id(self._raw.peer)] + else: + return None + + @property + def creator(self) -> bool: + """ + :data:`True` if the participant is the creator of the chat. + """ + return isinstance( + self._raw, (types.ChannelParticipantCreator, types.ChatParticipantCreator) + ) + + @property + def admin_rights(self) -> Optional[Set[AdminRight]]: + """ + The set of administrator rights this participant has been granted, if they are an administrator. + """ + if isinstance( + self._raw, (types.ChannelParticipantCreator, types.ChannelParticipantAdmin) + ): + return AdminRight._from_raw(self._raw.admin_rights) + elif isinstance( + self._raw, (types.ChatParticipantCreator, types.ChatParticipantAdmin) + ): + return AdminRight._chat_rights() + else: + return None diff --git a/client/src/telethon/types/__init__.py b/client/src/telethon/types/__init__.py index 29391b0c..8a40c5cf 100644 --- a/client/src/telethon/types/__init__.py +++ b/client/src/telethon/types/__init__.py @@ -2,6 +2,7 @@ Classes for the various objects the library returns. """ from .._impl.client.types import ( + AdminRight, AsyncList, CallbackAnswer, Channel, @@ -22,6 +23,7 @@ from .._impl.client.types.buttons import Button, InlineButton from .._impl.session import PackedChat, PackedType __all__ = [ + "AdminRight", "AsyncList", "CallbackAnswer", "Channel", diff --git a/client/tests/test_parsers.py b/client/tests/parsers_test.py similarity index 100% rename from client/tests/test_parsers.py rename to client/tests/parsers_test.py diff --git a/client/tests/types_test.py b/client/tests/types_test.py new file mode 100644 index 00000000..e19dc957 --- /dev/null +++ b/client/tests/types_test.py @@ -0,0 +1,14 @@ +from pytest import mark +from telethon._impl.client.types import AdminRight +from telethon._impl.tl import types + + +@mark.parametrize("slot", types.ChatAdminRights.__slots__) +def test_admin_right_covers_all(slot: str) -> None: + kwargs = {slot: False for slot in types.ChatAdminRights.__slots__} + kwargs[slot] = True + + rights = types.ChatAdminRights(**kwargs) + rights_set = AdminRight._from_raw(rights) + assert len(rights_set) == 1 + assert next(iter(rights_set)).value == slot