diff --git a/client/doc/developing/migration-guide.rst b/client/doc/developing/migration-guide.rst index 6040fb8d..6a9fdd9c 100644 --- a/client/doc/developing/migration-guide.rst +++ b/client/doc/developing/migration-guide.rst @@ -244,14 +244,17 @@ The simplest approach could be using a global ``states`` dictionary storing the This is not a thing in Telegram. It was implemented by restricting and then removing the restriction. -The old ``client.edit_permissions()`` was renamed to :meth:`Client.set_banned_rights`. -This defines the rights a restricted participant has (bans them from doing other things). +The old ``client.edit_permissions()`` was renamed to :meth:`Client.set_participant_restrictions`. +This defines the restrictions a banned participant has applied (bans them from doing those things). Revoking the right to view messages will kick them. This rename should avoid confusion, as it is now clear this is not to promote users to admin status. -For administrators, ``client.edit_admin`` was renamed to :meth:`Client.set_admin_rights` for consistency. +For administrators, ``client.edit_admin`` was renamed to :meth:`Client.set_participant_admin_rights` for consistency. -Note that a new method, :meth:`Client.set_default_rights`, must now be used to set a chat's default rights. +You can also use the aliases on the :class:`~types.Participant`, :meth:`types.Participant.set_restrictions` and :meth:`types.Participant.set_admin_rights`. + +Note that a new method, :meth:`Client.set_chat_default_restrictions`, must now be used to set a chat's default rights. +You can also use the alias on the :class:`~types.Group`, :meth:`types.Group.set_default_restrictions`. .. rubric:: No ``client.download_profile_photo()`` method. diff --git a/client/src/telethon/_impl/client/client/chats.py b/client/src/telethon/_impl/client/client/chats.py index 4adef002..32cca36f 100644 --- a/client/src/telethon/_impl/client/client/chats.py +++ b/client/src/telethon/_impl/client/client/chats.py @@ -1,9 +1,20 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Set +import datetime +from typing import TYPE_CHECKING, Optional, Sequence, Set +from ...session import PackedChat from ...tl import abcs, functions, types -from ..types import AsyncList, ChatLike, File, Participant, RecentAction, build_chat_map +from ..types import ( + AdminRight, + AsyncList, + ChatLike, + ChatRestriction, + File, + Participant, + RecentAction, + build_chat_map, +) from .messages import SearchList if TYPE_CHECKING: @@ -19,65 +30,65 @@ class ParticipantList(AsyncList[Participant]): super().__init__() self._client = client self._chat = chat - self._peer: Optional[abcs.InputPeer] = None + self._packed: Optional[PackedChat] = None self._offset = 0 self._seen: Set[int] = set() async def _fetch_next(self) -> None: - if self._peer is None: - self._peer = ( - await self._client._resolve_to_packed(self._chat) - )._to_input_peer() + if self._packed is None: + self._packed = await self._client._resolve_to_packed(self._chat) - if isinstance(self._peer, types.InputPeerChannel): - result = await self._client( + if self._packed.is_channel(): + chanp = await self._client( functions.channels.get_participants( - channel=types.InputChannel( - channel_id=self._peer.channel_id, - access_hash=self._peer.access_hash, - ), + channel=self._packed._to_input_channel(), filter=types.ChannelParticipantsRecent(), offset=self._offset, limit=200, hash=0, ) ) - assert isinstance(result, types.channels.ChannelParticipants) + assert isinstance(chanp, types.channels.ChannelParticipants) - chat_map = build_chat_map(result.users, result.chats) + chat_map = build_chat_map(self._client, chanp.users, chanp.chats) seen_count = len(self._seen) - for p in result.participants: - part = Participant._from_raw_channel(p, chat_map) + for p in chanp.participants: + part = Participant._from_raw_channel( + self._client, self._packed, 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._total = chanp.count + self._offset += len(chanp.participants) self._done = len(self._seen) == seen_count - elif isinstance(self._peer, types.InputPeerChat): - result = await self._client( - functions.messages.get_full_chat(chat_id=self._peer.chat_id) # type: ignore [arg-type] + elif self._packed.is_chat(): + chatp = await self._client( + functions.messages.get_full_chat(chat_id=self._packed.id) ) - assert isinstance(result, types.messages.ChatFull) - assert isinstance(result.full_chat, types.ChatFull) + assert isinstance(chatp, types.messages.ChatFull) + assert isinstance(chatp.full_chat, types.ChatFull) - chat_map = build_chat_map(result.users, result.chats) + chat_map = build_chat_map(self._client, chatp.users, chatp.chats) - participants = result.full_chat.participants + participants = chatp.full_chat.participants if isinstance(participants, types.ChatParticipantsForbidden): if participants.self_participant: self._buffer.append( Participant._from_raw_chat( - participants.self_participant, chat_map + self._client, + self._packed, + participants.self_participant, + chat_map, ) ) elif isinstance(participants, types.ChatParticipants): self._buffer.extend( - Participant._from_raw_chat(p, chat_map) + Participant._from_raw_chat(self._client, self._packed, p, chat_map) for p in participants.participants ) @@ -122,7 +133,7 @@ class RecentActionList(AsyncList[RecentAction]): ) assert isinstance(result, types.channels.AdminLogResults) - chat_map = build_chat_map(result.users, result.chats) + chat_map = build_chat_map(self._client, result.users, result.chats) self._buffer.extend(RecentAction._create(e, chat_map) for e in result.events) self._total += len(self._buffer) @@ -184,13 +195,82 @@ def get_profile_photos(self: Client, chat: ChatLike) -> AsyncList[File]: return ProfilePhotoList(self, chat) -def set_banned_rights(self: Client, chat: ChatLike, user: ChatLike) -> None: - pass +async def set_participant_admin_rights( + self: Client, chat: ChatLike, user: ChatLike, rights: Sequence[AdminRight] +) -> None: + packed = await self._resolve_to_packed(chat) + participant = await self._resolve_to_packed(user) + + if packed.is_channel(): + admin_rights = AdminRight._set_to_raw(set(rights)) + await self( + functions.channels.edit_admin( + channel=packed._to_input_channel(), + user_id=participant._to_input_user(), + admin_rights=admin_rights, + rank="", + ) + ) + elif packed.is_chat(): + await self( + functions.messages.edit_chat_admin( + chat_id=packed.id, + user_id=participant._to_input_user(), + is_admin=bool(rights), + ) + ) + else: + raise TypeError(f"Cannot set admin rights in {packed.ty}") -def set_admin_rights(self: Client, chat: ChatLike, user: ChatLike) -> None: - pass +async def set_participant_restrictions( + self: Client, + chat: ChatLike, + user: ChatLike, + restrictions: Sequence[ChatRestriction], + *, + until: Optional[datetime.datetime] = None, +) -> None: + packed = await self._resolve_to_packed(chat) + participant = await self._resolve_to_packed(user) + if packed.is_channel(): + banned_rights = ChatRestriction._set_to_raw( + set(restrictions), + until_date=int(until.timestamp()) if until else 0x7FFFFFFF, + ) + await self( + functions.channels.edit_banned( + channel=packed._to_input_channel(), + participant=participant._to_input_peer(), + banned_rights=banned_rights, + ) + ) + elif packed.is_chat(): + if restrictions: + await self( + functions.messages.delete_chat_user( + revoke_history=ChatRestriction.VIEW_MESSAGES in restrictions, + chat_id=packed.id, + user_id=participant._to_input_user(), + ) + ) + else: + raise TypeError(f"Cannot set banned rights in {packed.ty}") -def set_default_rights(self: Client, chat: ChatLike, user: ChatLike) -> None: - pass +async def set_chat_default_restrictions( + self: Client, + chat: ChatLike, + restrictions: Sequence[ChatRestriction], + *, + until: Optional[datetime.datetime] = None, +) -> None: + peer = (await self._resolve_to_packed(chat))._to_input_peer() + banned_rights = ChatRestriction._set_to_raw( + set(restrictions), int(until.timestamp()) if until else 0x7FFFFFFF + ) + await self( + functions.messages.edit_chat_default_banned_rights( + peer=peer, banned_rights=banned_rights + ) + ) diff --git a/client/src/telethon/_impl/client/client/client.py b/client/src/telethon/_impl/client/client/client.py index 63e091d2..7382c8d1 100644 --- a/client/src/telethon/_impl/client/client/client.py +++ b/client/src/telethon/_impl/client/client/client.py @@ -13,6 +13,7 @@ from typing import ( Literal, Optional, Self, + Sequence, Tuple, Type, TypeVar, @@ -35,9 +36,11 @@ from ...tl import Request, abcs from ..events import Event from ..events.filters import Filter from ..types import ( + AdminRight, AsyncList, Chat, ChatLike, + ChatRestriction, Dialog, Draft, File, @@ -66,9 +69,9 @@ from .chats import ( get_admin_log, get_participants, get_profile_photos, - set_admin_rights, - set_banned_rights, - set_default_rights, + set_chat_default_restrictions, + set_participant_admin_rights, + set_participant_restrictions, ) from .dialogs import delete_dialog, edit_draft, get_dialogs, get_drafts from .files import ( @@ -1700,14 +1703,46 @@ class Client: caption_html=caption_html, ) - def set_admin_rights(self, chat: ChatLike, user: ChatLike) -> None: - set_admin_rights(self, chat, user) + async def set_chat_default_restrictions( + self, + chat: ChatLike, + restrictions: Sequence[ChatRestriction], + *, + until: Optional[datetime.datetime] = None, + ) -> None: + """ + Set the default restrictions to apply to all participant in a chat. - def set_banned_rights(self, chat: ChatLike, user: ChatLike) -> None: - set_banned_rights(self, chat, user) + :param chat: + The :term:`chat` where the restrictions will be applied. - def set_default_rights(self, chat: ChatLike, user: ChatLike) -> None: - set_default_rights(self, chat, user) + :param restrictions: + The sequence of restrictions to apply. + + :param until: + Date until which the restrictions should be applied. + By default, restrictions apply for as long as possible. + + .. rubric:: Example + + .. code-block:: python + + from datetime import datetime, timedelta + from telethon.types import ChatRestriction + + # Don't allow anyone except administrators to send stickers for a day + await client.set_chat_default_restrictions( + chat, user, [ChatRestriction.SEND_STICKERS], + until=datetime.now() + timedelta(days=1)) + + # Remove all default restrictions from the chat + await client.set_chat_default_restrictions(chat, user, []) + + .. seealso:: + + :meth:`telethon.types.Group.set_default_restrictions` + """ + await set_chat_default_restrictions(self, chat, restrictions, until=until) def set_handler_filter( self, @@ -1737,6 +1772,102 @@ class Client: """ set_handler_filter(self, handler, filter) + async def set_participant_admin_rights( + self, chat: ChatLike, user: ChatLike, rights: Sequence[AdminRight] + ) -> None: + """ + Set the administrator rights granted to the participant in the chat. + + If an empty sequence of rights is given, the user will be demoted and stop being an administrator. + + In small group chats, there are no separate administrator rights. + In this case, granting any right will make the user an administrator with all rights. + + :param chat: + The :term:`chat` where the rights will be granted. + + :param participant: + The participant to promote to administrator, usually a :class:`types.User`. + + :param rights: + The sequence of rights to grant. + Can be empty to revoke the administrator status from the participant. + + .. rubric:: Example + + .. code-block:: python + + from telethon.types import AdminRight + + # Make user an administrator allowed to pin messages + await client.set_participant_admin_rights( + chat, user, [AdminRight.PIN_MESSAGES]) + + # Demote an administrator + await client.set_participant_admin_rights(chat, user, []) + + .. seealso:: + + :meth:`telethon.types.Participant.set_admin_rights` + """ + await set_participant_admin_rights(self, chat, user, rights) + + async def set_participant_restrictions( + self, + chat: ChatLike, + user: ChatLike, + restrictions: Sequence[ChatRestriction], + *, + until: Optional[datetime.datetime] = None, + ) -> None: + """ + Set the restrictions to apply to a participant in the chat. + + Restricting the participant to :attr:`~types.ChatRestriction.VIEW_MESSAGES` will kick them out of the chat. + + In small group chats, there are no separate restrictions. + In this case, any restriction will kick the participant. + The participant's history will be revoked if the restriction to :attr:`~types.ChatRestriction.VIEW_MESSAGES` is applied. + + :param chat: + The :term:`chat` where the restrictions will be applied. + + :param participant: + The participant to restrict or ban, usually a :class:`types.User`. + + :param restrictions: + The sequence of restrictions to apply. + Can be empty to remove all restrictions from the participant and unban them. + + :param until: + Date until which the restrictions should be applied. + By default, restrictions apply for as long as possible. + + .. rubric:: Example + + .. code-block:: python + + from datetime import datetime, timedelta + from telethon.types import ChatRestriction + + # Kick the user out of the chat + await client.set_participant_restrictions( + chat, user, [ChatRestriction.VIEW_MESSAGES]) + + # Don't allow the user to send media for 5 minutes + await client.set_participant_restrictions( + chat, user, [ChatRestriction.SEND_MEDIA], + until=datetime.now() + timedelta(minutes=5)) + + # Unban the user + await client.set_participant_restrictions(chat, user, []) + + .. seealso:: + + :meth:`telethon.types.Participant.set_restrictions` + """ + await set_participant_restrictions(self, chat, user, restrictions, until=until) + async def sign_in(self, token: LoginToken, code: str) -> Union[User, PasswordToken]: """ Sign in to a user account. diff --git a/client/src/telethon/_impl/client/client/dialogs.py b/client/src/telethon/_impl/client/client/dialogs.py index e52a723c..6fdabd74 100644 --- a/client/src/telethon/_impl/client/client/dialogs.py +++ b/client/src/telethon/_impl/client/client/dialogs.py @@ -40,7 +40,7 @@ class DialogList(AsyncList[Dialog]): assert isinstance(result, (types.messages.Dialogs, types.messages.DialogsSlice)) - chat_map = build_chat_map(result.users, result.chats) + chat_map = build_chat_map(self._client, result.users, result.chats) msg_map = build_msg_map(self._client, result.messages, chat_map) self._buffer.extend( @@ -94,7 +94,7 @@ class DraftList(AsyncList[Draft]): result = await self._client(functions.messages.get_all_drafts()) assert isinstance(result, types.Updates) - chat_map = build_chat_map(result.users, result.chats) + chat_map = build_chat_map(self._client, result.users, result.chats) self._buffer.extend( Draft._from_raw_update(self._client, u, chat_map) diff --git a/client/src/telethon/_impl/client/client/messages.py b/client/src/telethon/_impl/client/client/messages.py index 0602b2ca..3f585be3 100644 --- a/client/src/telethon/_impl/client/client/messages.py +++ b/client/src/telethon/_impl/client/client/messages.py @@ -271,7 +271,7 @@ class MessageList(AsyncList[Message]): else: raise RuntimeError("unexpected case") - chat_map = build_chat_map(messages.users, messages.chats) + chat_map = build_chat_map(client, messages.users, messages.chats) self._buffer.extend( Message._from_raw(client, m, chat_map) for m in ( @@ -650,7 +650,7 @@ def build_message_map( ) -> MessageMap: if isinstance(result, (types.Updates, types.UpdatesCombined)): updates = result.updates - chat_map = build_chat_map(result.users, result.chats) + chat_map = build_chat_map(client, result.users, result.chats) elif isinstance(result, types.UpdateShort): updates = [result.update] chat_map = {} diff --git a/client/src/telethon/_impl/client/client/updates.py b/client/src/telethon/_impl/client/client/updates.py index 0534baef..d0d8bb6d 100644 --- a/client/src/telethon/_impl/client/client/updates.py +++ b/client/src/telethon/_impl/client/client/updates.py @@ -106,7 +106,7 @@ def extend_update_queue( users: List[abcs.User], chats: List[abcs.Chat], ) -> None: - chat_map = build_chat_map(users, chats) + chat_map = build_chat_map(client, users, chats) for update in updates: try: diff --git a/client/src/telethon/_impl/client/client/users.py b/client/src/telethon/_impl/client/client/users.py index eb6b72bc..2345e405 100644 --- a/client/src/telethon/_impl/client/client/users.py +++ b/client/src/telethon/_impl/client/client/users.py @@ -51,10 +51,10 @@ def get_contacts(self: Client) -> AsyncList[User]: return ContactList(self) -def resolved_peer_to_chat(resolved: abcs.contacts.ResolvedPeer) -> Chat: +def resolved_peer_to_chat(client: Client, resolved: abcs.contacts.ResolvedPeer) -> Chat: assert isinstance(resolved, types.contacts.ResolvedPeer) - map = build_chat_map(resolved.users, resolved.chats) + map = build_chat_map(client, resolved.users, resolved.chats) if chat := map.get(peer_id(resolved.peer)): return chat else: @@ -63,13 +63,13 @@ def resolved_peer_to_chat(resolved: abcs.contacts.ResolvedPeer) -> Chat: async def resolve_phone(client: Client, phone: str) -> Chat: return resolved_peer_to_chat( - await client(functions.contacts.resolve_phone(phone=phone)) + client, await client(functions.contacts.resolve_phone(phone=phone)) ) async def resolve_username(self: Client, username: str) -> Chat: return resolved_peer_to_chat( - await self(functions.contacts.resolve_username(username=username)) + self, await self(functions.contacts.resolve_username(username=username)) ) diff --git a/client/src/telethon/_impl/client/types/__init__.py b/client/src/telethon/_impl/client/types/__init__.py index 9ceda3e0..1d13b2cc 100644 --- a/client/src/telethon/_impl/client/types/__init__.py +++ b/client/src/telethon/_impl/client/types/__init__.py @@ -11,6 +11,7 @@ from .chat import ( expand_peer, peer_id, ) +from .chat_restriction import ChatRestriction from .dialog import Dialog from .draft import Draft from .file import File, InFileLike, OutFileLike, OutWrapper, expand_stripped_size @@ -25,6 +26,7 @@ from .recent_action import RecentAction __all__ = [ "AdminRight", "AsyncList", + "ChatRestriction", "CallbackAnswer", "Channel", "Chat", diff --git a/client/src/telethon/_impl/client/types/admin_right.py b/client/src/telethon/_impl/client/types/admin_right.py index 5b1dd8ce..4c34d414 100644 --- a/client/src/telethon/_impl/client/types/admin_right.py +++ b/client/src/telethon/_impl/client/types/admin_right.py @@ -102,3 +102,23 @@ class AdminRight(Enum): cls.EDIT_STORIES, cls.DELETE_STORIES, } + + @classmethod + def _set_to_raw(cls, all_rights: Set[AdminRight]) -> types.ChatAdminRights: + return types.ChatAdminRights( + change_info=cls.CHANGE_INFO in all_rights, + post_messages=cls.POST_MESSAGES in all_rights, + edit_messages=cls.EDIT_MESSAGES in all_rights, + delete_messages=cls.DELETE_MESSAGES in all_rights, + ban_users=cls.BAN_USERS in all_rights, + invite_users=cls.INVITE_USERS in all_rights, + pin_messages=cls.PIN_MESSAGES in all_rights, + add_admins=cls.MANAGE_ADMINS in all_rights, + anonymous=cls.REMAIN_ANONYMOUS in all_rights, + manage_call=cls.MANAGE_CALLS in all_rights, + other=cls.OTHER in all_rights, + manage_topics=cls.MANAGE_TOPICS in all_rights, + post_stories=cls.POST_STORIES in all_rights, + edit_stories=cls.EDIT_STORIES in all_rights, + delete_stories=cls.DELETE_STORIES in all_rights, + ) diff --git a/client/src/telethon/_impl/client/types/chat/__init__.py b/client/src/telethon/_impl/client/types/chat/__init__.py index 21c860a8..1ef819c2 100644 --- a/client/src/telethon/_impl/client/types/chat/__init__.py +++ b/client/src/telethon/_impl/client/types/chat/__init__.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import itertools import sys from collections import defaultdict -from typing import DefaultDict, Dict, List, Optional, Union +from typing import TYPE_CHECKING, DefaultDict, Dict, List, Optional, Union from ....session import PackedChat from ....tl import abcs, types @@ -10,15 +12,20 @@ from .chat import Chat from .group import Group from .user import User +if TYPE_CHECKING: + from ...client.client import Client + ChatLike = Union[Chat, PackedChat, int, str] -def build_chat_map(users: List[abcs.User], chats: List[abcs.Chat]) -> Dict[int, Chat]: +def build_chat_map( + client: Client, users: List[abcs.User], chats: List[abcs.Chat] +) -> Dict[int, Chat]: users_iter = (User._from_raw(u) for u in users) chats_iter = ( Channel._from_raw(c) if isinstance(c, (types.Channel, types.ChannelForbidden)) and c.broadcast - else Group._from_raw(c) + else Group._from_raw(client, c) for c in chats ) @@ -57,11 +64,11 @@ def peer_id(peer: abcs.Peer) -> int: raise RuntimeError("unexpected case") -def expand_peer(peer: abcs.Peer, *, broadcast: Optional[bool]) -> Chat: +def expand_peer(client: Client, peer: abcs.Peer, *, broadcast: Optional[bool]) -> Chat: if isinstance(peer, types.PeerUser): return User._from_raw(types.UserEmpty(id=peer.user_id)) elif isinstance(peer, types.PeerChat): - return Group._from_raw(types.ChatEmpty(id=peer.chat_id)) + return Group._from_raw(client, types.ChatEmpty(id=peer.chat_id)) elif isinstance(peer, types.PeerChannel): if broadcast is None: broadcast = True # assume broadcast by default (Channel type is more accurate than Group) @@ -75,7 +82,11 @@ def expand_peer(peer: abcs.Peer, *, broadcast: Optional[bool]) -> Chat: until_date=None, ) - return Channel._from_raw(channel) if broadcast else Group._from_raw(channel) + return ( + Channel._from_raw(channel) + if broadcast + else Group._from_raw(client, channel) + ) else: raise RuntimeError("unexpected case") diff --git a/client/src/telethon/_impl/client/types/chat/group.py b/client/src/telethon/_impl/client/types/chat/group.py index 1e8d9ec2..5999bc15 100644 --- a/client/src/telethon/_impl/client/types/chat/group.py +++ b/client/src/telethon/_impl/client/types/chat/group.py @@ -1,10 +1,17 @@ -from typing import Optional, Self, Union +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING, Optional, Self, Sequence, Union from ....session import PackedChat, PackedType from ....tl import abcs, types +from ..chat_restriction import ChatRestriction from ..meta import NoPublicConstructor from .chat import Chat +if TYPE_CHECKING: + from ...client.client import Client + class Group(Chat, metaclass=NoPublicConstructor): """ @@ -18,7 +25,8 @@ class Group(Chat, metaclass=NoPublicConstructor): def __init__( self, - raw: Union[ + client: Client, + chat: Union[ types.ChatEmpty, types.Chat, types.ChatForbidden, @@ -26,16 +34,17 @@ class Group(Chat, metaclass=NoPublicConstructor): types.ChannelForbidden, ], ) -> None: - self._raw = raw + self._client = client + self._raw = chat @classmethod - def _from_raw(cls, chat: abcs.Chat) -> Self: + def _from_raw(cls, client: Client, chat: abcs.Chat) -> Self: if isinstance(chat, (types.ChatEmpty, types.Chat, types.ChatForbidden)): - return cls._create(chat) + return cls._create(client, chat) elif isinstance(chat, (types.Channel, types.ChannelForbidden)): if chat.broadcast: raise RuntimeError("cannot create group from broadcast channel") - return cls._create(chat) + return cls._create(client, chat) else: raise RuntimeError("unexpected case") @@ -80,3 +89,16 @@ class Group(Chat, metaclass=NoPublicConstructor): These are known as "megagroups" in Telegram's API, and are different from "gigagroups". """ return isinstance(self._raw, (types.Channel, types.ChannelForbidden)) + + async def set_default_restrictions( + self, + restrictions: Sequence[ChatRestriction], + *, + until: Optional[datetime.datetime] = None, + ) -> None: + """ + Alias for :meth:`telethon.Client.set_chat_default_restrictions`. + """ + await self._client.set_chat_default_restrictions( + self, restrictions, until=until + ) diff --git a/client/src/telethon/_impl/client/types/chat_restriction.py b/client/src/telethon/_impl/client/types/chat_restriction.py new file mode 100644 index 00000000..31cb83e2 --- /dev/null +++ b/client/src/telethon/_impl/client/types/chat_restriction.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from enum import Enum +from typing import Set + +from ...tl import abcs, types + + +class ChatRestriction(Enum): + """ + A restriction that may be applied to a banned chat's participant. + + A banned participant is completley banned from a chat if they are forbidden to :attr:`VIEW_MESSAGES`. + + A banned participant that can :attr:`VIEW_MESSAGES` is restricted, but can still be part of the chat, + + .. 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. + """ + + VIEW_MESSAGES = "view_messages" + """ + Prevents being in the chat and fetching the message history. + + Applying this restriction will kick the participant out of the group. + """ + + SEND_MESSAGES = "send_messages" + """Prevents sending messages to the chat.""" + + SEND_MEDIA = "send_media" + """Prevents sending messages with media such as photos or documents to the chat.""" + + SEND_STICKERS = "send_stickers" + """Prevents sending sticker media to the chat.""" + + SEND_GIFS = "send_gifs" + """Prevents sending muted looping video media ("GIFs") to the chat.""" + + SEND_GAMES = "send_games" + """Prevents sending *@bot inline* games to the chat.""" + + SEND_INLINE = "send_inline" + """Prevents sending messages via *@bot inline* to the chat.""" + + EMBED_LINKS = "embed_links" + """Prevents sending messages that include links to external URLs to the chat.""" + + SEND_POLLS = "send_polls" + """Prevents sending poll media to the chat.""" + + CHANGE_INFO = "change_info" + """Prevents changing the description of the chat.""" + + INVITE_USERS = "invite_users" + """Prevents inviting users to the chat.""" + + PIN_MESSAGES = "pin_messages" + """Prevents pinning messages to the chat.""" + + MANAGE_TOPICS = "manage_topics" + """Prevents managing the topics of the chat.""" + + SEND_PHOTOS = "send_photos" + """Prevents sending photo media files to the chat.""" + + SEND_VIDEOS = "send_videos" + """Prevents sending video media files to the chat.""" + + SEND_ROUND_VIDEOS = "send_roundvideos" + """Prevents sending round video media files to the chat.""" + + SEND_AUDIOS = "send_audios" + """Prevents sending audio media files to the chat.""" + + SEND_VOICE_NOTES = "send_voices" + """Prevents sending voice note audio media files to the chat.""" + + SEND_DOCUMENTS = "send_docs" + """Prevents sending document media files to the chat.""" + + SEND_PLAIN_MESSAGES = "send_plain" + """Prevents sending plain text messages with no media to the chat.""" + + @classmethod + def _from_raw(cls, rights: abcs.ChatBannedRights) -> Set[ChatRestriction]: + assert isinstance(rights, types.ChatBannedRights) + restrictions = ( + cls.VIEW_MESSAGES if rights.view_messages else None, + cls.SEND_MESSAGES if rights.send_messages else None, + cls.SEND_MEDIA if rights.send_media else None, + cls.SEND_STICKERS if rights.send_stickers else None, + cls.SEND_GIFS if rights.send_gifs else None, + cls.SEND_GAMES if rights.send_games else None, + cls.SEND_INLINE if rights.send_inline else None, + cls.EMBED_LINKS if rights.embed_links else None, + cls.SEND_POLLS if rights.send_polls else None, + cls.CHANGE_INFO if rights.change_info else None, + cls.INVITE_USERS if rights.invite_users else None, + cls.PIN_MESSAGES if rights.pin_messages else None, + cls.MANAGE_TOPICS if rights.manage_topics else None, + cls.SEND_PHOTOS if rights.send_photos else None, + cls.SEND_VIDEOS if rights.send_videos else None, + cls.SEND_ROUND_VIDEOS if rights.send_roundvideos else None, + cls.SEND_AUDIOS if rights.send_audios else None, + cls.SEND_VOICE_NOTES if rights.send_voices else None, + cls.SEND_DOCUMENTS if rights.send_docs else None, + cls.SEND_PLAIN_MESSAGES if rights.send_plain else None, + ) + return set(filter(None, iter(restrictions))) + + @classmethod + def _set_to_raw( + cls, restrictions: Set[ChatRestriction], until_date: int + ) -> types.ChatBannedRights: + return types.ChatBannedRights( + view_messages=cls.VIEW_MESSAGES in restrictions, + send_messages=cls.SEND_MESSAGES in restrictions, + send_media=cls.SEND_MEDIA in restrictions, + send_stickers=cls.SEND_STICKERS in restrictions, + send_gifs=cls.SEND_GIFS in restrictions, + send_games=cls.SEND_GAMES in restrictions, + send_inline=cls.SEND_INLINE in restrictions, + embed_links=cls.EMBED_LINKS in restrictions, + send_polls=cls.SEND_POLLS in restrictions, + change_info=cls.CHANGE_INFO in restrictions, + invite_users=cls.INVITE_USERS in restrictions, + pin_messages=cls.PIN_MESSAGES in restrictions, + manage_topics=cls.MANAGE_TOPICS in restrictions, + send_photos=cls.SEND_PHOTOS in restrictions, + send_videos=cls.SEND_VIDEOS in restrictions, + send_roundvideos=cls.SEND_ROUND_VIDEOS in restrictions, + send_audios=cls.SEND_AUDIOS in restrictions, + send_voices=cls.SEND_VOICE_NOTES in restrictions, + send_docs=cls.SEND_DOCUMENTS in restrictions, + send_plain=cls.SEND_PLAIN_MESSAGES in restrictions, + until_date=until_date, + ) diff --git a/client/src/telethon/_impl/client/types/draft.py b/client/src/telethon/_impl/client/types/draft.py index c517968a..d985f654 100644 --- a/client/src/telethon/_impl/client/types/draft.py +++ b/client/src/telethon/_impl/client/types/draft.py @@ -61,7 +61,7 @@ class Draft(metaclass=NoPublicConstructor): This is also the chat where the message will be sent to by :meth:`send`. """ return self._chat_map.get(peer_id(self._peer)) or expand_peer( - self._peer, broadcast=None + self._client, self._peer, broadcast=None ) @property diff --git a/client/src/telethon/_impl/client/types/message.py b/client/src/telethon/_impl/client/types/message.py index ca979ef0..7c700c45 100644 --- a/client/src/telethon/_impl/client/types/message.py +++ b/client/src/telethon/_impl/client/types/message.py @@ -179,7 +179,7 @@ class Message(metaclass=NoPublicConstructor): pid = peer_id(peer) if pid not in self._chat_map: self._chat_map[pid] = expand_peer( - peer, broadcast=getattr(self._raw, "post", None) + self._client, peer, broadcast=getattr(self._raw, "post", None) ) return self._chat_map[pid] @@ -194,7 +194,7 @@ class Message(metaclass=NoPublicConstructor): """ if (from_ := getattr(self._raw, "from_id", None)) is not None: return self._chat_map.get(peer_id(from_)) or expand_peer( - from_, broadcast=getattr(self._raw, "post", None) + self._client, from_, broadcast=getattr(self._raw, "post", None) ) else: return None diff --git a/client/src/telethon/_impl/client/types/participant.py b/client/src/telethon/_impl/client/types/participant.py index 8d6360bf..a3e17871 100644 --- a/client/src/telethon/_impl/client/types/participant.py +++ b/client/src/telethon/_impl/client/types/participant.py @@ -1,10 +1,18 @@ -from typing import Dict, Optional, Self, Set, Union +from __future__ import annotations +import datetime +from typing import TYPE_CHECKING, Dict, Optional, Self, Sequence, Set, Union + +from ...session import PackedChat from ...tl import abcs, types from .admin_right import AdminRight from .chat import Chat, User, peer_id +from .chat_restriction import ChatRestriction from .meta import NoPublicConstructor +if TYPE_CHECKING: + from ..client.client import Client + class Participant(metaclass=NoPublicConstructor): """ @@ -13,10 +21,10 @@ class Participant(metaclass=NoPublicConstructor): You can obtain participants with methods such as :meth:`telethon.Client.get_participants`. """ - __slots__ = ("_raw", "_chat_map") - def __init__( self, + client: Client, + chat: PackedChat, participant: Union[ types.ChannelParticipant, types.ChannelParticipantSelf, @@ -30,12 +38,18 @@ class Participant(metaclass=NoPublicConstructor): ], chat_map: Dict[int, Chat], ) -> None: + self._client = client + self._chat = chat self._raw = participant self._chat_map = chat_map @classmethod def _from_raw_channel( - cls, participant: abcs.ChannelParticipant, chat_map: Dict[int, Chat] + cls, + client: Client, + chat: PackedChat, + participant: abcs.ChannelParticipant, + chat_map: Dict[int, Chat], ) -> Self: if isinstance( participant, @@ -48,13 +62,17 @@ class Participant(metaclass=NoPublicConstructor): types.ChannelParticipantLeft, ), ): - return cls._create(participant, chat_map) + return cls._create(client, chat, participant, chat_map) else: raise RuntimeError("unexpected case") @classmethod def _from_raw_chat( - cls, participant: abcs.ChatParticipant, chat_map: Dict[int, Chat] + cls, + client: Client, + chat: PackedChat, + participant: abcs.ChatParticipant, + chat_map: Dict[int, Chat], ) -> Self: if isinstance( participant, @@ -64,7 +82,7 @@ class Participant(metaclass=NoPublicConstructor): types.ChatParticipantAdmin, ), ): - return cls._create(participant, chat_map) + return cls._create(client, chat, participant, chat_map) else: raise RuntimeError("unexpected case") @@ -162,3 +180,36 @@ class Participant(metaclass=NoPublicConstructor): return AdminRight._chat_rights() else: return None + + @property + def restrictions(self) -> Optional[Set[ChatRestriction]]: + """ + The set of restrictions applied to this participant, if they are banned. + """ + if isinstance(self._raw, types.ChannelParticipantBanned): + return ChatRestriction._from_raw(self._raw.banned_rights) + else: + return None + + async def set_admin_rights(self, rights: Sequence[AdminRight]) -> None: + """ + Alias for :meth:`telethon.Client.set_participant_admin_rights`. + """ + participant = self.user or self.banned or self.left + assert participant + await self._client.set_participant_admin_rights(self._chat, participant, rights) + + async def set_restrictions( + self, + restrictions: Sequence[ChatRestriction], + *, + until: Optional[datetime.datetime] = None, + ) -> None: + """ + Alias for :meth:`telethon.Client.set_participant_restrictions`. + """ + participant = self.user or self.banned or self.left + assert participant + await self._client.set_participant_restrictions( + self._chat, participant, restrictions, until=until + ) diff --git a/client/src/telethon/types/__init__.py b/client/src/telethon/types/__init__.py index 8a40c5cf..e6988b10 100644 --- a/client/src/telethon/types/__init__.py +++ b/client/src/telethon/types/__init__.py @@ -7,6 +7,7 @@ from .._impl.client.types import ( CallbackAnswer, Channel, Chat, + ChatRestriction, Dialog, Draft, File, @@ -28,6 +29,7 @@ __all__ = [ "CallbackAnswer", "Channel", "Chat", + "ChatRestriction", "Dialog", "Draft", "File",