mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-07-10 16:12:22 +03:00
Work towards message buttons
This commit is contained in:
parent
d15e84e595
commit
8fe89496d6
|
@ -43,7 +43,7 @@ The :class:`session.Storage` abstract base class defines the required methods to
|
||||||
Telethon comes with two built-in storages:
|
Telethon comes with two built-in storages:
|
||||||
|
|
||||||
* :class:`~session.SqliteSession`. This is used by default when a string or path is used.
|
* :class:`~session.SqliteSession`. This is used by default when a string or path is used.
|
||||||
* :class:`~session.MemorySession`. This is used by default when the path is ``None``.
|
* :class:`~session.MemorySession`. This is used by default when the path is :data:`None`.
|
||||||
You can also use it directly when you have a :class:`~session.Session` instance.
|
You can also use it directly when you have a :class:`~session.Session` instance.
|
||||||
It's useful when you don't have file-system access.
|
It's useful when you don't have file-system access.
|
||||||
|
|
||||||
|
|
|
@ -377,7 +377,7 @@ Now handlers are called in order until the filter for one returns :data:`True`.
|
||||||
The default behaviour is that handlers after that one are not called.
|
The default behaviour is that handlers after that one are not called.
|
||||||
This behaviour can be changed with the ``check_all_handlers`` flag in :class:`Client` constructor.
|
This behaviour can be changed with the ``check_all_handlers`` flag in :class:`Client` constructor.
|
||||||
|
|
||||||
:class:`events.CallbackQuery` no longer also handles "inline bot callback queries".
|
``events.CallbackQuery`` has been renamed to :class:`events.ButtonCallback` and no longer also handles "inline bot callback queries".
|
||||||
This was a hacky workaround.
|
This was a hacky workaround.
|
||||||
|
|
||||||
:class:`events.MessageRead` no longer triggers when the *contents* of a message are read, such as voice notes being played.
|
:class:`events.MessageRead` no longer triggers when the *contents* of a message are read, such as voice notes being played.
|
||||||
|
|
|
@ -1 +1,12 @@
|
||||||
.. autoclass:: telethon.Client
|
.. currentmodule:: telethon
|
||||||
|
|
||||||
|
Client class
|
||||||
|
============
|
||||||
|
|
||||||
|
The :class:`Client` class is the "entry point" of the library.
|
||||||
|
|
||||||
|
Most client methods have an alias in the respective types.
|
||||||
|
For example, :meth:`Client.forward_messages` can also be invoked from :meth:`types.Message.forward`.
|
||||||
|
With a few exceptions, "client.verb_object" methods also exist as "object.verb".
|
||||||
|
|
||||||
|
.. autoclass:: Client
|
||||||
|
|
|
@ -1,8 +1,25 @@
|
||||||
Types
|
Types
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
This section contains most custom types used by the library.
|
||||||
|
:doc:`events` and the :doc:`client` get their own section to prevent the page from growing out of control.
|
||||||
|
|
||||||
|
Some of these are further divided into additional submodules.
|
||||||
|
This keeps them neatly grouped and avoids polluting a single module too much.
|
||||||
|
|
||||||
|
|
||||||
|
Core types
|
||||||
|
----------
|
||||||
|
|
||||||
.. automodule:: telethon.types
|
.. automodule:: telethon.types
|
||||||
|
|
||||||
|
|
||||||
|
Keyboard buttons
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. automodule:: telethon.types.buttons
|
||||||
|
|
||||||
|
|
||||||
Errors
|
Errors
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ from ..types import (
|
||||||
PasswordToken,
|
PasswordToken,
|
||||||
RecentAction,
|
RecentAction,
|
||||||
User,
|
User,
|
||||||
|
buttons,
|
||||||
)
|
)
|
||||||
from .auth import (
|
from .auth import (
|
||||||
bot_sign_in,
|
bot_sign_in,
|
||||||
|
@ -88,6 +89,7 @@ from .messages import (
|
||||||
get_messages,
|
get_messages,
|
||||||
get_messages_with_ids,
|
get_messages_with_ids,
|
||||||
pin_message,
|
pin_message,
|
||||||
|
read_message,
|
||||||
search_all_messages,
|
search_all_messages,
|
||||||
search_messages,
|
search_messages,
|
||||||
send_message,
|
send_message,
|
||||||
|
@ -126,8 +128,6 @@ class Client:
|
||||||
"""
|
"""
|
||||||
A client capable of connecting to Telegram and sending requests.
|
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`:
|
This class can be used as an asynchronous context manager to automatically :meth:`connect` and :meth:`disconnect`:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -491,6 +491,58 @@ class Client:
|
||||||
"""
|
"""
|
||||||
await download(self, media, file)
|
await download(self, media, file)
|
||||||
|
|
||||||
|
async def edit_draft(
|
||||||
|
self,
|
||||||
|
chat: ChatLike,
|
||||||
|
text: Optional[str] = None,
|
||||||
|
*,
|
||||||
|
markdown: Optional[str] = None,
|
||||||
|
html: Optional[str] = None,
|
||||||
|
link_preview: bool = False,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
) -> Draft:
|
||||||
|
"""
|
||||||
|
Set a draft message in a chat.
|
||||||
|
|
||||||
|
This can also be used to clear the draft by setting the text to an empty string ``""``.
|
||||||
|
|
||||||
|
:param chat:
|
||||||
|
The :term:`chat` where the draft will be saved to.
|
||||||
|
|
||||||
|
:param text: See :ref:`formatting`.
|
||||||
|
:param markdown: See :ref:`formatting`.
|
||||||
|
:param html: See :ref:`formatting`.
|
||||||
|
:param link_preview: See :ref:`formatting`.
|
||||||
|
|
||||||
|
:param reply_to:
|
||||||
|
The message identifier of the message to reply to.
|
||||||
|
|
||||||
|
:return: The created draft.
|
||||||
|
|
||||||
|
.. 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_draft(
|
||||||
|
self,
|
||||||
|
chat,
|
||||||
|
text,
|
||||||
|
markdown=markdown,
|
||||||
|
html=html,
|
||||||
|
link_preview=link_preview,
|
||||||
|
reply_to=reply_to,
|
||||||
|
)
|
||||||
|
|
||||||
async def edit_message(
|
async def edit_message(
|
||||||
self,
|
self,
|
||||||
chat: ChatLike,
|
chat: ChatLike,
|
||||||
|
@ -987,6 +1039,40 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return await pin_message(self, chat, message_id)
|
return await pin_message(self, chat, message_id)
|
||||||
|
|
||||||
|
async def read_message(
|
||||||
|
self, chat: ChatLike, message_id: Union[int, Literal["all"]]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Mark messages as read.
|
||||||
|
|
||||||
|
This will send a read acknowledgment to all messages with identifiers below and up-to the given message identifier.
|
||||||
|
|
||||||
|
This is often represented as a blue double-check (✓✓).
|
||||||
|
|
||||||
|
A single check (✓) in Telegram often indicates the message was sent and perhaps received, but not read.
|
||||||
|
|
||||||
|
A clock (🕒) in Telegram often indicates the message was not yet sent at all.
|
||||||
|
This most commonly occurs when sending messages without a network connection.
|
||||||
|
|
||||||
|
:param chat:
|
||||||
|
The chat where the messages to be marked as read are.
|
||||||
|
|
||||||
|
:param message_id:
|
||||||
|
The identifier of the message to mark as read.
|
||||||
|
All messages older (sent before) this one will also be marked as read.
|
||||||
|
|
||||||
|
The literal ``'all'`` may be used to mark all messages in a chat as read.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Mark all messages as read
|
||||||
|
message = await client.read_message(chat, 'all')
|
||||||
|
await message.delete()
|
||||||
|
"""
|
||||||
|
await read_message(self, chat, message_id)
|
||||||
|
|
||||||
def remove_event_handler(self, handler: Callable[[Event], Awaitable[Any]]) -> None:
|
def remove_event_handler(self, handler: Callable[[Event], Awaitable[Any]]) -> None:
|
||||||
"""
|
"""
|
||||||
Remove the handler as a function to be called when events occur.
|
Remove the handler as a function to be called when events occur.
|
||||||
|
@ -1417,6 +1503,9 @@ class Client:
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
link_preview: bool = False,
|
link_preview: bool = False,
|
||||||
reply_to: Optional[int] = None,
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[
|
||||||
|
Union[List[buttons.Button], List[List[buttons.Button]]]
|
||||||
|
] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send a message.
|
Send a message.
|
||||||
|
@ -1432,6 +1521,11 @@ class Client:
|
||||||
:param reply_to:
|
:param reply_to:
|
||||||
The message identifier of the message to reply to.
|
The message identifier of the message to reply to.
|
||||||
|
|
||||||
|
:param buttons:
|
||||||
|
The buttons to use for the message.
|
||||||
|
|
||||||
|
Only bot accounts can send buttons.
|
||||||
|
|
||||||
.. rubric:: Example
|
.. rubric:: Example
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -1446,6 +1540,7 @@ class Client:
|
||||||
html=html,
|
html=html,
|
||||||
link_preview=link_preview,
|
link_preview=link_preview,
|
||||||
reply_to=reply_to,
|
reply_to=reply_to,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_photo(
|
async def send_photo(
|
||||||
|
@ -1573,58 +1668,6 @@ class Client:
|
||||||
def set_default_rights(self, chat: ChatLike, user: ChatLike) -> None:
|
def set_default_rights(self, chat: ChatLike, user: ChatLike) -> None:
|
||||||
set_default_rights(self, chat, user)
|
set_default_rights(self, chat, user)
|
||||||
|
|
||||||
async def edit_draft(
|
|
||||||
self,
|
|
||||||
chat: ChatLike,
|
|
||||||
text: Optional[str] = None,
|
|
||||||
*,
|
|
||||||
markdown: Optional[str] = None,
|
|
||||||
html: Optional[str] = None,
|
|
||||||
link_preview: bool = False,
|
|
||||||
reply_to: Optional[int] = None,
|
|
||||||
) -> Draft:
|
|
||||||
"""
|
|
||||||
Set a draft message in a chat.
|
|
||||||
|
|
||||||
This can also be used to clear the draft by setting the text to an empty string ``""``.
|
|
||||||
|
|
||||||
:param chat:
|
|
||||||
The :term:`chat` where the draft will be saved to.
|
|
||||||
|
|
||||||
:param text: See :ref:`formatting`.
|
|
||||||
:param markdown: See :ref:`formatting`.
|
|
||||||
:param html: See :ref:`formatting`.
|
|
||||||
:param link_preview: See :ref:`formatting`.
|
|
||||||
|
|
||||||
:param reply_to:
|
|
||||||
The message identifier of the message to reply to.
|
|
||||||
|
|
||||||
:return: The created draft.
|
|
||||||
|
|
||||||
.. 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_draft(
|
|
||||||
self,
|
|
||||||
chat,
|
|
||||||
text,
|
|
||||||
markdown=markdown,
|
|
||||||
html=html,
|
|
||||||
link_preview=link_preview,
|
|
||||||
reply_to=reply_to,
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_handler_filter(
|
def set_handler_filter(
|
||||||
self,
|
self,
|
||||||
handler: Callable[[Event], Awaitable[Any]],
|
handler: Callable[[Event], Awaitable[Any]],
|
||||||
|
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
|
||||||
from ...session import PackedChat
|
from ...session import PackedChat
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..parsers import parse_html_message, parse_markdown_message
|
from ..parsers import parse_html_message, parse_markdown_message
|
||||||
from ..types import AsyncList, Chat, ChatLike, Message
|
from ..types import AsyncList, Chat, ChatLike, Message, buttons
|
||||||
from ..utils import build_chat_map, generate_random_id, peer_id
|
from ..utils import build_chat_map, generate_random_id, peer_id
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -39,6 +39,40 @@ def parse_message(
|
||||||
return parsed, entities or None
|
return parsed, entities or None
|
||||||
|
|
||||||
|
|
||||||
|
def build_keyboard(
|
||||||
|
btns: Optional[Union[List[buttons.Button], List[List[buttons.Button]]]]
|
||||||
|
) -> Optional[abcs.ReplyMarkup]:
|
||||||
|
# list[button] -> list[list[button]]
|
||||||
|
# This does allow for "invalid" inputs (mixing lists and non-lists), but that's acceptable.
|
||||||
|
buttons_lists_iter = (
|
||||||
|
button if isinstance(button, list) else [button] for button in (btns or [])
|
||||||
|
)
|
||||||
|
# Remove empty rows (also making it easy to check if all-empty).
|
||||||
|
buttons_lists = [bs for bs in buttons_lists_iter if bs]
|
||||||
|
|
||||||
|
if not buttons_lists:
|
||||||
|
return None
|
||||||
|
|
||||||
|
rows: List[abcs.KeyboardButtonRow] = [
|
||||||
|
types.KeyboardButtonRow(buttons=[btn._raw for btn in btns])
|
||||||
|
for btns in buttons_lists
|
||||||
|
]
|
||||||
|
|
||||||
|
# Guaranteed to have at least one, first one used to check if it's inline.
|
||||||
|
# If the user mixed inline with non-inline, Telegram will complain.
|
||||||
|
if isinstance(buttons_lists[0][0], buttons.InlineButton):
|
||||||
|
return types.ReplyInlineMarkup(rows=rows)
|
||||||
|
else:
|
||||||
|
return types.ReplyKeyboardMarkup(
|
||||||
|
resize=False,
|
||||||
|
single_use=False,
|
||||||
|
selective=False,
|
||||||
|
persistent=False,
|
||||||
|
rows=rows,
|
||||||
|
placeholder=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
chat: ChatLike,
|
||||||
|
@ -48,6 +82,7 @@ async def send_message(
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
link_preview: bool = False,
|
link_preview: bool = False,
|
||||||
reply_to: Optional[int] = None,
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[buttons.Button], List[List[buttons.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
packed = await self._resolve_to_packed(chat)
|
packed = await self._resolve_to_packed(chat)
|
||||||
peer = packed._to_input_peer()
|
peer = packed._to_input_peer()
|
||||||
|
@ -71,14 +106,14 @@ async def send_message(
|
||||||
else None,
|
else None,
|
||||||
message=message,
|
message=message,
|
||||||
random_id=random_id,
|
random_id=random_id,
|
||||||
reply_markup=None,
|
reply_markup=build_keyboard(buttons),
|
||||||
entities=entities,
|
entities=entities,
|
||||||
schedule_date=None,
|
schedule_date=None,
|
||||||
send_as=None,
|
send_as=None,
|
||||||
)
|
)
|
||||||
if isinstance(message, str)
|
if isinstance(message, str)
|
||||||
else functions.messages.send_message(
|
else functions.messages.send_message(
|
||||||
no_webpage=not message.web_preview,
|
no_webpage=not message.link_preview,
|
||||||
silent=message.silent,
|
silent=message.silent,
|
||||||
background=False,
|
background=False,
|
||||||
clear_draft=False,
|
clear_draft=False,
|
||||||
|
@ -531,6 +566,27 @@ async def unpin_message(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_message(
|
||||||
|
self: Client, chat: ChatLike, message_id: Union[int, Literal["all"]]
|
||||||
|
) -> None:
|
||||||
|
packed = await self._resolve_to_packed(chat)
|
||||||
|
if message_id == "all":
|
||||||
|
message_id = 0
|
||||||
|
|
||||||
|
if packed.is_channel():
|
||||||
|
await self(
|
||||||
|
functions.channels.read_history(
|
||||||
|
channel=packed._to_input_channel(), max_id=message_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self(
|
||||||
|
functions.messages.read_history(
|
||||||
|
peer=packed._to_input_peer(), max_id=message_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MessageMap:
|
class MessageMap:
|
||||||
__slots__ = ("_client", "_peer", "_random_id_to_id", "_id_to_message")
|
__slots__ = ("_client", "_peer", "_random_id_to_id", "_id_to_message")
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .messages import MessageDeleted, MessageEdited, MessageRead, NewMessage
|
from .messages import MessageDeleted, MessageEdited, MessageRead, NewMessage
|
||||||
from .queries import CallbackQuery, InlineQuery
|
from .queries import ButtonCallback, InlineQuery
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Event",
|
"Event",
|
||||||
|
@ -8,6 +8,6 @@ __all__ = [
|
||||||
"MessageEdited",
|
"MessageEdited",
|
||||||
"MessageRead",
|
"MessageRead",
|
||||||
"NewMessage",
|
"NewMessage",
|
||||||
"CallbackQuery",
|
"ButtonCallback",
|
||||||
"InlineQuery",
|
"InlineQuery",
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Dict, Optional, Self
|
from typing import TYPE_CHECKING, Dict, Optional, Self
|
||||||
|
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, functions, types
|
||||||
from ..types import Chat
|
from ..types import Chat
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
|
@ -10,25 +10,61 @@ if TYPE_CHECKING:
|
||||||
from ..client.client import Client
|
from ..client.client import Client
|
||||||
|
|
||||||
|
|
||||||
class CallbackQuery(Event):
|
class ButtonCallback(Event):
|
||||||
"""
|
"""
|
||||||
Occurs when an inline button was pressed.
|
Occurs when the user :meth:`~telethon.types.buttons.Callback.click`\ s a :class:`~telethon.types.buttons.Callback` button.
|
||||||
|
|
||||||
Only bot accounts can receive this event.
|
Only bot accounts can receive this event, because only bots can send :class:`~telethon.types.buttons.Callback` buttons.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, update: types.UpdateBotCallbackQuery):
|
def __init__(
|
||||||
self._update = update
|
self,
|
||||||
|
client: Client,
|
||||||
|
update: types.UpdateBotCallbackQuery,
|
||||||
|
chat_map: Dict[int, Chat],
|
||||||
|
):
|
||||||
|
self._client = client
|
||||||
|
self._raw = update
|
||||||
|
self._chat_map = chat_map
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(
|
def _try_from_update(
|
||||||
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
if isinstance(update, types.UpdateBotCallbackQuery):
|
if isinstance(update, types.UpdateBotCallbackQuery) and update.data is not None:
|
||||||
return cls._create(update)
|
return cls._create(client, update, chat_map)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> bytes:
|
||||||
|
assert self._raw.data is not None
|
||||||
|
return self._raw.data
|
||||||
|
|
||||||
|
async def answer(
|
||||||
|
self,
|
||||||
|
text: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Answer the callback query.
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
You must call this function for the loading circle to stop on the user's side.
|
||||||
|
|
||||||
|
:param text:
|
||||||
|
The text of the message to display to the user, usually as a toast.
|
||||||
|
"""
|
||||||
|
await self._client(
|
||||||
|
functions.messages.set_bot_callback_answer(
|
||||||
|
alert=False,
|
||||||
|
query_id=self._raw.query_id,
|
||||||
|
message=text,
|
||||||
|
url=None,
|
||||||
|
cache_time=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InlineQuery(Event):
|
class InlineQuery(Event):
|
||||||
"""
|
"""
|
||||||
|
@ -38,7 +74,7 @@ class InlineQuery(Event):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, update: types.UpdateBotInlineQuery):
|
def __init__(self, update: types.UpdateBotInlineQuery):
|
||||||
self._update = update
|
self._raw = update
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(
|
def _try_from_update(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .async_list import AsyncList
|
from .async_list import AsyncList
|
||||||
|
from .callback_answer import CallbackAnswer
|
||||||
from .chat import Channel, Chat, ChatLike, Group, User
|
from .chat import Channel, Chat, ChatLike, Group, User
|
||||||
from .dialog import Dialog
|
from .dialog import Dialog
|
||||||
from .draft import Draft
|
from .draft import Draft
|
||||||
|
@ -13,6 +14,7 @@ from .recent_action import RecentAction
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AsyncList",
|
"AsyncList",
|
||||||
|
"CallbackAnswer",
|
||||||
"Channel",
|
"Channel",
|
||||||
"Chat",
|
"Chat",
|
||||||
"ChatLike",
|
"ChatLike",
|
||||||
|
|
89
client/src/telethon/_impl/client/types/buttons/__init__.py
Normal file
89
client/src/telethon/_impl/client/types/buttons/__init__.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import weakref
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ....tl import abcs, types
|
||||||
|
from .button import Button
|
||||||
|
from .callback import Callback
|
||||||
|
from .inline_button import InlineButton
|
||||||
|
from .request_geo_location import RequestGeoLocation
|
||||||
|
from .request_phone import RequestPhone
|
||||||
|
from .request_poll import RequestPoll
|
||||||
|
from .switch_inline import SwitchInline
|
||||||
|
from .text import Text
|
||||||
|
from .url import Url
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..message import Message
|
||||||
|
|
||||||
|
|
||||||
|
def as_concrete_row(row: abcs.KeyboardButtonRow) -> types.KeyboardButtonRow:
|
||||||
|
assert isinstance(row, types.KeyboardButtonRow)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def create_button(message: Message, raw: abcs.KeyboardButton) -> Button:
|
||||||
|
"""
|
||||||
|
Create a custom button from a Telegram button.
|
||||||
|
|
||||||
|
Types with no friendly variant fallback to :class:`telethon.types.buttons.Button` or `telethon.types.buttons.Inline`.
|
||||||
|
"""
|
||||||
|
cls = Button
|
||||||
|
|
||||||
|
if isinstance(raw, types.KeyboardButtonCallback):
|
||||||
|
cls = Callback
|
||||||
|
elif isinstance(raw, types.KeyboardButtonRequestGeoLocation):
|
||||||
|
cls = RequestGeoLocation
|
||||||
|
elif isinstance(raw, types.KeyboardButtonRequestPhone):
|
||||||
|
cls = RequestPhone
|
||||||
|
elif isinstance(raw, types.KeyboardButtonRequestPoll):
|
||||||
|
cls = RequestPoll
|
||||||
|
elif isinstance(raw, types.KeyboardButtonSwitchInline):
|
||||||
|
cls = SwitchInline
|
||||||
|
elif isinstance(raw, types.KeyboardButton):
|
||||||
|
cls = Text
|
||||||
|
elif isinstance(raw, types.KeyboardButtonUrl):
|
||||||
|
cls = Url
|
||||||
|
elif isinstance(
|
||||||
|
raw,
|
||||||
|
(
|
||||||
|
types.KeyboardButtonBuy,
|
||||||
|
types.KeyboardButtonGame,
|
||||||
|
types.KeyboardButtonUrlAuth,
|
||||||
|
types.InputKeyboardButtonUrlAuth,
|
||||||
|
types.KeyboardButtonWebView,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
cls = InlineButton
|
||||||
|
elif isinstance(
|
||||||
|
raw,
|
||||||
|
(
|
||||||
|
types.InputKeyboardButtonUserProfile,
|
||||||
|
types.KeyboardButtonUserProfile,
|
||||||
|
types.KeyboardButtonSimpleWebView,
|
||||||
|
types.KeyboardButtonRequestPeer,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
cls = Button
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
instance = cls.__new__(cls)
|
||||||
|
instance._msg = weakref.ref(message)
|
||||||
|
instance._raw = raw
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Button",
|
||||||
|
"Callback",
|
||||||
|
"InlineButton",
|
||||||
|
"RequestGeoLocation",
|
||||||
|
"RequestPhone",
|
||||||
|
"RequestPoll",
|
||||||
|
"SwitchInline",
|
||||||
|
"Text",
|
||||||
|
"Url",
|
||||||
|
"create",
|
||||||
|
]
|
74
client/src/telethon/_impl/client/types/buttons/button.py
Normal file
74
client/src/telethon/_impl/client/types/buttons/button.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import weakref
|
||||||
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
|
from ....tl import types
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..message import Message
|
||||||
|
|
||||||
|
|
||||||
|
ButtonTypes = Union[
|
||||||
|
types.KeyboardButton,
|
||||||
|
types.KeyboardButtonUrl,
|
||||||
|
types.KeyboardButtonCallback,
|
||||||
|
types.KeyboardButtonRequestPhone,
|
||||||
|
types.KeyboardButtonRequestGeoLocation,
|
||||||
|
types.KeyboardButtonSwitchInline,
|
||||||
|
types.KeyboardButtonGame,
|
||||||
|
types.KeyboardButtonBuy,
|
||||||
|
types.KeyboardButtonUrlAuth,
|
||||||
|
types.InputKeyboardButtonUrlAuth,
|
||||||
|
types.KeyboardButtonRequestPoll,
|
||||||
|
types.InputKeyboardButtonUserProfile,
|
||||||
|
types.KeyboardButtonUserProfile,
|
||||||
|
types.KeyboardButtonWebView,
|
||||||
|
types.KeyboardButtonSimpleWebView,
|
||||||
|
types.KeyboardButtonRequestPeer,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Button:
|
||||||
|
"""
|
||||||
|
The button base type.
|
||||||
|
|
||||||
|
All other :mod:`~telethon.types.buttons` inherit this class.
|
||||||
|
|
||||||
|
You can only click buttons that have been received from Telegram.
|
||||||
|
Attempting to click a button you created will fail with an error.
|
||||||
|
|
||||||
|
Not all buttons can be clicked, and each button will do something different when clicked.
|
||||||
|
The reason for this is that Telethon cannot interact with any user to complete certain tasks.
|
||||||
|
Only straightforward actions can be performed automatically, such as sending a text message.
|
||||||
|
|
||||||
|
To check if a button is clickable, use :func:`hasattr` on the ``'click'`` method.
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str) -> None:
|
||||||
|
if self.__class__ == Button:
|
||||||
|
raise TypeError(
|
||||||
|
f"Can't instantiate abstract class {self.__class__.__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._raw: ButtonTypes = types.KeyboardButton(text=text)
|
||||||
|
self._msg: Optional[weakref.ReferenceType[Message]] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self) -> str:
|
||||||
|
"""
|
||||||
|
The button's text that is displayed to the user.
|
||||||
|
"""
|
||||||
|
return self._raw.text
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, value: str) -> None:
|
||||||
|
self._raw.text = value
|
||||||
|
|
||||||
|
def _message(self) -> Message:
|
||||||
|
if self._msg and (message := self._msg()):
|
||||||
|
return message
|
||||||
|
else:
|
||||||
|
raise ValueError("Buttons created by yourself cannot be clicked")
|
62
client/src/telethon/_impl/client/types/buttons/callback.py
Normal file
62
client/src/telethon/_impl/client/types/buttons/callback.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ....tl import functions, types
|
||||||
|
from ..callback_answer import CallbackAnswer
|
||||||
|
from .inline_button import InlineButton
|
||||||
|
|
||||||
|
|
||||||
|
class Callback(InlineButton):
|
||||||
|
"""
|
||||||
|
Inline button that will trigger a :class:`telethon.events.ButtonCallback` with the button's data.
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
:param data: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str, data: Optional[bytes] = None) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
self._raw = types.KeyboardButtonCallback(
|
||||||
|
requires_password=False,
|
||||||
|
text=text,
|
||||||
|
data=data or text.encode("utf-8", errors="replace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> bytes:
|
||||||
|
"""
|
||||||
|
The button's binary payload.
|
||||||
|
|
||||||
|
This data will be received by :class:`telethon.events.ButtonCallback` when the button is pressed.
|
||||||
|
"""
|
||||||
|
assert isinstance(self._raw, types.KeyboardButtonCallback)
|
||||||
|
return self._raw.data
|
||||||
|
|
||||||
|
@data.setter
|
||||||
|
def data(self, value: bytes) -> None:
|
||||||
|
assert isinstance(self._raw, types.KeyboardButtonCallback)
|
||||||
|
self._raw.data = value
|
||||||
|
|
||||||
|
async def click(self) -> Optional[CallbackAnswer]:
|
||||||
|
"""
|
||||||
|
Click the button, sending the button's :attr:`data` to the bot.
|
||||||
|
|
||||||
|
The bot will receive a :class:`~telethon.events.ButtonCallback` event
|
||||||
|
which they must quickly :meth:`~telethon.events.ButtonCallback.answer`.
|
||||||
|
|
||||||
|
The bot's answer will be returned, or :data:`None` if they don't answer in time.
|
||||||
|
"""
|
||||||
|
message = self._message()
|
||||||
|
packed = message.chat.pack()
|
||||||
|
assert packed
|
||||||
|
|
||||||
|
return CallbackAnswer._create(
|
||||||
|
await message._client(
|
||||||
|
functions.messages.get_bot_callback_answer(
|
||||||
|
game=False,
|
||||||
|
peer=packed._to_input_peer(),
|
||||||
|
msg_id=message.id,
|
||||||
|
data=self.data,
|
||||||
|
password=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,34 @@
|
||||||
|
from .button import Button
|
||||||
|
|
||||||
|
|
||||||
|
class InlineButton(Button):
|
||||||
|
"""
|
||||||
|
Inline button base type.
|
||||||
|
|
||||||
|
Inline buttons appear directly under a message (inline in the chat history).
|
||||||
|
|
||||||
|
You cannot create a naked :class:`InlineButton` directly.
|
||||||
|
Instead, it can be used to check whether a button is inline or not.
|
||||||
|
|
||||||
|
Buttons that behave as a "custom key" and replace the user's virtual keyboard
|
||||||
|
can be tested by checking that they are not inline.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.types import buttons
|
||||||
|
|
||||||
|
is_inline_button = isinstance(button, buttons.Inline)
|
||||||
|
is_keyboard_button = not isinstance(button, buttons.Inline)
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str) -> None:
|
||||||
|
if self.__class__ == InlineButton:
|
||||||
|
raise TypeError(
|
||||||
|
f"Can't instantiate abstract class {self.__class__.__name__}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
super().__init__(text)
|
|
@ -0,0 +1,14 @@
|
||||||
|
from ....tl import types
|
||||||
|
from .button import Button
|
||||||
|
|
||||||
|
|
||||||
|
class RequestGeoLocation(Button):
|
||||||
|
"""
|
||||||
|
Keyboard button that will prompt the user to share the geo point with their current location.
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
self._raw = types.KeyboardButtonRequestGeoLocation(text=text)
|
|
@ -0,0 +1,14 @@
|
||||||
|
from ....tl import types
|
||||||
|
from .button import Button
|
||||||
|
|
||||||
|
|
||||||
|
class RequestPhone(Button):
|
||||||
|
"""
|
||||||
|
Keyboard button that will prompt the user to share the contact with their phone number.
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
self._raw = types.KeyboardButtonRequestPhone(text=text)
|
|
@ -0,0 +1,14 @@
|
||||||
|
from ....tl import types
|
||||||
|
from .button import Button
|
||||||
|
|
||||||
|
|
||||||
|
class RequestPoll(Button):
|
||||||
|
"""
|
||||||
|
Keyboard button that will prompt the user to create a poll.
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str, *, quiz: bool = False) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
self._raw = types.KeyboardButtonRequestPoll(text=text, quiz=quiz)
|
|
@ -0,0 +1,32 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ....tl import types
|
||||||
|
from .inline_button import InlineButton
|
||||||
|
|
||||||
|
|
||||||
|
class SwitchInline(InlineButton):
|
||||||
|
"""
|
||||||
|
Inline button that will switch the user to inline mode to trigger :class:`telethon.events.InlineQuery`.
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
:param query: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str, query: Optional[str] = None) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
self._raw = types.KeyboardButtonSwitchInline(
|
||||||
|
same_peer=False, text=text, query=query or "", peer_types=None
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def query(self) -> str:
|
||||||
|
"""
|
||||||
|
The query string to set by default on the user's message input.
|
||||||
|
"""
|
||||||
|
assert isinstance(self._raw, types.KeyboardButtonSwitchInline)
|
||||||
|
return self._raw.query
|
||||||
|
|
||||||
|
@query.setter
|
||||||
|
def query(self, value: str) -> None:
|
||||||
|
assert isinstance(self._raw, types.KeyboardButtonSwitchInline)
|
||||||
|
self._raw.query = value
|
38
client/src/telethon/_impl/client/types/buttons/text.py
Normal file
38
client/src/telethon/_impl/client/types/buttons/text.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .button import Button
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..message import Message
|
||||||
|
|
||||||
|
|
||||||
|
class Text(Button):
|
||||||
|
"""
|
||||||
|
This is the most basic keyboard button and only has :attr:`text`.
|
||||||
|
|
||||||
|
Note that it is not possible to distinguish between a :meth:`click` to this button being and the user typing the text themselves.
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self) -> str:
|
||||||
|
"""
|
||||||
|
The button's text that is both displayed to the user and will be sent on :meth:`click`.
|
||||||
|
"""
|
||||||
|
return self._raw.text
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, value: str) -> None:
|
||||||
|
self._raw.text = value
|
||||||
|
|
||||||
|
async def click(self) -> Message:
|
||||||
|
"""
|
||||||
|
Click the button, sending a message to the chat as-if the user typed and sent the text themselves.
|
||||||
|
"""
|
||||||
|
return await self._message().respond(self._raw.text)
|
30
client/src/telethon/_impl/client/types/buttons/url.py
Normal file
30
client/src/telethon/_impl/client/types/buttons/url.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ....tl import types
|
||||||
|
from .inline_button import InlineButton
|
||||||
|
|
||||||
|
|
||||||
|
class Url(InlineButton):
|
||||||
|
"""
|
||||||
|
Inline button that will prompt the user to open the specified URL when clicked.
|
||||||
|
|
||||||
|
:param text: See below.
|
||||||
|
:param url: See below.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, text: str, url: Optional[str] = None) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
self._raw = types.KeyboardButtonUrl(text=text, url=url or text)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
"""
|
||||||
|
The URL to open.
|
||||||
|
"""
|
||||||
|
assert isinstance(self._raw, types.KeyboardButtonUrl)
|
||||||
|
return self._raw.url
|
||||||
|
|
||||||
|
@url.setter
|
||||||
|
def url(self, value: str) -> None:
|
||||||
|
assert isinstance(self._raw, types.KeyboardButtonUrl)
|
||||||
|
self._raw.url = value
|
28
client/src/telethon/_impl/client/types/callback_answer.py
Normal file
28
client/src/telethon/_impl/client/types/callback_answer.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ...tl import abcs, types
|
||||||
|
from .meta import NoPublicConstructor
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackAnswer(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
A bot's :class:`~telethon.types.buttons.Callback` :meth:`~telethon.events.ButtonCallback.answer`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, raw: abcs.messages.BotCallbackAnswer) -> None:
|
||||||
|
assert isinstance(raw, types.messages.BotCallbackAnswer)
|
||||||
|
self._raw = raw
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The answer's text, usually displayed as a toast.
|
||||||
|
"""
|
||||||
|
return self._raw.message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The answer's URL.
|
||||||
|
"""
|
||||||
|
return self._raw.url
|
|
@ -56,7 +56,9 @@ class Draft(metaclass=NoPublicConstructor):
|
||||||
@property
|
@property
|
||||||
def chat(self) -> Chat:
|
def chat(self) -> Chat:
|
||||||
"""
|
"""
|
||||||
The chat where the draft will be sent to.
|
The chat where the draft is saved.
|
||||||
|
|
||||||
|
This is also the chat where the message will be sent to by :meth:`send`.
|
||||||
"""
|
"""
|
||||||
from ..utils import expand_peer, peer_id
|
from ..utils import expand_peer, peer_id
|
||||||
|
|
||||||
|
|
22
client/src/telethon/_impl/client/types/forward_info.py
Normal file
22
client/src/telethon/_impl/client/types/forward_info.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from ...tl import types
|
||||||
|
from .meta import NoPublicConstructor
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardInfo(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
Information about where a message was forwarded from.
|
||||||
|
|
||||||
|
This is also known as the forward header, as it's often displayed at the top of messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_code", "_phone")
|
||||||
|
|
||||||
|
def __init__(self, code: types.auth.SentCode, phone: str) -> None:
|
||||||
|
self._code = code
|
||||||
|
self._phone = phone
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _new(cls, code: types.auth.SentCode, phone: str) -> Self:
|
||||||
|
return cls._create(code, phone)
|
|
@ -12,6 +12,12 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class InlineResult(metaclass=NoPublicConstructor):
|
class InlineResult(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
A single inline result from an inline query made to a bot.
|
||||||
|
|
||||||
|
This is returned when calling :meth:`telethon.Client.inline_query`.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
client: Client,
|
client: Client,
|
||||||
|
@ -30,10 +36,16 @@ class InlineResult(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self) -> str:
|
def title(self) -> str:
|
||||||
|
"""
|
||||||
|
The title of the result, or the empty string if there is none.
|
||||||
|
"""
|
||||||
return self._raw.title or ""
|
return self._raw.title or ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self) -> Optional[str]:
|
def description(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The description of the result, if available.
|
||||||
|
"""
|
||||||
return self._raw.description
|
return self._raw.description
|
||||||
|
|
||||||
async def send(
|
async def send(
|
||||||
|
@ -41,7 +53,7 @@ class InlineResult(metaclass=NoPublicConstructor):
|
||||||
chat: Optional[ChatLike] = None,
|
chat: Optional[ChatLike] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send the inline result to the desired chat.
|
Send the result to the desired chat.
|
||||||
|
|
||||||
:param chat:
|
:param chat:
|
||||||
The chat where the inline result should be sent to.
|
The chat where the inline result should be sent to.
|
||||||
|
|
22
client/src/telethon/_impl/client/types/link_preview.py
Normal file
22
client/src/telethon/_impl/client/types/link_preview.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from ...tl import types
|
||||||
|
from .meta import NoPublicConstructor
|
||||||
|
|
||||||
|
|
||||||
|
class LinkPreview(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
Information about a link preview.
|
||||||
|
|
||||||
|
This comes from media attached to a message, and is automatically generated by Telegram.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_code", "_phone")
|
||||||
|
|
||||||
|
def __init__(self, code: types.auth.SentCode, phone: str) -> None:
|
||||||
|
self._code = code
|
||||||
|
self._phone = phone
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _new(cls, code: types.auth.SentCode, phone: str) -> Self:
|
||||||
|
return cls._create(code, phone)
|
|
@ -21,4 +21,10 @@ class LoginToken(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timeout(self) -> Optional[int]:
|
def timeout(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Number of seconds before this token expires.
|
||||||
|
|
||||||
|
This property does not return different values as the current time advances.
|
||||||
|
To determine when the token expires, add the timeout to the current time as soon as the token is obtained.
|
||||||
|
"""
|
||||||
return self._code.timeout
|
return self._code.timeout
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Self, Union
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Self, Union
|
||||||
|
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, types
|
||||||
from ..parsers import generate_html_message, generate_markdown_message
|
from ..parsers import generate_html_message, generate_markdown_message
|
||||||
|
from .buttons import Button, as_concrete_row, create_button
|
||||||
from .chat import Chat, ChatLike
|
from .chat import Chat, ChatLike
|
||||||
from .file import File
|
from .file import File
|
||||||
from .meta import NoPublicConstructor
|
from .meta import NoPublicConstructor
|
||||||
|
@ -109,10 +110,18 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self) -> Optional[str]:
|
def text(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The message text without any formatting.
|
||||||
|
"""
|
||||||
return getattr(self._raw, "message", None)
|
return getattr(self._raw, "message", None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text_html(self) -> Optional[str]:
|
def text_html(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The message text formatted using standard `HTML elements <https://developer.mozilla.org/en-US/docs/Web/HTML/Element>`_.
|
||||||
|
|
||||||
|
See :ref:`formatting` to learn the HTML elements used.
|
||||||
|
"""
|
||||||
if text := getattr(self._raw, "message", None):
|
if text := getattr(self._raw, "message", None):
|
||||||
return generate_html_message(
|
return generate_html_message(
|
||||||
text, getattr(self._raw, "entities", None) or []
|
text, getattr(self._raw, "entities", None) or []
|
||||||
|
@ -122,6 +131,11 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text_markdown(self) -> Optional[str]:
|
def text_markdown(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The message text formatted as `CommonMark's markdown <https://commonmark.org/>`_.
|
||||||
|
|
||||||
|
See :ref:`formatting` to learn the formatting characters used.
|
||||||
|
"""
|
||||||
if text := getattr(self._raw, "message", None):
|
if text := getattr(self._raw, "message", None):
|
||||||
return generate_markdown_message(
|
return generate_markdown_message(
|
||||||
text, getattr(self._raw, "entities", None) or []
|
text, getattr(self._raw, "entities", None) or []
|
||||||
|
@ -131,22 +145,37 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date(self) -> Optional[datetime.datetime]:
|
def date(self) -> Optional[datetime.datetime]:
|
||||||
|
"""
|
||||||
|
The date when the message was sent.
|
||||||
|
"""
|
||||||
from ..utils import adapt_date
|
from ..utils import adapt_date
|
||||||
|
|
||||||
return adapt_date(getattr(self._raw, "date", None))
|
return adapt_date(getattr(self._raw, "date", None))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chat(self) -> Chat:
|
def chat(self) -> Chat:
|
||||||
|
"""
|
||||||
|
The :term:`chat` when the message was sent.
|
||||||
|
"""
|
||||||
from ..utils import expand_peer, peer_id
|
from ..utils import expand_peer, peer_id
|
||||||
|
|
||||||
peer = self._raw.peer_id or types.PeerUser(user_id=0)
|
peer = self._raw.peer_id or types.PeerUser(user_id=0)
|
||||||
broadcast = broadcast = getattr(self._raw, "post", None)
|
pid = peer_id(peer)
|
||||||
return self._chat_map.get(peer_id(peer)) or expand_peer(
|
if pid not in self._chat_map:
|
||||||
peer, broadcast=broadcast
|
self._chat_map[pid] = expand_peer(
|
||||||
|
peer, broadcast=getattr(self._raw, "post", None)
|
||||||
)
|
)
|
||||||
|
return self._chat_map[pid]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sender(self) -> Optional[Chat]:
|
def sender(self) -> Optional[Chat]:
|
||||||
|
"""
|
||||||
|
The :term:`chat` that sent the message.
|
||||||
|
|
||||||
|
This will usually be a :class:`User`, but can also be a :class:`Channel`.
|
||||||
|
|
||||||
|
If there is no sender, it means the message was sent by an anonymous user.
|
||||||
|
"""
|
||||||
from ..utils import expand_peer, peer_id
|
from ..utils import expand_peer, peer_id
|
||||||
|
|
||||||
if (from_ := getattr(self._raw, "from_id", None)) is not None:
|
if (from_ := getattr(self._raw, "from_id", None)) is not None:
|
||||||
|
@ -165,11 +194,21 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def photo(self) -> Optional[File]:
|
def photo(self) -> Optional[File]:
|
||||||
|
"""
|
||||||
|
The compressed photo media :attr:`file` in the message.
|
||||||
|
|
||||||
|
This can also be used as a way to check that the message media is a photo.
|
||||||
|
"""
|
||||||
photo = self._file()
|
photo = self._file()
|
||||||
return photo if photo and photo._photo else None
|
return photo if photo and photo._photo else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audio(self) -> Optional[File]:
|
def audio(self) -> Optional[File]:
|
||||||
|
"""
|
||||||
|
The audio media :attr:`file` in the message.
|
||||||
|
|
||||||
|
This can also be used as a way to check that the message media is an audio.
|
||||||
|
"""
|
||||||
audio = self._file()
|
audio = self._file()
|
||||||
return (
|
return (
|
||||||
audio
|
audio
|
||||||
|
@ -182,6 +221,11 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def video(self) -> Optional[File]:
|
def video(self) -> Optional[File]:
|
||||||
|
"""
|
||||||
|
The video media :attr:`file` in the message.
|
||||||
|
|
||||||
|
This can also be used as a way to check that the message media is a video.
|
||||||
|
"""
|
||||||
audio = self._file()
|
audio = self._file()
|
||||||
return (
|
return (
|
||||||
audio
|
audio
|
||||||
|
@ -194,6 +238,16 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file(self) -> Optional[File]:
|
def file(self) -> Optional[File]:
|
||||||
|
"""
|
||||||
|
The downloadable file in the message.
|
||||||
|
|
||||||
|
This might also come from a link preview.
|
||||||
|
|
||||||
|
Unlike :attr:`photo`, :attr:`audio` and :attr:`video`,
|
||||||
|
this property does not care about the media type, only whether it can be downloaded.
|
||||||
|
|
||||||
|
This means the file will be :data:`None` for other media types, such as polls, venues or contacts.
|
||||||
|
"""
|
||||||
return self._file()
|
return self._file()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -231,6 +285,7 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
markdown: Optional[str] = None,
|
markdown: Optional[str] = None,
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
link_preview: bool = False,
|
link_preview: bool = False,
|
||||||
|
buttons: Optional[Union[List[Button], List[List[Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.send_message`.
|
Alias for :meth:`telethon.Client.send_message`.
|
||||||
|
@ -241,7 +296,12 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
:param link_preview: See :meth:`~telethon.Client.send_message`.
|
:param link_preview: See :meth:`~telethon.Client.send_message`.
|
||||||
"""
|
"""
|
||||||
return await self._client.send_message(
|
return await self._client.send_message(
|
||||||
self.chat, text, markdown=markdown, html=html, link_preview=link_preview
|
self.chat,
|
||||||
|
text,
|
||||||
|
markdown=markdown,
|
||||||
|
html=html,
|
||||||
|
link_preview=link_preview,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def reply(
|
async def reply(
|
||||||
|
@ -251,6 +311,7 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
markdown: Optional[str] = None,
|
markdown: Optional[str] = None,
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
link_preview: bool = False,
|
link_preview: bool = False,
|
||||||
|
buttons: Optional[Union[List[Button], List[List[Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.send_message` with the ``reply_to`` parameter set to this message.
|
Alias for :meth:`telethon.Client.send_message` with the ``reply_to`` parameter set to this message.
|
||||||
|
@ -267,6 +328,7 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
html=html,
|
html=html,
|
||||||
link_preview=link_preview,
|
link_preview=link_preview,
|
||||||
reply_to=self.id,
|
reply_to=self.id,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def delete(self, *, revoke: bool = True) -> None:
|
async def delete(self, *, revoke: bool = True) -> None:
|
||||||
|
@ -309,20 +371,23 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
return (await self._client.forward_messages(target, [self.id], self.chat))[0]
|
return (await self._client.forward_messages(target, [self.id], self.chat))[0]
|
||||||
|
|
||||||
async def mark_read(self) -> None:
|
async def read(self) -> None:
|
||||||
pass
|
"""
|
||||||
|
Alias for :meth:`telethon.Client.read_message`.
|
||||||
|
"""
|
||||||
|
await self._client.read_message(self.chat, self.id)
|
||||||
|
|
||||||
async def pin(self) -> None:
|
async def pin(self) -> Message:
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.pin_message`.
|
Alias for :meth:`telethon.Client.pin_message`.
|
||||||
"""
|
"""
|
||||||
pass
|
return await self._client.pin_message(self.chat, self.id)
|
||||||
|
|
||||||
async def unpin(self) -> None:
|
async def unpin(self) -> None:
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.unpin_message`.
|
Alias for :meth:`telethon.Client.unpin_message`.
|
||||||
"""
|
"""
|
||||||
pass
|
await self._client.unpin_message(self.chat, self.id)
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
|
@ -331,63 +396,46 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def buttons(self) -> None:
|
def buttons(self) -> Optional[List[List[Button]]]:
|
||||||
pass
|
"""
|
||||||
|
The buttons attached to the message.
|
||||||
|
|
||||||
|
These are displayed under the message if they are :class:`~telethon.types.InlineButton`,
|
||||||
|
and replace the user's virtual keyboard otherwise.
|
||||||
|
|
||||||
|
The returned value is a list of rows, each row having a list of buttons, one per column.
|
||||||
|
The amount of columns in each row can vary. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
buttons = [
|
||||||
|
[col_0, col_1], # row 0
|
||||||
|
[ col_0 ], # row 1
|
||||||
|
[col_0, col_1, col_2], # row 2
|
||||||
|
]
|
||||||
|
|
||||||
|
row = 2
|
||||||
|
col = 1
|
||||||
|
button = buttons[row][col] # the middle button on the bottom row
|
||||||
|
"""
|
||||||
|
markup = getattr(self._raw, "reply_markup", None)
|
||||||
|
if not isinstance(markup, (types.ReplyKeyboardMarkup, types.ReplyInlineMarkup)):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return [
|
||||||
|
[create_button(self, button) for button in row.buttons]
|
||||||
|
for row in map(as_concrete_row, markup.rows)
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def web_preview(self) -> None:
|
def link_preview(self) -> None:
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def voice(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def video_note(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gif(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sticker(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def contact(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def game(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def geo(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def invoice(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def poll(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def venue(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dice(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def via_bot(self) -> None:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def silent(self) -> bool:
|
def silent(self) -> bool:
|
||||||
|
"""
|
||||||
|
:data:`True` if the message is silent and should not cause a notification.
|
||||||
|
"""
|
||||||
return getattr(self._raw, "silent", None) or False
|
return getattr(self._raw, "silent", None) or False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -20,4 +20,7 @@ class PasswordToken(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hint(self) -> str:
|
def hint(self) -> str:
|
||||||
|
"""
|
||||||
|
The password hint, or the empty string if none is known.
|
||||||
|
"""
|
||||||
return self._password.hint or ""
|
return self._password.hint or ""
|
||||||
|
|
|
@ -27,4 +27,9 @@ class RecentAction(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> int:
|
def id(self) -> int:
|
||||||
|
"""
|
||||||
|
The identifier of this action.
|
||||||
|
|
||||||
|
This identifier is *not* the same as the one in the message that was edited or deleted.
|
||||||
|
"""
|
||||||
return self._raw.id
|
return self._raw.id
|
||||||
|
|
|
@ -18,6 +18,11 @@ class RpcError(ValueError):
|
||||||
|
|
||||||
This is the parent class of all :data:`telethon.errors` subtypes.
|
This is the parent class of all :data:`telethon.errors` subtypes.
|
||||||
|
|
||||||
|
:param code: See below.
|
||||||
|
:param name: See below.
|
||||||
|
:param value: See below.
|
||||||
|
:param caused_by: Constructor identifier of the request that caused the error.
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
:doc:`/concepts/errors`
|
:doc:`/concepts/errors`
|
||||||
|
@ -41,14 +46,27 @@ class RpcError(ValueError):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code(self) -> int:
|
def code(self) -> int:
|
||||||
|
"""
|
||||||
|
Integer code of the error.
|
||||||
|
|
||||||
|
This usually reassembles an `HTTP status code <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status>`_.
|
||||||
|
"""
|
||||||
return self._code
|
return self._code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
|
"""
|
||||||
|
Name of the error, usually in ``SCREAMING_CASE``.
|
||||||
|
"""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self) -> Optional[int]:
|
def value(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Integer value contained within the error.
|
||||||
|
|
||||||
|
For example, if the :attr:`name` is ``'FLOOD_WAIT'``, this would be the number of seconds.
|
||||||
|
"""
|
||||||
return self._value
|
return self._value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -6,7 +6,7 @@ Classes related to the different event types that wrap incoming Telegram updates
|
||||||
The :doc:`/concepts/updates` concept to learn how to listen to these events.
|
The :doc:`/concepts/updates` concept to learn how to listen to these events.
|
||||||
"""
|
"""
|
||||||
from .._impl.client.events import (
|
from .._impl.client.events import (
|
||||||
CallbackQuery,
|
ButtonCallback,
|
||||||
Event,
|
Event,
|
||||||
InlineQuery,
|
InlineQuery,
|
||||||
MessageDeleted,
|
MessageDeleted,
|
||||||
|
@ -16,7 +16,7 @@ from .._impl.client.events import (
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CallbackQuery",
|
"ButtonCallback",
|
||||||
"Event",
|
"Event",
|
||||||
"InlineQuery",
|
"InlineQuery",
|
||||||
"MessageDeleted",
|
"MessageDeleted",
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
"""
|
"""
|
||||||
Classes for the various objects the library returns.
|
Classes for the various objects the library returns.
|
||||||
"""
|
"""
|
||||||
from ._impl.client.types import (
|
from .._impl.client.types import (
|
||||||
AsyncList,
|
AsyncList,
|
||||||
|
CallbackAnswer,
|
||||||
Channel,
|
Channel,
|
||||||
Chat,
|
Chat,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -17,23 +18,27 @@ from ._impl.client.types import (
|
||||||
RecentAction,
|
RecentAction,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from ._impl.session import PackedChat, PackedType
|
from .._impl.client.types.buttons import Button, InlineButton
|
||||||
|
from .._impl.session import PackedChat, PackedType
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"InlineResult",
|
|
||||||
"AsyncList",
|
"AsyncList",
|
||||||
|
"CallbackAnswer",
|
||||||
"Channel",
|
"Channel",
|
||||||
"Chat",
|
"Chat",
|
||||||
"Dialog",
|
"Dialog",
|
||||||
"Draft",
|
"Draft",
|
||||||
"File",
|
"File",
|
||||||
"Group",
|
"Group",
|
||||||
|
"InlineResult",
|
||||||
"LoginToken",
|
"LoginToken",
|
||||||
"Message",
|
"Message",
|
||||||
"Participant",
|
"Participant",
|
||||||
"PasswordToken",
|
"PasswordToken",
|
||||||
"RecentAction",
|
"RecentAction",
|
||||||
"User",
|
"User",
|
||||||
|
"Button",
|
||||||
|
"InlineButton",
|
||||||
"PackedChat",
|
"PackedChat",
|
||||||
"PackedType",
|
"PackedType",
|
||||||
]
|
]
|
40
client/src/telethon/types/buttons.py
Normal file
40
client/src/telethon/types/buttons.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"""
|
||||||
|
Keyboard buttons.
|
||||||
|
|
||||||
|
This includes both the buttons returned by :attr:`telethon.types.Message.buttons`
|
||||||
|
and those you can define when using :meth:`telethon.Client.send_message`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.types import buttons
|
||||||
|
|
||||||
|
# As a user account, you can search for and click on buttons:
|
||||||
|
for row in message.buttons:
|
||||||
|
for button in row:
|
||||||
|
if isinstance(button, buttons.Callback) and button.data == b'data':
|
||||||
|
await button.click()
|
||||||
|
|
||||||
|
# As a bot account, you can send them:
|
||||||
|
await bot.send_message(chat, text, buttons=[
|
||||||
|
buttons.Callback('Demo', b'data')
|
||||||
|
])
|
||||||
|
"""
|
||||||
|
from .._impl.client.types.buttons import (
|
||||||
|
Callback,
|
||||||
|
RequestGeoLocation,
|
||||||
|
RequestPhone,
|
||||||
|
RequestPoll,
|
||||||
|
SwitchInline,
|
||||||
|
Text,
|
||||||
|
Url,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Callback",
|
||||||
|
"RequestGeoLocation",
|
||||||
|
"RequestPhone",
|
||||||
|
"RequestPoll",
|
||||||
|
"SwitchInline",
|
||||||
|
"Text",
|
||||||
|
"Url",
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user