diff --git a/client/doc/concepts/glossary.rst b/client/doc/concepts/glossary.rst index 11ca8d74..5ed51f2c 100644 --- a/client/doc/concepts/glossary.rst +++ b/client/doc/concepts/glossary.rst @@ -11,6 +11,9 @@ Glossary .. seealso:: The :doc:`../concepts/chats` concept. + yourself + The logged-in account, whether that represents a bot or a user with a phone number. + Raw API Functions and types under ``telethon._tl`` that enable access to all of Telegram's API. diff --git a/client/src/telethon/_impl/client/client/auth.py b/client/src/telethon/_impl/client/client/auth.py index bd3cdc4e..20a859af 100644 --- a/client/src/telethon/_impl/client/client/auth.py +++ b/client/src/telethon/_impl/client/client/auth.py @@ -128,11 +128,8 @@ async def sign_in( async def interactive_login(self: Client) -> User: - try: - return await self.get_me() - except RpcError as e: - if e.code != 401: - raise + if me := await self.get_me(): + return me phone_or_token = "" while not re.match(r"\+?[\s()]*\d", phone_or_token): diff --git a/client/src/telethon/_impl/client/client/bots.py b/client/src/telethon/_impl/client/client/bots.py index 92460a77..42271483 100644 --- a/client/src/telethon/_impl/client/client/bots.py +++ b/client/src/telethon/_impl/client/client/bots.py @@ -11,6 +11,10 @@ if TYPE_CHECKING: class InlineResults(metaclass=NoPublicConstructor): + """ + :final: + """ + def __init__( self, client: Client, @@ -119,7 +123,7 @@ class InlineResult(metaclass=NoPublicConstructor): async def inline_query( - self: Client, bot: ChatLike, query: str, *, chat: Optional[ChatLike] = None + self: Client, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None ) -> AsyncIterator[InlineResult]: packed_bot = await self._resolve_to_packed(bot) packed_chat = await self._resolve_to_packed(chat) if chat else None diff --git a/client/src/telethon/_impl/client/client/client.py b/client/src/telethon/_impl/client/client/client.py index e707116b..dbd0f0b4 100644 --- a/client/src/telethon/_impl/client/client/client.py +++ b/client/src/telethon/_impl/client/client/client.py @@ -107,6 +107,59 @@ T = TypeVar("T") class Client: + """ + A client capable of connecting to Telegram and sending requests. + + This is the "entry point" of the library. + + This class can be used as an asynchronous context manager to automatically :meth:`connect` and :meth:`disconnect`: + + .. code-block:: python + + async with Client(session, api_id, api_hash) as client: + ... # automatically connect()-ed + + ... # after exiting the block, disconnect() was automatically called + + :param session: + A name or path to a ``.session`` file, or a different storage. + + :param api_id: + The API ID. See :doc:`/basic/signing-in` to learn how to obtain it. + + :param api_hash: + The API hash. See :doc:`/basic/signing-in` to learn how to obtain it. + + :param device_model: + Device model. + + :param system_version: + System version. + + :param app_version: + Application version. + + :param system_lang_code: + ISO 639-1 language code of the system's language. + + :param lang_code: + ISO 639-1 language code of the application's language. + + :param catch_up: + Whether to "catch up" on updates that occured while the client was not connected. + + :param server_addr: + Override the server address ``'ip:port'`` pair to connect to. + Useful to connect to one of Telegram's test servers. + + :param flood_sleep_threshold: + Maximum amount of time, in seconds, to automatically sleep before retrying a request. + This sleeping occurs when ``FLOOD_WAIT`` :class:`~telethon.RpcError` is raised by Telegram. + + :param update_queue_limit: + Maximum amount of updates to keep in memory before dropping them. + """ + def __init__( self, session: Optional[Union[str, Path, Storage]], @@ -152,17 +205,114 @@ class Client: event_cls: Type[Event], filter: Optional[Filter] = None, ) -> None: + """ + Register a callable to be invoked when the provided event type occurs. + + :param handler: + The callable to invoke when an event occurs. + This is often just a function object. + + :param event_cls: + The event type to bind to the handler. + When Telegram sends an update corresponding to this type, + *handler* is called with an instance of this event type as the only argument. + + :param filter: + Filter function to call with the event before calling *handler*. + If it returns `False`, *handler* will not be called. + See the :mod:`~telethon.events.filters` module to learn more. + + .. rubric:: Example + + .. code-block:: python + + async def my_print_handler(event): + print(event.chat.full_name, event.text) + + # Register a handler to be called on new messages + client.add_event_handler(my_print_handler, events.NewMessage) + + # Register a handler to be called on new messages if they contain "hello" or "/start" + from telethon.events import filters + + client.add_event_handler( + my_print_handler, + events.NewMessage, + filters.Any(filters.Text(r'hello'), filters.Command('/start')), + ) + + .. seealso:: + + :meth:`on`, used to register handlers with the decorator syntax. + """ add_event_handler(self, handler, event_cls, filter) async def bot_sign_in(self, token: str) -> User: + """ + Sign in to a bot account. + + :param token: + The bot token obtained from `@BotFather `_. + It's a string composed of digits, a colon, and characters from the base-64 alphabet. + + :return: The bot user corresponding to :term:`yourself`. + + .. rubric:: Example + + .. code-block:: python + + await client.bot_sign_in('12345:abc67DEF89ghi') + + .. seealso:: + + :meth:`request_login_code`, used to sign in as a user instead. + """ return await bot_sign_in(self, token) async def check_password( self, token: PasswordToken, password: Union[str, bytes] ) -> User: + """ + Check the two-factor-authentication (2FA) password. + If it is correct, completes the login. + + :param token: + The return value from :meth:`sign_in`. + + :param password: + The 2FA password. + + :return: The user corresponding to :term:`yourself`. + + .. rubric:: Example + + .. code-block:: python + + from telethon.types import PasswordToken + + login_token = await client.request_login_code('+1 23 456') + password_token = await client.sign_in(login_token, input('code: ')) + assert isinstance(password_token, PasswordToken) + + user = await client.check_password(password_token, '1-L0V3+T3l3th0n') + + .. seealso:: + + :meth:`request_login_code` and :meth:`sign_in` + """ return await check_password(self, token, password) async def connect(self) -> None: + """ + Connect to the Telegram servers. + + .. rubric:: Example + + .. code-block:: python + + await client.connect() + # success! + """ await connect(self) async def delete_dialog(self) -> None: @@ -171,17 +321,84 @@ class Client: async def delete_messages( self, chat: ChatLike, message_ids: List[int], *, revoke: bool = True ) -> int: + """ + Delete messages. + + :param chat: + The :term:`chat` where the messages are. + + .. warning:: + + When deleting messages from private conversations or small groups, + this parameter is ignored. This means the *message_ids* may delete + messages in different chats. + + :param message_ids: + The list of message identifiers to delete. + + :param revoke: + When set to :data:`True`, the message will be deleted for everyone that is part of *chat*. + Otherwise, the message will only be deleted for :term:`yourself`. + + :return: The amount of messages that were deleted. + + .. rubric:: Example + + .. code-block:: python + + # Delete two messages from chat for yourself + await client.delete_messages( + chat, + [187481, 187482], + revoke=False, + ) + + .. seealso:: + + :meth:`telethon.types.Message.delete` + """ return await delete_messages(self, chat, message_ids, revoke=revoke) async def disconnect(self) -> None: + """ + Disconnect from the Telegram servers. + + This call will only fail if saving the :term:`session` fails. + + .. rubric:: Example + + .. code-block:: python + + await client.disconnect() + # success! + """ await disconnect(self) async def download(self, media: MediaLike, file: OutFileLike) -> None: """ Download a file. - This is simply a more convenient method to `iter_download`, - as it will handle dealing with the file chunks and writes by itself. + :param media: + The media to download. + This will often come from :attr:`telethon.types.Message.file`. + + :param file: + The output file path or :term:`file-like object`. + + .. rubric:: Example + + .. code-block:: python + + if photo := message.photo: + await client.download(photo, 'picture.jpg') + + if video := message.video: + with open('video.mp4, 'wb') as file: + await client.download(video, file) + + .. seealso:: + + :meth:`iter_download`, for fine-grained control over the download. """ await download(self, media, file) @@ -195,6 +412,33 @@ class Client: html: Optional[str] = None, link_preview: Optional[bool] = None, ) -> Message: + """ + Edit a message. + + :param chat: + The :term:`chat` where the message to edit is. + + :param message_id: + The identifier of the message to edit. + + The rest of parameters behave the same as they do in `send_message` or `send_file`. + + :return: The edited message. + + .. rubric:: Example + + .. code-block:: python + + # Edit message to have text without formatting + await client.edit_message(chat, msg_id, text='New text') + + # Remove the link preview without changing the text + await client.edit_message(chat, msg_id, link_preview=False) + + .. seealso:: + + :meth:`telethon.types.Message.edit` + """ return await edit_message( self, chat, @@ -208,9 +452,50 @@ class Client: async def forward_messages( self, target: ChatLike, message_ids: List[int], source: ChatLike ) -> List[Message]: + """ + Forward messages from one :term:`chat` to another. + + :param target: + The :term:`chat` where the messages will be forwarded to. + + :param message_ids: + The list of message identifiers to forward. + + :param source: + The source :term:`chat` where the messages to forward exist. + + :return: The forwarded messages. + + .. rubric:: Example + + .. code-block:: python + + # Forward two messages from chat to the destination + await client.forward_messages( + destination, + [187481, 187482], + chat, + ) + + .. seealso:: + + :meth:`telethon.types.Message.forward_to` + """ return await forward_messages(self, target, message_ids, source) async def get_contacts(self) -> AsyncList[User]: + """ + Get the users in your contact list. + + :return: Your contacts. + + .. rubric:: Example + + .. code-block:: python + + async for user in client.get_contacts(): + print(user.full_name, user.id) + """ return await get_contacts(self) def get_dialogs(self) -> None: @@ -219,9 +504,51 @@ class Client: def get_handler_filter( self, handler: Callable[[Event], Awaitable[Any]] ) -> Optional[Filter]: + """ + Get the filter associated to the given event handler. + + :param handler: + The callable that was previously added as an event handler. + + :return: + The filter, if *handler* was actually registered and had a filter. + + .. rubric:: Example + + .. code-block:: python + + from telethon.events import filters + + # Get the current filter... + filt = client.get_handler_filter(my_handler) + + # ...and "append" a new filter that also must match. + client.set_handler_filter(my_handler, filters.All(filt, filt.Text(r'test'))) + """ return get_handler_filter(self, handler) - async def get_me(self) -> User: + async def get_me(self) -> Optional[User]: + """ + Get information about :term:`yourself`. + + :return: + The user associated with the logged-in account, or :data:`None` if the client is not authorized. + + .. rubric:: Example + + .. code-block:: python + + me = await client.get_me() + assert me is not None, "not logged in!" + + if me.bot: + print('I am a bot') + + print('My name is', me.full_name) + + if me.phone: + print('My phone number is', me.phone) + """ return await get_me(self) def get_messages( @@ -232,6 +559,40 @@ class Client: offset_id: Optional[int] = None, offset_date: Optional[datetime.datetime] = None, ) -> AsyncList[Message]: + """ + Get the message history from a :term:`chat`. + + Edit a message. + + :param chat: + The :term:`chat` where the message to edit is. + + :param limit: + How many messages to fetch at most. + + :param offset_id: + Start getting messages with an identifier lower than this one. + This means only messages older than the message with ``id = offset_id`` will be fetched. + + :param offset_date: + Start getting messages with a date lower than this one. + This means only messages sent before *offset_date* will be fetched. + + :return: The message history. + + .. rubric:: Example + + .. code-block:: python + + # Get the last message in a chat + last_message = (await client.get_messages(chat, 1))[0] + + # Print all messages before 2023 as HTML + from datetime import datetime + + async for message in client.get_messages(chat, offset_date=datetime(2023, 1, 1)): + print(message.sender.full_name, ':', message.html_text) + """ return get_messages( self, chat, limit, offset_id=offset_id, offset_date=offset_date ) @@ -245,14 +606,70 @@ class Client: get_participants(self) async def inline_query( - self, bot: ChatLike, query: str, *, chat: Optional[ChatLike] = None + self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None ) -> AsyncIterator[InlineResult]: + """ + Perform a *@bot inline query*. + + It's known as inline because clients with a GUI display the results *inline*, + after typing on the message input textbox, without sending any message. + + :param bot: + The bot to sent the query string to. + + :param query: + The query string to send to the bot. + + :param chat: + Where the query is being made and will be sent. + Some bots display different results based on the type of chat. + + :return: The query results returned by the bot. + + .. rubric:: Example + + .. code-block:: python + + i = 0 + + # This is equivalent to typing "@bot songs" in an official client + async for result in client.inline_query(bot, 'songs'): + if 'keyword' in result.title: + await result.send(chat) + break + + i += 1 + if i == 10: + break # did not find 'keyword' in the first few results + """ return await inline_query(self, bot, query, chat=chat) async def interactive_login(self) -> User: + """ + Begin an interactive login if needed. + If the account was already logged-in, this method simply returns :term:`yourself`. + + :return: The user corresponding to :term:`yourself`. + + .. rubric:: Example + + .. code-block:: python + + me = await client.interactive_login() + print('Logged in as:', me.full_name) + + .. seealso:: + + In-depth explanation for :doc:`/basic/signing-in`. + """ return await interactive_login(self) async def is_authorized(self) -> bool: + """ + Check whether the client instance is authorized (i.e. logged-in). + + :return: :data:`True` if the client instance has signed-in. + """ return await is_authorized(self) async def iter_download(self) -> None: @@ -263,18 +680,137 @@ class Client: ) -> Callable[ [Callable[[Event], Awaitable[Any]]], Callable[[Event], Awaitable[Any]] ]: + """ + Register the decorated function to be invoked when the provided event type occurs. + + :param event_cls: + The event type to bind to the handler. + When Telegram sends an update corresponding to this type, + the decorated function is called with an instance of this event type as the only argument. + + :param filter: + Filter function to call with the event before calling *handler*. + If it returns `False`, *handler* will not be called. + See the :mod:`~telethon.events.filters` module to learn more. + + :return: The decorator. + + .. rubric:: Example + + .. code-block:: python + + # Register a handler to be called on new messages + @client.on(events.NewMessage) + async def my_print_handler(event): + print(event.chat.full_name, event.text) + + # Register a handler to be called on new messages if they contain "hello" or "/start" + from telethon.events.filters import Any, Text, Command + + @client.on(events.NewMessage, Any(Text(r'hello'), Command('/start'))) + async def my_other_print_handler(event): + print(event.chat.full_name, event.text) + + .. seealso:: + + :meth:`add_event_handler`, used to register existing functions as event handlers. + """ return on(self, event_cls, filter) async def pin_message(self, chat: ChatLike, message_id: int) -> Message: + """ + Pin a message to be at the top. + + :param chat: + The :term:`chat` where the message to pin is. + + :param message_id: + The identifier of the message to pin. + + :return: The service message announcing the pin. + + .. rubric:: Example + + .. code-block:: python + + # Pin a message, then delete the service message + message = await client.pin_message(chat, 187481) + await message.delete() + """ return await pin_message(self, chat, message_id) def remove_event_handler(self, handler: Callable[[Event], Awaitable[Any]]) -> None: + """ + Remove the handler as a function to be called when events occur. + This is simply the opposite of :meth:`add_event_handler`. + Does nothing if the handler was not actually registered. + + :param handler: + The callable to stop invoking when events occur. + + .. rubric:: Example + + .. code-block:: python + + # Register a handler that removes itself when it receives 'stop' + @client.on(events.NewMessage) + async def my_handler(event): + if 'stop' in event.text: + client.remove_event_handler(my_handler) + else: + print('still going!') + + .. seealso:: + + :meth:`add_event_handler`, used to register existing functions as event handlers. + """ remove_event_handler(self, handler) async def request_login_code(self, phone: str) -> LoginToken: + """ + Request Telegram to send a login code to the provided phone number. + This is simply the opposite of :meth:`add_event_handler`. + Does nothing if the handler was not actually registered. + + :param phone: + The phone number string, in international format. + The plus-sign ``+`` can be kept in the string. + + :return: Information about the sent code. + + .. rubric:: Example + + .. code-block:: python + + login_token = await client.request_login_code('+1 23 456...') + print(login_token.timeout, 'seconds before code expires') + + .. seealso:: + + :meth:`sign_in`, to complete the login procedure. + """ return await request_login_code(self, phone) async def resolve_to_packed(self, chat: ChatLike) -> PackedChat: + """ + Resolve a :term:`chat` and return a compact, reusable reference to it. + + :param chat: + The :term:`chat` to resolve. + + :return: An efficient, reusable version of the input. + + .. rubric:: Example + + .. code-block:: python + + friend = await client.resolve_to_packed('@cat') + # Now you can use `friend` to get or send messages, files... + + .. seealso:: + + In-depth explanation for :doc:`/concepts/chats`. + """ return await resolve_to_packed(self, chat) async def resolve_username(self) -> Chat: @@ -291,6 +827,30 @@ class Client: offset_id: Optional[int] = None, offset_date: Optional[datetime.datetime] = None, ) -> AsyncList[Message]: + """ + Perform a global message search. + This is used to search messages in no particular chat (i.e. everywhere possible). + + :param chat: + The :term:`chat` where the message to edit is. + + :param limit: + How many messages to fetch at most. + + :param query: + Text query to use for fuzzy matching messages. + The rules for how "fuzzy" works are an implementation detail of the server. + + :param offset_id: + Start getting messages with an identifier lower than this one. + This means only messages older than the message with ``id = offset_id`` will be fetched. + + :param offset_date: + Start getting messages with a date lower than this one. + This means only messages sent before *offset_date* will be fetched. + + :return: The found messages. + """ return search_all_messages( self, limit, query=query, offset_id=offset_id, offset_date=offset_date ) @@ -304,6 +864,29 @@ class Client: offset_id: Optional[int] = None, offset_date: Optional[datetime.datetime] = None, ) -> AsyncList[Message]: + """ + Search messages in a chat. + + :param chat: + The :term:`chat` where messages will be searched. + + :param limit: + How many messages to fetch at most. + + :param query: + Text query to use for fuzzy matching messages. + The rules for how "fuzzy" works are an implementation detail of the server. + + :param offset_id: + Start getting messages with an identifier lower than this one. + This means only messages older than the message with ``id = offset_id`` will be fetched. + + :param offset_date: + Start getting messages with a date lower than this one. + This means only messages sent before *offset_date* will be fetched. + + :return: The found messages. + """ return search_messages( self, chat, limit, query=query, offset_id=offset_id, offset_date=offset_date ) @@ -325,8 +908,16 @@ class Client: """ Send an audio file. - Unlike `send_file`, this method will attempt to guess the values for + Unlike :meth:`send_file`, this method will attempt to guess the values for duration, title and performer if they are not provided. + + :param chat: + The :term:`chat` where the message will be sent to. + + :param path: + A local file path or :class:`~telethon.types.File` to send. + + The rest of parameters behave the same as they do in :meth:`send_file`. """ return await send_audio( self, @@ -372,15 +963,32 @@ class Client: """ Send any type of file with any amount of attributes. - This method will not attempt to guess any of the file metadata such as - width, duration, title, etc. If you want to let the library attempt to - guess the file metadata, use the type-specific methods to send media: + This method will *not* attempt to guess any of the file metadata such as width, duration, title, etc. + If you want to let the library attempt to guess the file metadata, use the type-specific methods to send media: `send_photo`, `send_audio` or `send_file`. Unlike `send_photo`, image files will be sent as documents by default. - The parameters are used to construct a `File`. See the documentation - for `File.new` to learn what they do and when they are in effect. + :param chat: + The :term:`chat` where the message will be sent to. + + :param path: + A local file path or :class:`~telethon.types.File` to send. + + :param caption: + Caption text to display under the media, with no formatting. + + :param caption_markdown: + Caption text to display under the media, parsed as markdown. + + :param caption_html: + Caption text to display under the media, parsed as HTML. + + The rest of parameters are passed to :meth:`telethon.types.File.new` + if *path* isn't a :class:`~telethon.types.File`. + See the documentation of :meth:`~telethon.types.File.new` to learn what they do. + + Note that only one *caption* parameter can be provided. """ return await send_file( self, @@ -418,6 +1026,23 @@ class Client: html: Optional[str] = None, link_preview: Optional[bool] = None, ) -> Message: + """ + Send a message. + + :param chat: + The :term:`chat` where the message will be sent to. + + :param text: + Message text, with no formatting. + + :param text_markdown: + Message text, parsed as markdown. + + :param text_html: + Message text, parsed as HTML. + + Note that exactly one *text* parameter must be provided. + """ return await send_message( self, chat, @@ -443,16 +1068,20 @@ class Client: """ Send a photo file. - Exactly one of path, url or file must be specified. - A `File` can also be used as the second parameter. - By default, the server will be allowed to `compress` the image. Only compressed images can be displayed as photos in applications. - Images that cannot be compressed will be sent as file documents, - with a thumbnail if possible. + If *compress* is set to :data:`False`, the image will be sent as a file document. Unlike `send_file`, this method will attempt to guess the values for - width and height if they are not provided and the can't be compressed. + width and height if they are not provided. + + :param chat: + The :term:`chat` where the message will be sent to. + + :param path: + A local file path or :class:`~telethon.types.File` to send. + + The rest of parameters behave the same as they do in :meth:`send_file`. """ return await send_photo( self, @@ -487,6 +1116,14 @@ class Client: Unlike `send_file`, this method will attempt to guess the values for duration, width and height if they are not provided. + + :param chat: + The :term:`chat` where the message will be sent to. + + :param path: + A local file path or :class:`~telethon.types.File` to send. + + The rest of parameters behave the same as they do in :meth:`send_file`. """ return await send_video( self, @@ -508,17 +1145,89 @@ class Client: handler: Callable[[Event], Awaitable[Any]], filter: Optional[Filter] = None, ) -> None: + """ + Set the filter to use for the given event handler. + + :param handler: + The callable that was previously added as an event handler. + + :param filter: + The filter to use for *handler*, or :data:`None` to remove the old filter. + + .. rubric:: Example + + .. code-block:: python + + from telethon.events import filters + + # Change the filter to handle '/stop' + client.set_handler_filter(my_handler, filters.Command('/stop')) + + # Remove the filter + client.set_handler_filter(my_handler, None) + """ set_handler_filter(self, handler, filter) async def sign_in(self, token: LoginToken, code: str) -> Union[User, PasswordToken]: + """ + Sign in to a user account. + + :param token: + The login token returned from :meth:`request_login_code`. + + :return: + The user corresponding to :term:`yourself`, or a password token if the account has 2FA enabled. + + .. rubric:: Example + + .. code-block:: python + + from telethon.types import PasswordToken + + login_token = await client.request_login_code('+1 23 456') + user_or_token = await client.sign_in(login_token, input('code: ')) + + if isinstance(password_token, PasswordToken): + user = await client.check_password(password_token, '1-L0V3+T3l3th0n') + + .. seealso:: + + :meth:`check_password`, the next step if the account has 2FA enabled. + """ return await sign_in(self, token, code) async def sign_out(self) -> None: + """ + Sign out, revoking the authorization of the current :term:`session`. + + .. rubric:: Example + + .. code-block:: python + + await client.sign_out() # turn off the lights + await client.disconnect() # shut the door + """ await sign_out(self) async def unpin_message( self, chat: ChatLike, message_id: Union[int, Literal["all"]] ) -> None: + """ + Unpin one or all messages from the top. + + :param chat: + The :term:`chat` where the message pinned message is. + + :param message_id: + The identifier of the message to unpin, or ``'all'`` to unpin them all. + + .. rubric:: Example + + .. code-block:: python + + # Unpin all messages + await client.unpin_message(chat, 'all') + """ await unpin_message(self, chat, message_id) # --- diff --git a/client/src/telethon/_impl/client/client/net.py b/client/src/telethon/_impl/client/client/net.py index 9ee6fe04..c1516a4a 100644 --- a/client/src/telethon/_impl/client/client/net.py +++ b/client/src/telethon/_impl/client/client/net.py @@ -39,6 +39,12 @@ def default_system_version() -> str: @dataclass class Config: + """ + Configuration used by the :class:`telethon.Client`. + + See the parameters of :class:`~telethon.Client` for an explanation of the fields. + """ + session: Session api_id: int api_hash: str diff --git a/client/src/telethon/_impl/client/client/users.py b/client/src/telethon/_impl/client/client/users.py index 0cb4288b..0d74b7df 100644 --- a/client/src/telethon/_impl/client/client/users.py +++ b/client/src/telethon/_impl/client/client/users.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from .client import Client -async def get_me(self: Client) -> User: +async def get_me(self: Client) -> Optional[User]: self raise NotImplementedError diff --git a/client/src/telethon/_impl/client/events/event.py b/client/src/telethon/_impl/client/events/event.py index ed62e333..8ad81801 100644 --- a/client/src/telethon/_impl/client/events/event.py +++ b/client/src/telethon/_impl/client/events/event.py @@ -11,6 +11,10 @@ if TYPE_CHECKING: class Event(metaclass=NoPublicConstructor): + """ + The base type of all events. + """ + @classmethod @abc.abstractmethod def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]: diff --git a/client/src/telethon/_impl/client/events/filters/combinators.py b/client/src/telethon/_impl/client/events/filters/combinators.py index ec5f1ede..54505cc4 100644 --- a/client/src/telethon/_impl/client/events/filters/combinators.py +++ b/client/src/telethon/_impl/client/events/filters/combinators.py @@ -6,7 +6,7 @@ from .common import Filter class Any: """ - Combine multiple filters, returning `True` if any of the filters pass. + Combine multiple filters, returning :data:`True` if any of the filters pass. """ __slots__ = ("_filters",) @@ -27,7 +27,7 @@ class Any: class All: """ - Combine multiple filters, returning `True` if all of the filters pass. + Combine multiple filters, returning :data:`True` if all of the filters pass. """ __slots__ = ("_filters",) @@ -48,8 +48,7 @@ class All: class Not: """ - Negate the output of a single filter, returning `True` if the nested - filter does *not* pass. + Negate the output of a single filter, returning :data:`True` if the nested filter does *not* pass. """ __slots__ = ("_filter",) diff --git a/client/src/telethon/_impl/client/events/filters/common.py b/client/src/telethon/_impl/client/events/filters/common.py index 0b26165b..8376511b 100644 --- a/client/src/telethon/_impl/client/events/filters/common.py +++ b/client/src/telethon/_impl/client/events/filters/common.py @@ -7,7 +7,7 @@ Filter = Callable[[Event], bool] class Chats: """ - Filter by `event.chat.id`. + Filter by ``event.chat.id``, if the event has a chat. """ __slots__ = ("_chats",) @@ -31,7 +31,7 @@ class Chats: class Senders: """ - Filter by `event.sender.id`. + Filter by ``event.sender.id``, if the event has a sender. """ __slots__ = ("_senders",) diff --git a/client/src/telethon/_impl/client/events/filters/messages.py b/client/src/telethon/_impl/client/events/filters/messages.py index e63cd879..dbf97df4 100644 --- a/client/src/telethon/_impl/client/events/filters/messages.py +++ b/client/src/telethon/_impl/client/events/filters/messages.py @@ -6,10 +6,10 @@ from ..event import Event class Text: """ - Filter by `event.text` using a *regular expression* pattern. + Filter by ``event.text`` using a *regular expression* pattern. The pattern is searched on the text anywhere, not matched at the start. - Use the `'^'` anchor if you want to match the text from the start. + Use the ``'^'`` anchor if you want to match the text from the start. The match, if any, is discarded. If you need to access captured groups, you need to manually perform the check inside the handler instead. @@ -27,12 +27,12 @@ class Text: class Command: """ - Filter by `event.text` to make sure the first word matches the command or + Filter by ``event.text`` to make sure the first word matches the command or the command + '@' + username, using the username of the logged-in account. For example, if the logged-in account has an username of "bot", then the - filter `Command('/help')` will match both "/help" and "/help@bot", but not - "/list" or "/help@other". + filter ``Command('/help')`` will match both ``"/help"`` and ``"/help@bot"``, but not + ``"/list"`` or ``"/help@other"``. Note that the leading forward-slash is not automatically added, which allows for using a different prefix or no prefix at all. @@ -49,11 +49,11 @@ class Command: class Incoming: """ - Filter by `event.incoming`, that is, messages sent from others to the + Filter by ``event.incoming``, that is, messages sent from others to the logged-in account. This is not a reliable way to check that the update was not produced by - the logged-in account. + the logged-in account in broadcast channels. """ __slots__ = () @@ -64,11 +64,11 @@ class Incoming: class Outgoing: """ - Filter by `event.outgoing`, that is, messages sent from others to the + Filter by ``event.outgoing``, that is, messages sent from others to the logged-in account. This is not a reliable way to check that the update was not produced by - the logged-in account. + the logged-in account in broadcast channels. """ __slots__ = () @@ -79,7 +79,7 @@ class Outgoing: class Forward: """ - Filter by `event.forward`. + Filter by ``event.forward``. """ __slots__ = () @@ -90,7 +90,7 @@ class Forward: class Reply: """ - Filter by `event.reply`. + Filter by ``event.reply``. """ __slots__ = () diff --git a/client/src/telethon/_impl/client/events/messages.py b/client/src/telethon/_impl/client/events/messages.py index 4bdb8a63..d58b4153 100644 --- a/client/src/telethon/_impl/client/events/messages.py +++ b/client/src/telethon/_impl/client/events/messages.py @@ -15,6 +15,16 @@ if TYPE_CHECKING: class NewMessage(Event, Message): + """ + Occurs when a new message is sent or received. + + .. warning:: + + Messages sent with the :class:`~telethon.Client` are also caught, + so be careful not to enter infinite loops! + This is true for all event types, including edits. + """ + @classmethod def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]: if isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)): @@ -29,18 +39,30 @@ class NewMessage(Event, Message): class MessageEdited(Event): + """ + Occurs when a new message is sent or received. + """ + @classmethod def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]: raise NotImplementedError() class MessageDeleted(Event): + """ + Occurs when one or more messages are deleted. + """ + @classmethod def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]: raise NotImplementedError() class MessageRead(Event): + """ + Occurs both when your messages are read by others, and when you read messages. + """ + @classmethod def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]: raise NotImplementedError() diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index 0ca523a0..b7455b87 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -10,12 +10,24 @@ if TYPE_CHECKING: class CallbackQuery(Event): + """ + Occurs when an inline button was pressed. + + Only bot accounts can receive this event. + """ + @classmethod def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]: raise NotImplementedError() class InlineQuery(Event): + """ + Occurs when users type ``@bot query`` in their chat box. + + Only bot accounts can receive this event. + """ + @classmethod def _try_from_update(cls, client: Client, update: abcs.Update) -> Optional[Self]: raise NotImplementedError() diff --git a/client/src/telethon/_impl/client/types/async_list.py b/client/src/telethon/_impl/client/types/async_list.py index fdab0af3..337c9e50 100644 --- a/client/src/telethon/_impl/client/types/async_list.py +++ b/client/src/telethon/_impl/client/types/async_list.py @@ -21,6 +21,7 @@ class AsyncList(abc.ABC, Generic[T]): The `len()` of the asynchronous list will be the "total count" reported by the server. It does not necessarily reflect how many items will actually be returned. This count can change as more items are fetched. + Note that this method cannot be awaited. """ def __init__(self) -> None: diff --git a/client/src/telethon/_impl/client/types/chat/channel.py b/client/src/telethon/_impl/client/types/chat/channel.py index 56bd00fe..7f106589 100644 --- a/client/src/telethon/_impl/client/types/chat/channel.py +++ b/client/src/telethon/_impl/client/types/chat/channel.py @@ -6,6 +6,10 @@ from ..meta import NoPublicConstructor class Channel(metaclass=NoPublicConstructor): + """ + A broadcast channel. + """ + __slots__ = ("_raw",) def __init__( @@ -45,6 +49,10 @@ class Channel(metaclass=NoPublicConstructor): def title(self) -> str: return getattr(self._raw, "title", None) or "" + @property + def full_name(self) -> str: + return self.title + @property def username(self) -> Optional[str]: return getattr(self._raw, "username", None) diff --git a/client/src/telethon/_impl/client/types/chat/group.py b/client/src/telethon/_impl/client/types/chat/group.py index f1c2a818..d1dd968e 100644 --- a/client/src/telethon/_impl/client/types/chat/group.py +++ b/client/src/telethon/_impl/client/types/chat/group.py @@ -6,6 +6,10 @@ from ..meta import NoPublicConstructor class Group(metaclass=NoPublicConstructor): + """ + A small group or supergroup. + """ + __slots__ = ("_raw",) def __init__( @@ -49,6 +53,10 @@ class Group(metaclass=NoPublicConstructor): def title(self) -> str: return getattr(self._raw, "title", None) or "" + @property + def full_name(self) -> str: + return self.title + @property def username(self) -> Optional[str]: return getattr(self._raw, "username", None) diff --git a/client/src/telethon/_impl/client/types/chat/user.py b/client/src/telethon/_impl/client/types/chat/user.py index 5ef82a32..9917faba 100644 --- a/client/src/telethon/_impl/client/types/chat/user.py +++ b/client/src/telethon/_impl/client/types/chat/user.py @@ -6,6 +6,10 @@ from ..meta import NoPublicConstructor class RestrictionReason(metaclass=NoPublicConstructor): + """ + A restriction reason for :class:`telethon.types.User`. + """ + __slots__ = ("_raw",) def __init__(self, raw: types.RestrictionReason) -> None: @@ -30,6 +34,10 @@ class RestrictionReason(metaclass=NoPublicConstructor): class User(metaclass=NoPublicConstructor): + """ + A user, representing either a bot account or an account created with a phone number. + """ + __slots__ = ("_raw",) def __init__(self, raw: types.User) -> None: diff --git a/client/src/telethon/_impl/client/types/file.py b/client/src/telethon/_impl/client/types/file.py index 8ec98031..e2f6173b 100644 --- a/client/src/telethon/_impl/client/types/file.py +++ b/client/src/telethon/_impl/client/types/file.py @@ -34,8 +34,8 @@ MediaLike = object class InFileLike(Protocol): """ - [File-like object](https://docs.python.org/3/glossary.html#term-file-like-object) - used for input only, where the `read` method can be `async`. + A :term:`file-like object` used for input only. + The :meth:`read` method can be :keyword:`async`. """ def read(self, n: int) -> Union[bytes, Coroutine[Any, Any, bytes]]: @@ -44,8 +44,8 @@ class InFileLike(Protocol): class OutFileLike(Protocol): """ - [File-like object](https://docs.python.org/3/glossary.html#term-file-like-object) - used for output only, where the `write` method can be `async`. + A :term:`file-like object` used for output only. + The :meth:`write` method can be :keyword:`async`. """ def write(self, data: bytes) -> Union[Any, Coroutine[Any, Any, Any]]: diff --git a/client/src/telethon/_impl/client/types/login_token.py b/client/src/telethon/_impl/client/types/login_token.py index c00bdd2a..3a74d7cc 100644 --- a/client/src/telethon/_impl/client/types/login_token.py +++ b/client/src/telethon/_impl/client/types/login_token.py @@ -1,10 +1,14 @@ -from typing import Self +from typing import Optional, Self from ...tl import types from .meta import NoPublicConstructor class LoginToken(metaclass=NoPublicConstructor): + """ + Result of requesting a login code via :meth:`telethon.Client.request_login_code`. + """ + __slots__ = ("_code", "_phone") def __init__(self, code: types.auth.SentCode, phone: str) -> None: @@ -14,3 +18,7 @@ class LoginToken(metaclass=NoPublicConstructor): @classmethod def _new(cls, code: types.auth.SentCode, phone: str) -> Self: return cls._create(code, phone) + + @property + def timeout(self) -> Optional[int]: + return self._code.timeout diff --git a/client/src/telethon/_impl/client/types/message.py b/client/src/telethon/_impl/client/types/message.py index c70ba05c..ed334c51 100644 --- a/client/src/telethon/_impl/client/types/message.py +++ b/client/src/telethon/_impl/client/types/message.py @@ -8,6 +8,10 @@ from .meta import NoPublicConstructor class Message(metaclass=NoPublicConstructor): + """ + A sent message. + """ + __slots__ = ("_raw",) def __init__(self, message: abcs.Message) -> None: @@ -28,6 +32,14 @@ class Message(metaclass=NoPublicConstructor): def text(self) -> Optional[str]: return getattr(self._raw, "message", None) + @property + def html_text(self) -> Optional[str]: + raise NotImplementedError + + @property + def markdown_text(self) -> Optional[str]: + raise NotImplementedError + @property def date(self) -> Optional[datetime.datetime]: date = getattr(self._raw, "date", None) @@ -80,3 +92,16 @@ class Message(metaclass=NoPublicConstructor): ) else None ) + + @property + def file(self) -> Optional[File]: + return self._file() + + async def delete(self) -> None: + raise NotImplementedError + + async def edit(self) -> None: + raise NotImplementedError + + async def forward_to(self) -> None: + raise NotImplementedError diff --git a/client/src/telethon/_impl/client/types/password_token.py b/client/src/telethon/_impl/client/types/password_token.py index a601ad4d..e769fc17 100644 --- a/client/src/telethon/_impl/client/types/password_token.py +++ b/client/src/telethon/_impl/client/types/password_token.py @@ -5,6 +5,10 @@ from .meta import NoPublicConstructor class PasswordToken(metaclass=NoPublicConstructor): + """ + Result of attempting to :meth:`~telethon.Client.sign_in` to a 2FA-protected account. + """ + __slots__ = ("_password",) def __init__(self, password: types.account.Password) -> None: diff --git a/client/src/telethon/_impl/mtproto/mtp/types.py b/client/src/telethon/_impl/mtproto/mtp/types.py index 60fac4ef..31027df5 100644 --- a/client/src/telethon/_impl/mtproto/mtp/types.py +++ b/client/src/telethon/_impl/mtproto/mtp/types.py @@ -9,6 +9,17 @@ MsgId = NewType("MsgId", int) class RpcError(ValueError): + """ + A Remote Procedure Call Error. + + Only occurs when the answer to a request sent to Telegram is not the expected result. + The library will never construct instances of this error by itself. + + .. seealso:: + + :doc:`/concepts/errors` + """ + def __init__( self, *, diff --git a/client/src/telethon/_impl/session/chat/packed.py b/client/src/telethon/_impl/session/chat/packed.py index 2097ec0e..350bbb2f 100644 --- a/client/src/telethon/_impl/session/chat/packed.py +++ b/client/src/telethon/_impl/session/chat/packed.py @@ -6,6 +6,10 @@ from ...tl import abcs, types class PackedType(Enum): + """ + The type of a :class:`PackedChat`. + """ + # bits: zero, has-access-hash, channel, broadcast, group, chat, user, bot USER = 0b0000_0010 BOT = 0b0000_0011 @@ -16,6 +20,16 @@ class PackedType(Enum): class PackedChat: + """ + A compact representation of a :term:`chat`. + + You can reuse it as many times as you want. + + .. seealso:: + + :doc:`/concepts/chats` + """ + __slots__ = ("ty", "id", "access_hash") def __init__(self, ty: PackedType, id: int, access_hash: Optional[int]) -> None: diff --git a/client/src/telethon/_impl/session/session.py b/client/src/telethon/_impl/session/session.py index 68f77854..3a584389 100644 --- a/client/src/telethon/_impl/session/session.py +++ b/client/src/telethon/_impl/session/session.py @@ -3,6 +3,14 @@ from typing import Any, Dict, List, Optional, Self class DataCenter: + """ + Data-center information. + + :var id: The DC identifier. + :var addr: The server address of the DC, in ``'ip:port'`` format. + :var auth: Authentication key to encrypt communication with. + """ + __slots__ = ("id", "addr", "auth") def __init__(self, *, id: int, addr: str, auth: Optional[bytes]) -> None: @@ -12,6 +20,14 @@ class DataCenter: class User: + """ + Information about the logged-in user. + + :var id: User identifier. + :var dc: Data-center identifier of the user's "home" DC. + :var bot: :data:`True` if the user is from a bot account. + """ + __slots__ = ("id", "dc", "bot") def __init__(self, *, id: int, dc: int, bot: bool) -> None: @@ -21,6 +37,13 @@ class User: class ChannelState: + """ + Update state for a channel. + + :var id: The channel identifier. + :var pts: The channel's partial sequence number. + """ + __slots__ = ("id", "pts") def __init__(self, *, id: int, pts: int) -> None: @@ -29,6 +52,16 @@ class ChannelState: class UpdateState: + """ + Update state for an account. + + :var pts: The primary partial sequence number. + :var qts: The secondary partial sequence number. + :var date: Date of the latest update sequence. + :var seq: The sequence number. + :var channels: Update state for channels. + """ + __slots__ = ( "pts", "qts", @@ -55,9 +88,9 @@ class UpdateState: class Session: """ - A Telethon session. + A Telethon :term:`session`. - A `Session` instance contains the required information to login into your + A session instance contains the required information to login into your Telegram account. **Never** give the saved session file to anyone else or make it public. diff --git a/client/src/telethon/_impl/session/storage/sqlite.py b/client/src/telethon/_impl/session/storage/sqlite.py index 3ffd45bf..322ff9b3 100644 --- a/client/src/telethon/_impl/session/storage/sqlite.py +++ b/client/src/telethon/_impl/session/storage/sqlite.py @@ -15,9 +15,9 @@ class SqliteSession(Storage): SQLite is a reliable way to persist data to disk and offers file locking. - Paths without extension will have '.session' appended to them. + Paths without extension will have ``'.session'`` appended to them. This is by convention, and to make it harder to commit session files to - an VCS by accident (adding `*.session` to `.gitignore` will catch them). + an VCS by accident (adding ``*.session`` to ``.gitignore`` will catch them). """ def __init__(self, file: Union[str, Path]): diff --git a/client/src/telethon/_impl/session/storage/storage.py b/client/src/telethon/_impl/session/storage/storage.py index 7cd3e806..0ab1a3c4 100644 --- a/client/src/telethon/_impl/session/storage/storage.py +++ b/client/src/telethon/_impl/session/storage/storage.py @@ -5,6 +5,10 @@ from ..session import Session class Storage(abc.ABC): + """ + Interface declaring the required methods of a :term:`session` storage. + """ + @abc.abstractmethod async def load(self) -> Optional[Session]: """ @@ -27,4 +31,6 @@ class Storage(abc.ABC): Delete the saved `Session`. This method is called by the library post `log_out`. + + Note that both :meth:`load` and :meth:`save` may still be called after. """ diff --git a/client/src/telethon/events/filters.py b/client/src/telethon/events/filters.py index 1fbfd8d8..0dbbf805 100644 --- a/client/src/telethon/events/filters.py +++ b/client/src/telethon/events/filters.py @@ -1,3 +1,12 @@ +""" +Filters are functions that accept a single parameter, an :class:`~telethon.events.Event` instance, and return a :class:`bool`. + +When the return value is :data:`True`, the associated :mod:`~telethon.events` handler will be invoked. + +.. seealso:: + + The :doc:`/concepts/updates` concept to learn to combine filters or define your own. +""" from .._impl.client.events.filters import ( All, Any, diff --git a/client/src/telethon/types.py b/client/src/telethon/types.py index 0bb73d51..4e9504c0 100644 --- a/client/src/telethon/types.py +++ b/client/src/telethon/types.py @@ -1,4 +1,4 @@ -from ._impl.client.client import Config, InlineResult, InlineResults +from ._impl.client.client import Config, InlineResult from ._impl.client.types import ( AsyncList, Channel, @@ -20,7 +20,6 @@ from ._impl.session import PackedChat, PackedType __all__ = [ "Config", "InlineResult", - "InlineResults", "AsyncList", "Channel", "Chat", diff --git a/tools/check.py b/tools/check.py index 80906ecb..4fce513b 100644 --- a/tools/check.py +++ b/tools/check.py @@ -18,7 +18,7 @@ def main() -> None: run("isort", ".", "-c", "--profile", "black", "--gitignore") or run("black", ".", "--check", "--extend-exclude", BLACK_IGNORE) or run("mypy", "--strict", ".") - or run("sphinx", "-nW", "client/doc", tmp_dir) + or run("sphinx", "-M", "dummy", "client/doc", tmp_dir, "-n", "-W") or run("pytest", ".", "-m", "not net") )