[3.0] Bot API 5.1 + FSM + Utils (#525)

* Regenerate corresponding to Bot API 5.1

* Added base of FSM. Markup constructor and small refactoring

* Fix dependencies

* Fix mypy windows error

* Move StatesGroup.get_root() from meta to class

* Fixed chat and user constraints

* Update pipeline

* Remove docs pipeline

* Added GLOBAL_USER FSM strategy

* Reformat code

* Fixed Dispatcher._process_update

* Bump Bot API 5.2. Added integration with MagicFilter

* Coverage
This commit is contained in:
Alex Root Junior 2021-05-11 23:04:32 +03:00 committed by GitHub
parent a6f824a117
commit 0e72d8e65b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
265 changed files with 2921 additions and 1324 deletions

View file

@ -1 +1 @@
4.9 5.1

View file

@ -1,58 +0,0 @@
name: Build docs
on:
push:
branches:
- dev-3.x
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip poetry==1.1.4
poetry install
mkdir -p reports
- name: Bump versions
run: |
make bump
- name: Lint code
run: |
make flake8-report
make mypy-report
- name: Run tests
run: |
make test-coverage
- name: Build docs
run: |
make docs
make docs-copy-reports
- name: Build package
run: |
poetry build
mkdir -p site/simple
mv dist site/simple/aiogram
- name: Publish docs
uses: SamKirkland/FTP-Deploy-Action@2.0.0
env:
FTP_SERVER: 2038.host
FTP_USERNAME: ${{ secrets.DOCS_FTP_USERNAME }}
FTP_PASSWORD: ${{ secrets.DOCS_FTP_PASSWORD }}
LOCAL_DIR: site
REMOTE_DIR: public
ARGS: --delete --parallel=20

View file

@ -34,7 +34,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip poetry==1.1.4 python -m pip install --upgrade pip poetry
poetry install poetry install
- name: Lint code - name: Lint code

View file

@ -1,3 +1,5 @@
from magic_filter import MagicFilter
from .client import session from .client import session
from .client.bot import Bot from .client.bot import Bot
from .dispatcher import filters, handler from .dispatcher import filters, handler
@ -10,8 +12,9 @@ try:
_uvloop.install() _uvloop.install()
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
_uvloop = None pass
F = MagicFilter()
__all__ = ( __all__ = (
"__api_version__", "__api_version__",
@ -25,7 +28,8 @@ __all__ = (
"BaseMiddleware", "BaseMiddleware",
"filters", "filters",
"handler", "handler",
"F",
) )
__version__ = "3.0.0-alpha.6" __version__ = "3.0.0-alpha.6"
__api_version__ = "4.9" __api_version__ = "5.1"

View file

@ -30,12 +30,14 @@ from ..methods import (
AnswerShippingQuery, AnswerShippingQuery,
Close, Close,
CopyMessage, CopyMessage,
CreateChatInviteLink,
CreateNewStickerSet, CreateNewStickerSet,
DeleteChatPhoto, DeleteChatPhoto,
DeleteChatStickerSet, DeleteChatStickerSet,
DeleteMessage, DeleteMessage,
DeleteStickerFromSet, DeleteStickerFromSet,
DeleteWebhook, DeleteWebhook,
EditChatInviteLink,
EditMessageCaption, EditMessageCaption,
EditMessageLiveLocation, EditMessageLiveLocation,
EditMessageMedia, EditMessageMedia,
@ -61,6 +63,7 @@ from ..methods import (
PinChatMessage, PinChatMessage,
PromoteChatMember, PromoteChatMember,
RestrictChatMember, RestrictChatMember,
RevokeChatInviteLink,
SendAnimation, SendAnimation,
SendAudio, SendAudio,
SendChatAction, SendChatAction,
@ -103,6 +106,7 @@ from ..types import (
UNSET, UNSET,
BotCommand, BotCommand,
Chat, Chat,
ChatInviteLink,
ChatMember, ChatMember,
ChatPermissions, ChatPermissions,
Downloadable, Downloadable,
@ -279,6 +283,9 @@ class Bot(ContextInstanceMixin["Bot"]):
raise TypeError("file can only be of the string or Downloadable type") raise TypeError("file can only be of the string or Downloadable type")
file_ = await self.get_file(file_id) file_ = await self.get_file(file_id)
# `file_path` can be None for large files but this files can't be downloaded
# So we need to do type-cast
# https://github.com/aiogram/aiogram/pull/282/files#r394110017 # https://github.com/aiogram/aiogram/pull/282/files#r394110017
file_path = cast(str, file_.file_path) file_path = cast(str, file_.file_path)
@ -343,7 +350,7 @@ class Bot(ContextInstanceMixin["Bot"]):
:param offset: Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as :class:`aiogram.methods.get_updates.GetUpdates` is called with an *offset* higher than its *update_id*. The negative offset can be specified to retrieve updates starting from *-offset* update from the end of the updates queue. All previous updates will forgotten. :param offset: Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as :class:`aiogram.methods.get_updates.GetUpdates` is called with an *offset* higher than its *update_id*. The negative offset can be specified to retrieve updates starting from *-offset* update from the end of the updates queue. All previous updates will forgotten.
:param limit: Limits the number of updates to be retrieved. Values between 1-100 are accepted. Defaults to 100. :param limit: Limits the number of updates to be retrieved. Values between 1-100 are accepted. Defaults to 100.
:param timeout: Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only. :param timeout: Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only.
:param allowed_updates: A JSON-serialized list of the update types you want your bot to receive. For example, specify ['message', 'edited_channel_post', 'callback_query'] to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all updates regardless of type (default). If not specified, the previous setting will be used. :param allowed_updates: A JSON-serialized list of the update types you want your bot to receive. For example, specify ['message', 'edited_channel_post', 'callback_query'] to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all update types except *chat_member* (default). If not specified, the previous setting will be used.
:param request_timeout: Request timeout :param request_timeout: Request timeout
:return: An Array of Update objects is returned. :return: An Array of Update objects is returned.
""" """
@ -384,7 +391,7 @@ class Bot(ContextInstanceMixin["Bot"]):
:param certificate: Upload your public key certificate so that the root certificate in use can be checked. See our `self-signed guide <https://core.telegram.org/bots/self-signed>`_ for details. :param certificate: Upload your public key certificate so that the root certificate in use can be checked. See our `self-signed guide <https://core.telegram.org/bots/self-signed>`_ for details.
:param ip_address: The fixed IP address which will be used to send webhook requests instead of the IP address resolved through DNS :param ip_address: The fixed IP address which will be used to send webhook requests instead of the IP address resolved through DNS
:param max_connections: Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to *40*. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput. :param max_connections: Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to *40*. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput.
:param allowed_updates: A JSON-serialized list of the update types you want your bot to receive. For example, specify ['message', 'edited_channel_post', 'callback_query'] to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all updates regardless of type (default). If not specified, the previous setting will be used. :param allowed_updates: A JSON-serialized list of the update types you want your bot to receive. For example, specify ['message', 'edited_channel_post', 'callback_query'] to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all update types except *chat_member* (default). If not specified, the previous setting will be used.
:param drop_pending_updates: Pass :code:`True` to drop all pending updates :param drop_pending_updates: Pass :code:`True` to drop all pending updates
:param request_timeout: Request timeout :param request_timeout: Request timeout
:return: Returns True on success. :return: Returns True on success.
@ -539,7 +546,7 @@ class Bot(ContextInstanceMixin["Bot"]):
request_timeout: Optional[int] = None, request_timeout: Optional[int] = None,
) -> Message: ) -> Message:
""" """
Use this method to forward messages of any kind. On success, the sent :class:`aiogram.types.message.Message` is returned. Use this method to forward messages of any kind. Service messages can't be forwarded. On success, the sent :class:`aiogram.types.message.Message` is returned.
Source: https://core.telegram.org/bots/api#forwardmessage Source: https://core.telegram.org/bots/api#forwardmessage
@ -575,7 +582,7 @@ class Bot(ContextInstanceMixin["Bot"]):
request_timeout: Optional[int] = None, request_timeout: Optional[int] = None,
) -> MessageId: ) -> MessageId:
""" """
Use this method to copy messages of any kind. The method is analogous to the method :class:`aiogram.methods.forward_messages.ForwardMessages`, but the copied message doesn't have a link to the original message. Returns the :class:`aiogram.types.message_id.MessageId` of the sent message on success. Use this method to copy messages of any kind. Service messages and invoice messages can't be copied. The method is analogous to the method :class:`aiogram.methods.forward_message.ForwardMessage`, but the copied message doesn't have a link to the original message. Returns the :class:`aiogram.types.message_id.MessageId` of the sent message on success.
Source: https://core.telegram.org/bots/api#copymessage Source: https://core.telegram.org/bots/api#copymessage
@ -1314,7 +1321,7 @@ class Bot(ContextInstanceMixin["Bot"]):
Source: https://core.telegram.org/bots/api#senddice Source: https://core.telegram.org/bots/api#senddice
:param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`) :param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
:param emoji: Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '', or '🎰'. Dice can have values 1-6 for '🎲' and '🎯', values 1-5 for '🏀' and '', and values 1-64 for '🎰'. Defaults to '🎲' :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '', '🎳', or '🎰'. Dice can have values 1-6 for '🎲', '🎯' and '🎳', values 1-5 for '🏀' and '', and values 1-64 for '🎰'. Defaults to '🎲'
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound. :param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
:param reply_to_message_id: If the message is a reply, ID of the original message :param reply_to_message_id: If the message is a reply, ID of the original message
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found :param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
@ -1408,6 +1415,7 @@ class Bot(ContextInstanceMixin["Bot"]):
chat_id: Union[int, str], chat_id: Union[int, str],
user_id: int, user_id: int,
until_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None, until_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None,
revoke_messages: Optional[bool] = None,
request_timeout: Optional[int] = None, request_timeout: Optional[int] = None,
) -> bool: ) -> bool:
""" """
@ -1418,6 +1426,7 @@ class Bot(ContextInstanceMixin["Bot"]):
:param chat_id: Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@channelusername`) :param chat_id: Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@channelusername`)
:param user_id: Unique identifier of the target user :param user_id: Unique identifier of the target user
:param until_date: Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever. Applied for supergroups and channels only. :param until_date: Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever. Applied for supergroups and channels only.
:param revoke_messages: Pass :code:`True` to delete all messages from the chat for the user that is being removed. If :code:`False`, the user will be able to see messages in the group that were sent before the user was removed. Always :code:`True` for supergroups and channels.
:param request_timeout: Request timeout :param request_timeout: Request timeout
:return: In the case of supergroups and channels, the user will not be able to return to :return: In the case of supergroups and channels, the user will not be able to return to
the chat on their own using invite links, etc. Returns True on success. the chat on their own using invite links, etc. Returns True on success.
@ -1426,6 +1435,7 @@ class Bot(ContextInstanceMixin["Bot"]):
chat_id=chat_id, chat_id=chat_id,
user_id=user_id, user_id=user_id,
until_date=until_date, until_date=until_date,
revoke_messages=revoke_messages,
) )
return await self(call, request_timeout=request_timeout) return await self(call, request_timeout=request_timeout)
@ -1488,14 +1498,16 @@ class Bot(ContextInstanceMixin["Bot"]):
chat_id: Union[int, str], chat_id: Union[int, str],
user_id: int, user_id: int,
is_anonymous: Optional[bool] = None, is_anonymous: Optional[bool] = None,
can_change_info: Optional[bool] = None, can_manage_chat: Optional[bool] = None,
can_post_messages: Optional[bool] = None, can_post_messages: Optional[bool] = None,
can_edit_messages: Optional[bool] = None, can_edit_messages: Optional[bool] = None,
can_delete_messages: Optional[bool] = None, can_delete_messages: Optional[bool] = None,
can_invite_users: Optional[bool] = None, can_manage_voice_chats: Optional[bool] = None,
can_restrict_members: Optional[bool] = None, can_restrict_members: Optional[bool] = None,
can_pin_messages: Optional[bool] = None,
can_promote_members: Optional[bool] = None, can_promote_members: Optional[bool] = None,
can_change_info: Optional[bool] = None,
can_invite_users: Optional[bool] = None,
can_pin_messages: Optional[bool] = None,
request_timeout: Optional[int] = None, request_timeout: Optional[int] = None,
) -> bool: ) -> bool:
""" """
@ -1506,14 +1518,16 @@ class Bot(ContextInstanceMixin["Bot"]):
:param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`) :param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
:param user_id: Unique identifier of the target user :param user_id: Unique identifier of the target user
:param is_anonymous: Pass :code:`True`, if the administrator's presence in the chat is hidden :param is_anonymous: Pass :code:`True`, if the administrator's presence in the chat is hidden
:param can_change_info: Pass True, if the administrator can change chat title, photo and other settings :param can_manage_chat: Pass True, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege
:param can_post_messages: Pass True, if the administrator can create channel posts, channels only :param can_post_messages: Pass True, if the administrator can create channel posts, channels only
:param can_edit_messages: Pass True, if the administrator can edit messages of other users and can pin messages, channels only :param can_edit_messages: Pass True, if the administrator can edit messages of other users and can pin messages, channels only
:param can_delete_messages: Pass True, if the administrator can delete messages of other users :param can_delete_messages: Pass True, if the administrator can delete messages of other users
:param can_invite_users: Pass True, if the administrator can invite new users to the chat :param can_manage_voice_chats: Pass True, if the administrator can manage voice chats
:param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members :param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members
:param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only
:param can_promote_members: Pass True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) :param can_promote_members: Pass True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him)
:param can_change_info: Pass True, if the administrator can change chat title, photo and other settings
:param can_invite_users: Pass True, if the administrator can invite new users to the chat
:param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only
:param request_timeout: Request timeout :param request_timeout: Request timeout
:return: Returns True on success. :return: Returns True on success.
""" """
@ -1521,14 +1535,16 @@ class Bot(ContextInstanceMixin["Bot"]):
chat_id=chat_id, chat_id=chat_id,
user_id=user_id, user_id=user_id,
is_anonymous=is_anonymous, is_anonymous=is_anonymous,
can_change_info=can_change_info, can_manage_chat=can_manage_chat,
can_post_messages=can_post_messages, can_post_messages=can_post_messages,
can_edit_messages=can_edit_messages, can_edit_messages=can_edit_messages,
can_delete_messages=can_delete_messages, can_delete_messages=can_delete_messages,
can_invite_users=can_invite_users, can_manage_voice_chats=can_manage_voice_chats,
can_restrict_members=can_restrict_members, can_restrict_members=can_restrict_members,
can_pin_messages=can_pin_messages,
can_promote_members=can_promote_members, can_promote_members=can_promote_members,
can_change_info=can_change_info,
can_invite_users=can_invite_users,
can_pin_messages=can_pin_messages,
) )
return await self(call, request_timeout=request_timeout) return await self(call, request_timeout=request_timeout)
@ -1585,9 +1601,9 @@ class Bot(ContextInstanceMixin["Bot"]):
request_timeout: Optional[int] = None, request_timeout: Optional[int] = None,
) -> str: ) -> str:
""" """
Use this method to generate a new invite link for a chat; any previously generated link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the new invite link as *String* on success. Use this method to generate a new primary invite link for a chat; any previously generated primary link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the new invite link as *String* on success.
Note: Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators. If you want your bot to work with invite links, it will need to generate its own link using :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink` — after this the link will become available to the bot via the :class:`aiogram.methods.get_chat.GetChat` method. If your bot needs to generate a new invite link replacing its previous one, use :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink` again. Note: Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators. If you want your bot to work with invite links, it will need to generate its own link using :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink` or by calling the :class:`aiogram.methods.get_chat.GetChat` method. If your bot needs to generate a new primary invite link replacing its previous one, use :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink` again.
Source: https://core.telegram.org/bots/api#exportchatinvitelink Source: https://core.telegram.org/bots/api#exportchatinvitelink
@ -1600,6 +1616,81 @@ class Bot(ContextInstanceMixin["Bot"]):
) )
return await self(call, request_timeout=request_timeout) return await self(call, request_timeout=request_timeout)
async def create_chat_invite_link(
self,
chat_id: Union[int, str],
expire_date: Optional[int] = None,
member_limit: Optional[int] = None,
request_timeout: Optional[int] = None,
) -> ChatInviteLink:
"""
Use this method to create an additional invite link for a chat. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. The link can be revoked using the method :class:`aiogram.methods.revoke_chat_invite_link.RevokeChatInviteLink`. Returns the new invite link as :class:`aiogram.types.chat_invite_link.ChatInviteLink` object.
Source: https://core.telegram.org/bots/api#createchatinvitelink
:param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
:param expire_date: Point in time (Unix timestamp) when the link will expire
:param member_limit: Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999
:param request_timeout: Request timeout
:return: Returns the new invite link as ChatInviteLink object.
"""
call = CreateChatInviteLink(
chat_id=chat_id,
expire_date=expire_date,
member_limit=member_limit,
)
return await self(call, request_timeout=request_timeout)
async def edit_chat_invite_link(
self,
chat_id: Union[int, str],
invite_link: str,
expire_date: Optional[int] = None,
member_limit: Optional[int] = None,
request_timeout: Optional[int] = None,
) -> ChatInviteLink:
"""
Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the edited invite link as a :class:`aiogram.types.chat_invite_link.ChatInviteLink` object.
Source: https://core.telegram.org/bots/api#editchatinvitelink
:param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
:param invite_link: The invite link to edit
:param expire_date: Point in time (Unix timestamp) when the link will expire
:param member_limit: Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999
:param request_timeout: Request timeout
:return: Returns the edited invite link as a ChatInviteLink object.
"""
call = EditChatInviteLink(
chat_id=chat_id,
invite_link=invite_link,
expire_date=expire_date,
member_limit=member_limit,
)
return await self(call, request_timeout=request_timeout)
async def revoke_chat_invite_link(
self,
chat_id: Union[int, str],
invite_link: str,
request_timeout: Optional[int] = None,
) -> ChatInviteLink:
"""
Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the revoked invite link as :class:`aiogram.types.chat_invite_link.ChatInviteLink` object.
Source: https://core.telegram.org/bots/api#revokechatinvitelink
:param chat_id: Unique identifier of the target chat or username of the target channel (in the format :code:`@channelusername`)
:param invite_link: The invite link to revoke
:param request_timeout: Request timeout
:return: Returns the revoked invite link as ChatInviteLink object.
"""
call = RevokeChatInviteLink(
chat_id=chat_id,
invite_link=invite_link,
)
return await self(call, request_timeout=request_timeout)
async def set_chat_photo( async def set_chat_photo(
self, self,
chat_id: Union[int, str], chat_id: Union[int, str],
@ -2443,14 +2534,16 @@ class Bot(ContextInstanceMixin["Bot"]):
async def send_invoice( async def send_invoice(
self, self,
chat_id: int, chat_id: Union[int, str],
title: str, title: str,
description: str, description: str,
payload: str, payload: str,
provider_token: str, provider_token: str,
start_parameter: str,
currency: str, currency: str,
prices: List[LabeledPrice], prices: List[LabeledPrice],
max_tip_amount: Optional[int] = None,
suggested_tip_amounts: Optional[List[int]] = None,
start_parameter: Optional[str] = None,
provider_data: Optional[str] = None, provider_data: Optional[str] = None,
photo_url: Optional[str] = None, photo_url: Optional[str] = None,
photo_size: Optional[int] = None, photo_size: Optional[int] = None,
@ -2474,14 +2567,16 @@ class Bot(ContextInstanceMixin["Bot"]):
Source: https://core.telegram.org/bots/api#sendinvoice Source: https://core.telegram.org/bots/api#sendinvoice
:param chat_id: Unique identifier for the target private chat :param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
:param title: Product name, 1-32 characters :param title: Product name, 1-32 characters
:param description: Product description, 1-255 characters :param description: Product description, 1-255 characters
:param payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. :param payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes.
:param provider_token: Payments provider token, obtained via `Botfather <https://t.me/botfather>`_ :param provider_token: Payments provider token, obtained via `Botfather <https://t.me/botfather>`_
:param start_parameter: Unique deep-linking parameter that can be used to generate this invoice when used as a start parameter
:param currency: Three-letter ISO 4217 currency code, see `more on currencies <https://core.telegram.org/bots/payments#supported-currencies>`_ :param currency: Three-letter ISO 4217 currency code, see `more on currencies <https://core.telegram.org/bots/payments#supported-currencies>`_
:param prices: Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) :param prices: Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)
:param max_tip_amount: The maximum accepted amount for tips in the *smallest units* of the currency (integer, **not** float/double). For example, for a maximum tip of :code:`US$ 1.45` pass :code:`max_tip_amount = 145`. See the *exp* parameter in `currencies.json <https://core.telegram.org/bots/payments/currencies.json>`_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0
:param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the *smallest units* of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed *max_tip_amount*.
:param start_parameter: Unique deep-linking parameter. If left empty, **forwarded copies** of the sent message will have a *Pay* button, allowing multiple users to pay directly from the forwarded message, using the same invoice. If non-empty, forwarded copies of the sent message will have a *URL* button with a deep link to the bot (instead of a *Pay* button), with the value used as the start parameter
:param provider_data: A JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider. :param provider_data: A JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider.
:param photo_url: URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for. :param photo_url: URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for.
:param photo_size: Photo size :param photo_size: Photo size
@ -2507,9 +2602,11 @@ class Bot(ContextInstanceMixin["Bot"]):
description=description, description=description,
payload=payload, payload=payload,
provider_token=provider_token, provider_token=provider_token,
start_parameter=start_parameter,
currency=currency, currency=currency,
prices=prices, prices=prices,
max_tip_amount=max_tip_amount,
suggested_tip_amounts=suggested_tip_amounts,
start_parameter=start_parameter,
provider_data=provider_data, provider_data=provider_data,
photo_url=photo_url, photo_url=photo_url,
photo_size=photo_size, photo_size=photo_size,

View file

@ -13,6 +13,11 @@ from ..types import TelegramObject, Update, User
from ..utils.exceptions import TelegramAPIError from ..utils.exceptions import TelegramAPIError
from .event.bases import UNHANDLED, SkipHandler from .event.bases import UNHANDLED, SkipHandler
from .event.telegram import TelegramEventObserver from .event.telegram import TelegramEventObserver
from .fsm.context import FSMContext
from .fsm.middleware import FSMContextMiddleware
from .fsm.storage.base import BaseStorage
from .fsm.storage.memory import MemoryStorage
from .fsm.strategy import FSMStrategy
from .middlewares.error import ErrorsMiddleware from .middlewares.error import ErrorsMiddleware
from .middlewares.user_context import UserContextMiddleware from .middlewares.user_context import UserContextMiddleware
from .router import Router from .router import Router
@ -23,15 +28,36 @@ class Dispatcher(Router):
Root router Root router
""" """
def __init__(self, **kwargs: Any) -> None: def __init__(
self,
storage: Optional[BaseStorage] = None,
fsm_strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
isolate_events: bool = True,
**kwargs: Any,
) -> None:
super(Dispatcher, self).__init__(**kwargs) super(Dispatcher, self).__init__(**kwargs)
self.update = TelegramEventObserver(router=self, event_name="update") # Telegram API provides originally only one event type - Update
self.observers["update"] = self.update # For making easily interactions with events here is registered handler which helps
# to separate Update to different event types like Message, CallbackQuery and etc.
self.update = self.observers["update"] = TelegramEventObserver(
router=self, event_name="update"
)
self.update.register(self._listen_update) self.update.register(self._listen_update)
self.update.outer_middleware(UserContextMiddleware())
# Error handlers should works is out of all other functions and be registered before all other middlewares
self.update.outer_middleware(ErrorsMiddleware(self)) self.update.outer_middleware(ErrorsMiddleware(self))
# User context middleware makes small optimization for all other builtin
# middlewares via caching the user and chat instances in the event context
self.update.outer_middleware(UserContextMiddleware())
# FSM middleware should always be registered after User context middleware
# because here is used context from previous step
self.fsm = FSMContextMiddleware(
storage=storage if storage else MemoryStorage(),
strategy=fsm_strategy,
isolate_events=isolate_events,
)
self.update.outer_middleware(self.fsm)
self._running_lock = Lock() self._running_lock = Lock()
@ -150,6 +176,12 @@ class Dispatcher(Router):
elif update.poll_answer: elif update.poll_answer:
update_type = "poll_answer" update_type = "poll_answer"
event = update.poll_answer event = update.poll_answer
elif update.my_chat_member:
update_type = "my_chat_member"
event = update.my_chat_member
elif update.chat_member:
update_type = "chat_member"
event = update.chat_member
else: else:
warnings.warn( warnings.warn(
"Detected unknown update type.\n" "Detected unknown update type.\n"
@ -201,13 +233,11 @@ class Dispatcher(Router):
:param kwargs: contextual data for middlewares, filters and handlers :param kwargs: contextual data for middlewares, filters and handlers
:return: status :return: status
""" """
handled = False
try: try:
response = await self.feed_update(bot, update, **kwargs) response = await self.feed_update(bot, update, **kwargs)
handled = handled is not UNHANDLED
if call_answer and isinstance(response, TelegramMethod): if call_answer and isinstance(response, TelegramMethod):
await self._silent_call_request(bot=bot, result=response) await self._silent_call_request(bot=bot, result=response)
return handled return response is not UNHANDLED
except Exception as e: except Exception as e:
loggers.dispatcher.exception( loggers.dispatcher.exception(
@ -347,3 +377,6 @@ class Dispatcher(Router):
except (KeyboardInterrupt, SystemExit): # pragma: no cover except (KeyboardInterrupt, SystemExit): # pragma: no cover
# Allow to graceful shutdown # Allow to graceful shutdown
pass pass
def current_state(self, user_id: int, chat_id: int) -> FSMContext:
return self.fsm.get_context(user_id=user_id, chat_id=chat_id)

View file

@ -5,13 +5,15 @@ from dataclasses import dataclass, field
from functools import partial from functools import partial
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union
from magic_filter import MagicFilter
from aiogram.dispatcher.filters.base import BaseFilter from aiogram.dispatcher.filters.base import BaseFilter
from aiogram.dispatcher.handler.base import BaseHandler from aiogram.dispatcher.handler.base import BaseHandler
CallbackType = Callable[..., Awaitable[Any]] CallbackType = Callable[..., Awaitable[Any]]
SyncFilter = Callable[..., Any] SyncFilter = Callable[..., Any]
AsyncFilter = Callable[..., Awaitable[Any]] AsyncFilter = Callable[..., Awaitable[Any]]
FilterType = Union[SyncFilter, AsyncFilter, BaseFilter] FilterType = Union[SyncFilter, AsyncFilter, BaseFilter, MagicFilter]
HandlerType = Union[FilterType, Type[BaseHandler]] HandlerType = Union[FilterType, Type[BaseHandler]]
@ -47,6 +49,14 @@ class CallableMixin:
class FilterObject(CallableMixin): class FilterObject(CallableMixin):
callback: FilterType callback: FilterType
def __post_init__(self) -> None:
# TODO: Make possibility to extract and explain magic from filter object.
# Current solution is hard for debugging because the MagicFilter instance can't be extracted
if isinstance(self.callback, MagicFilter):
# MagicFilter instance is callable but generates only "CallOperation" instead of applying the filter
self.callback = self.callback.resolve
super().__post_init__()
@dataclass @dataclass
class HandlerObject(CallableMixin): class HandlerObject(CallableMixin):

View file

@ -29,5 +29,7 @@ BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = {
"pre_checkout_query": (), "pre_checkout_query": (),
"poll": (), "poll": (),
"poll_answer": (), "poll_answer": (),
"my_chat_member": (),
"chat_member": (),
"error": (ExceptionMessageFilter, ExceptionTypeFilter), "error": (ExceptionMessageFilter, ExceptionTypeFilter),
} }

View file

@ -22,7 +22,7 @@ class ContentTypesFilter(BaseFilter):
cls, value: Optional[Union[Sequence[str], str]] cls, value: Optional[Union[Sequence[str], str]]
) -> Optional[Sequence[str]]: ) -> Optional[Sequence[str]]:
if not value: if not value:
value = [ContentType.TEXT] return value
if isinstance(value, str): if isinstance(value, str):
value = [value] value = [value]
allowed_content_types = set(ContentType.all()) allowed_content_types = set(ContentType.all())

View file

View file

@ -0,0 +1,35 @@
from typing import Any, Dict, Optional
from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType
class FSMContext:
def __init__(self, storage: BaseStorage, chat_id: int, user_id: int) -> None:
self.storage = storage
self.chat_id = chat_id
self.user_id = user_id
async def set_state(self, state: StateType = None) -> None:
await self.storage.set_state(chat_id=self.chat_id, user_id=self.user_id, state=state)
async def get_state(self) -> Optional[str]:
return await self.storage.get_state(chat_id=self.chat_id, user_id=self.user_id)
async def set_data(self, data: Dict[str, Any]) -> None:
await self.storage.set_data(chat_id=self.chat_id, user_id=self.user_id, data=data)
async def get_data(self) -> Dict[str, Any]:
return await self.storage.get_data(chat_id=self.chat_id, user_id=self.user_id)
async def update_data(
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
) -> Dict[str, Any]:
if data:
kwargs.update(data)
return await self.storage.update_data(
chat_id=self.chat_id, user_id=self.user_id, data=kwargs
)
async def clear(self) -> None:
await self.set_state(state=None)
await self.set_data({})

View file

@ -0,0 +1,53 @@
from typing import Any, Awaitable, Callable, Dict, Optional
from aiogram.dispatcher.fsm.context import FSMContext
from aiogram.dispatcher.fsm.storage.base import BaseStorage
from aiogram.dispatcher.fsm.strategy import FSMStrategy, apply_strategy
from aiogram.dispatcher.middlewares.base import BaseMiddleware
from aiogram.types import Update
class FSMContextMiddleware(BaseMiddleware[Update]):
def __init__(
self,
storage: BaseStorage,
strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
isolate_events: bool = True,
) -> None:
self.storage = storage
self.strategy = strategy
self.isolate_events = isolate_events
async def __call__(
self,
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
event: Update,
data: Dict[str, Any],
) -> Any:
context = self._resolve_context(data)
data["fsm_storage"] = self.storage
if context:
data.update({"state": context, "raw_state": await context.get_state()})
if self.isolate_events:
async with self.storage.lock():
return await handler(event, data)
return await handler(event, data)
def _resolve_context(self, data: Dict[str, Any]) -> Optional[FSMContext]:
user = data.get("event_from_user")
chat = data.get("event_chat")
chat_id = chat.id if chat else None
user_id = user.id if user else None
if chat_id is None:
chat_id = user_id
if chat_id is not None and user_id is not None:
chat_id, user_id = apply_strategy(
chat_id=chat_id, user_id=user_id, strategy=self.strategy
)
return self.get_context(chat_id=chat_id, user_id=user_id)
return None
def get_context(self, chat_id: int, user_id: int) -> FSMContext:
return FSMContext(storage=self.storage, chat_id=chat_id, user_id=user_id)

View file

@ -0,0 +1,134 @@
import inspect
from typing import Any, Optional, Tuple, Type, no_type_check
from ...types import TelegramObject
class State:
"""
State object
"""
def __init__(self, state: Optional[str] = None, group_name: Optional[str] = None) -> None:
self._state = state
self._group_name = group_name
self._group: Optional[Type[StatesGroup]] = None
@property
def group(self) -> "Type[StatesGroup]":
if not self._group:
raise RuntimeError("This state is not in any group.")
return self._group
@property
def state(self) -> Optional[str]:
if self._state is None or self._state == "*":
return self._state
if self._group_name is None and self._group:
group = self._group.__full_group_name__
elif self._group_name:
group = self._group_name
else:
group = "@"
return f"{group}:{self._state}"
def set_parent(self, group: "Type[StatesGroup]") -> None:
if not issubclass(group, StatesGroup):
raise ValueError("Group must be subclass of StatesGroup")
self._group = group
def __set_name__(self, owner: "Type[StatesGroup]", name: str) -> None:
if self._state is None:
self._state = name
self.set_parent(owner)
def __str__(self) -> str:
return f"<State '{self.state or ''}'>"
__repr__ = __str__
def __call__(self, event: TelegramObject, raw_state: Optional[str] = None) -> bool:
if self.state == "*":
return True
return raw_state == self.state
class StatesGroupMeta(type):
__parent__: "Optional[Type[StatesGroup]]"
__childs__: "Tuple[Type[StatesGroup], ...]"
__states__: Tuple[State, ...]
__state_names__: Tuple[str, ...]
@no_type_check
def __new__(mcs, name, bases, namespace, **kwargs):
cls = super(StatesGroupMeta, mcs).__new__(mcs, name, bases, namespace)
states = []
childs = []
for name, arg in namespace.items():
if isinstance(arg, State):
states.append(arg)
elif inspect.isclass(arg) and issubclass(arg, StatesGroup):
childs.append(arg)
arg.__parent__ = cls
cls.__parent__ = None
cls.__childs__ = tuple(childs)
cls.__states__ = tuple(states)
cls.__state_names__ = tuple(state.state for state in states)
return cls
@property
def __full_group_name__(cls) -> str:
if cls.__parent__:
return ".".join((cls.__parent__.__full_group_name__, cls.__name__))
return cls.__name__
@property
def __all_childs__(cls) -> Tuple[Type["StatesGroup"], ...]:
result = cls.__childs__
for child in cls.__childs__:
result += child.__childs__
return result
@property
def __all_states__(cls) -> Tuple[State, ...]:
result = cls.__states__
for group in cls.__childs__:
result += group.__all_states__
return result
@property
def __all_states_names__(cls) -> Tuple[str, ...]:
return tuple(state.state for state in cls.__all_states__ if state.state)
def __contains__(cls, item: Any) -> bool:
if isinstance(item, str):
return item in cls.__all_states_names__
if isinstance(item, State):
return item in cls.__all_states__
# if isinstance(item, StatesGroup):
# return item in cls.__all_childs__
return False
def __str__(self) -> str:
return f"<StatesGroup '{self.__full_group_name__}'>"
class StatesGroup(metaclass=StatesGroupMeta):
@classmethod
def get_root(cls) -> Type["StatesGroup"]:
if cls.__parent__ is None:
return cls
return cls.__parent__.get_root()
# def __call__(cls, event: TelegramObject, raw_state: Optional[str] = None) -> bool:
# return raw_state in cls.__all_states_names__
default_state = State()
any_state = State(state="*")

View file

@ -0,0 +1,38 @@
from abc import ABC, abstractmethod
from contextlib import asynccontextmanager
from typing import Any, AsyncGenerator, Dict, Optional, Union
from aiogram.dispatcher.fsm.state import State
StateType = Optional[Union[str, State]]
class BaseStorage(ABC):
@abstractmethod
@asynccontextmanager
async def lock(self) -> AsyncGenerator[None, None]:
yield None
@abstractmethod
async def set_state(self, chat_id: int, user_id: int, state: StateType = None) -> None:
pass
@abstractmethod
async def get_state(self, chat_id: int, user_id: int) -> Optional[str]:
pass
@abstractmethod
async def set_data(self, chat_id: int, user_id: int, data: Dict[str, Any]) -> None:
pass
@abstractmethod
async def get_data(self, chat_id: int, user_id: int) -> Dict[str, Any]:
pass
async def update_data(
self, chat_id: int, user_id: int, data: Dict[str, Any]
) -> Dict[str, Any]:
current_data = await self.get_data(chat_id=chat_id, user_id=user_id)
current_data.update(data)
await self.set_data(chat_id=chat_id, user_id=user_id, data=current_data)
return current_data.copy()

View file

@ -0,0 +1,39 @@
from asyncio import Lock
from collections import defaultdict
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import Any, AsyncGenerator, DefaultDict, Dict, Optional
from aiogram.dispatcher.fsm.state import State
from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType
@dataclass
class MemoryStorageRecord:
data: Dict[str, Any] = field(default_factory=dict)
state: Optional[str] = None
class MemoryStorage(BaseStorage):
def __init__(self) -> None:
self.storage: DefaultDict[int, DefaultDict[int, MemoryStorageRecord]] = defaultdict(
lambda: defaultdict(MemoryStorageRecord)
)
self._lock = Lock()
@asynccontextmanager
async def lock(self) -> AsyncGenerator[None, None]:
async with self._lock:
yield None
async def set_state(self, chat_id: int, user_id: int, state: StateType = None) -> None:
self.storage[chat_id][user_id].state = state.state if isinstance(state, State) else state
async def get_state(self, chat_id: int, user_id: int) -> Optional[str]:
return self.storage[chat_id][user_id].state
async def set_data(self, chat_id: int, user_id: int, data: Dict[str, Any]) -> None:
self.storage[chat_id][user_id].data = data.copy()
async def get_data(self, chat_id: int, user_id: int) -> Dict[str, Any]:
return self.storage[chat_id][user_id].data.copy()

View file

@ -0,0 +1,16 @@
from enum import Enum, auto
from typing import Tuple
class FSMStrategy(Enum):
USER_IN_CHAT = auto()
CHAT = auto()
GLOBAL_USER = auto()
def apply_strategy(chat_id: int, user_id: int, strategy: FSMStrategy) -> Tuple[int, int]:
if strategy == FSMStrategy.CHAT:
return chat_id, chat_id
if strategy == FSMStrategy.GLOBAL_USER:
return user_id, user_id
return chat_id, user_id

View file

@ -1,5 +1,6 @@
from .base import BaseHandler, BaseHandlerMixin from .base import BaseHandler, BaseHandlerMixin
from .callback_query import CallbackQueryHandler from .callback_query import CallbackQueryHandler
from .chat_member import ChatMemberUpdated
from .chosen_inline_result import ChosenInlineResultHandler from .chosen_inline_result import ChosenInlineResultHandler
from .error import ErrorHandler from .error import ErrorHandler
from .inline_query import InlineQueryHandler from .inline_query import InlineQueryHandler
@ -12,6 +13,7 @@ __all__ = (
"BaseHandler", "BaseHandler",
"BaseHandlerMixin", "BaseHandlerMixin",
"CallbackQueryHandler", "CallbackQueryHandler",
"ChatMemberUpdated",
"ChosenInlineResultHandler", "ChosenInlineResultHandler",
"ErrorHandler", "ErrorHandler",
"InlineQueryHandler", "InlineQueryHandler",

View file

@ -7,17 +7,37 @@ from aiogram.types import CallbackQuery, Message, User
class CallbackQueryHandler(BaseHandler[CallbackQuery], ABC): class CallbackQueryHandler(BaseHandler[CallbackQuery], ABC):
""" """
Base class for callback query handlers There is base class for callback query handlers.
Example:
.. code-block:: python
from aiogram.handlers import CallbackQueryHandler
...
@router.callback_query()
class MyHandler(CallbackQueryHandler):
async def handle(self) -> Any: ...
""" """
@property @property
def from_user(self) -> User: def from_user(self) -> User:
"""
Is alias for `event.from_user`
"""
return self.event.from_user return self.event.from_user
@property @property
def message(self) -> Optional[Message]: def message(self) -> Optional[Message]:
"""
Is alias for `event.message`
"""
return self.event.message return self.event.message
@property @property
def callback_data(self) -> Optional[str]: def callback_data(self) -> Optional[str]:
"""
Is alias for `event.data`
"""
return self.event.data return self.event.data

View file

@ -0,0 +1,14 @@
from abc import ABC
from aiogram.dispatcher.handler import BaseHandler
from aiogram.types import ChatMemberUpdated, User
class ChatMemberHandler(BaseHandler[ChatMemberUpdated], ABC):
"""
Base class for chat member updated events
"""
@property
def from_user(self) -> User:
return self.event.from_user

View file

@ -14,6 +14,10 @@ class UserContextMiddleware(BaseMiddleware[Update]):
) -> Any: ) -> Any:
chat, user = self.resolve_event_context(event=event) chat, user = self.resolve_event_context(event=event)
with self.context(chat=chat, user=user): with self.context(chat=chat, user=user):
if user is not None:
data["event_from_user"] = user
if chat is not None:
data["event_chat"] = chat
return await handler(event, data) return await handler(event, data)
@contextmanager @contextmanager
@ -59,4 +63,8 @@ class UserContextMiddleware(BaseMiddleware[Update]):
return None, event.pre_checkout_query.from_user return None, event.pre_checkout_query.from_user
if event.poll_answer: if event.poll_answer:
return None, event.poll_answer.user return None, event.poll_answer.user
if event.my_chat_member:
return event.my_chat_member.chat, event.my_chat_member.from_user
if event.chat_member:
return event.chat_member.chat, event.chat_member.from_user
return None, None return None, None

View file

@ -51,6 +51,9 @@ class Router:
) )
self.poll = TelegramEventObserver(router=self, event_name="poll") self.poll = TelegramEventObserver(router=self, event_name="poll")
self.poll_answer = TelegramEventObserver(router=self, event_name="poll_answer") self.poll_answer = TelegramEventObserver(router=self, event_name="poll_answer")
self.my_chat_member = TelegramEventObserver(router=self, event_name="my_chat_member")
self.chat_member = TelegramEventObserver(router=self, event_name="chat_member")
self.errors = TelegramEventObserver(router=self, event_name="error") self.errors = TelegramEventObserver(router=self, event_name="error")
self.startup = EventObserver() self.startup = EventObserver()
@ -68,6 +71,8 @@ class Router:
"pre_checkout_query": self.pre_checkout_query, "pre_checkout_query": self.pre_checkout_query,
"poll": self.poll, "poll": self.poll,
"poll_answer": self.poll_answer, "poll_answer": self.poll_answer,
"my_chat_member": self.my_chat_member,
"chat_member": self.chat_member,
"error": self.errors, "error": self.errors,
} }

View file

@ -6,12 +6,14 @@ from .answer_shipping_query import AnswerShippingQuery
from .base import Request, Response, TelegramMethod from .base import Request, Response, TelegramMethod
from .close import Close from .close import Close
from .copy_message import CopyMessage from .copy_message import CopyMessage
from .create_chat_invite_link import CreateChatInviteLink
from .create_new_sticker_set import CreateNewStickerSet from .create_new_sticker_set import CreateNewStickerSet
from .delete_chat_photo import DeleteChatPhoto from .delete_chat_photo import DeleteChatPhoto
from .delete_chat_sticker_set import DeleteChatStickerSet from .delete_chat_sticker_set import DeleteChatStickerSet
from .delete_message import DeleteMessage from .delete_message import DeleteMessage
from .delete_sticker_from_set import DeleteStickerFromSet from .delete_sticker_from_set import DeleteStickerFromSet
from .delete_webhook import DeleteWebhook from .delete_webhook import DeleteWebhook
from .edit_chat_invite_link import EditChatInviteLink
from .edit_message_caption import EditMessageCaption from .edit_message_caption import EditMessageCaption
from .edit_message_live_location import EditMessageLiveLocation from .edit_message_live_location import EditMessageLiveLocation
from .edit_message_media import EditMessageMedia from .edit_message_media import EditMessageMedia
@ -37,6 +39,7 @@ from .log_out import LogOut
from .pin_chat_message import PinChatMessage from .pin_chat_message import PinChatMessage
from .promote_chat_member import PromoteChatMember from .promote_chat_member import PromoteChatMember
from .restrict_chat_member import RestrictChatMember from .restrict_chat_member import RestrictChatMember
from .revoke_chat_invite_link import RevokeChatInviteLink
from .send_animation import SendAnimation from .send_animation import SendAnimation
from .send_audio import SendAudio from .send_audio import SendAudio
from .send_chat_action import SendChatAction from .send_chat_action import SendChatAction
@ -113,6 +116,9 @@ __all__ = (
"SetChatAdministratorCustomTitle", "SetChatAdministratorCustomTitle",
"SetChatPermissions", "SetChatPermissions",
"ExportChatInviteLink", "ExportChatInviteLink",
"CreateChatInviteLink",
"EditChatInviteLink",
"RevokeChatInviteLink",
"SetChatPhoto", "SetChatPhoto",
"DeleteChatPhoto", "DeleteChatPhoto",
"SetChatTitle", "SetChatTitle",

View file

@ -19,7 +19,7 @@ if TYPE_CHECKING: # pragma: no cover
class CopyMessage(TelegramMethod[MessageId]): class CopyMessage(TelegramMethod[MessageId]):
""" """
Use this method to copy messages of any kind. The method is analogous to the method :class:`aiogram.methods.forward_messages.ForwardMessages`, but the copied message doesn't have a link to the original message. Returns the :class:`aiogram.types.message_id.MessageId` of the sent message on success. Use this method to copy messages of any kind. Service messages and invoice messages can't be copied. The method is analogous to the method :class:`aiogram.methods.forward_message.ForwardMessage`, but the copied message doesn't have a link to the original message. Returns the :class:`aiogram.types.message_id.MessageId` of the sent message on success.
Source: https://core.telegram.org/bots/api#copymessage Source: https://core.telegram.org/bots/api#copymessage
""" """

View file

@ -0,0 +1,31 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from ..types import ChatInviteLink
from .base import Request, TelegramMethod
if TYPE_CHECKING: # pragma: no cover
from ..client.bot import Bot
class CreateChatInviteLink(TelegramMethod[ChatInviteLink]):
"""
Use this method to create an additional invite link for a chat. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. The link can be revoked using the method :class:`aiogram.methods.revoke_chat_invite_link.RevokeChatInviteLink`. Returns the new invite link as :class:`aiogram.types.chat_invite_link.ChatInviteLink` object.
Source: https://core.telegram.org/bots/api#createchatinvitelink
"""
__returning__ = ChatInviteLink
chat_id: Union[int, str]
"""Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)"""
expire_date: Optional[int] = None
"""Point in time (Unix timestamp) when the link will expire"""
member_limit: Optional[int] = None
"""Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999"""
def build_request(self, bot: Bot) -> Request:
data: Dict[str, Any] = self.dict()
return Request(method="createChatInviteLink", data=data)

View file

@ -0,0 +1,33 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from ..types import ChatInviteLink
from .base import Request, TelegramMethod
if TYPE_CHECKING: # pragma: no cover
from ..client.bot import Bot
class EditChatInviteLink(TelegramMethod[ChatInviteLink]):
"""
Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the edited invite link as a :class:`aiogram.types.chat_invite_link.ChatInviteLink` object.
Source: https://core.telegram.org/bots/api#editchatinvitelink
"""
__returning__ = ChatInviteLink
chat_id: Union[int, str]
"""Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)"""
invite_link: str
"""The invite link to edit"""
expire_date: Optional[int] = None
"""Point in time (Unix timestamp) when the link will expire"""
member_limit: Optional[int] = None
"""Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999"""
def build_request(self, bot: Bot) -> Request:
data: Dict[str, Any] = self.dict()
return Request(method="editChatInviteLink", data=data)

View file

@ -10,9 +10,9 @@ if TYPE_CHECKING: # pragma: no cover
class ExportChatInviteLink(TelegramMethod[str]): class ExportChatInviteLink(TelegramMethod[str]):
""" """
Use this method to generate a new invite link for a chat; any previously generated link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the new invite link as *String* on success. Use this method to generate a new primary invite link for a chat; any previously generated primary link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the new invite link as *String* on success.
Note: Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators. If you want your bot to work with invite links, it will need to generate its own link using :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink` — after this the link will become available to the bot via the :class:`aiogram.methods.get_chat.GetChat` method. If your bot needs to generate a new invite link replacing its previous one, use :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink` again. Note: Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators. If you want your bot to work with invite links, it will need to generate its own link using :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink` or by calling the :class:`aiogram.methods.get_chat.GetChat` method. If your bot needs to generate a new primary invite link replacing its previous one, use :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink` again.
Source: https://core.telegram.org/bots/api#exportchatinvitelink Source: https://core.telegram.org/bots/api#exportchatinvitelink
""" """

View file

@ -11,7 +11,7 @@ if TYPE_CHECKING: # pragma: no cover
class ForwardMessage(TelegramMethod[Message]): class ForwardMessage(TelegramMethod[Message]):
""" """
Use this method to forward messages of any kind. On success, the sent :class:`aiogram.types.message.Message` is returned. Use this method to forward messages of any kind. Service messages can't be forwarded. On success, the sent :class:`aiogram.types.message.Message` is returned.
Source: https://core.telegram.org/bots/api#forwardmessage Source: https://core.telegram.org/bots/api#forwardmessage
""" """

View file

@ -31,7 +31,7 @@ class GetUpdates(TelegramMethod[List[Update]]):
timeout: Optional[int] = None timeout: Optional[int] = None
"""Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only.""" """Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only."""
allowed_updates: Optional[List[str]] = None allowed_updates: Optional[List[str]] = None
"""A JSON-serialized list of the update types you want your bot to receive. For example, specify ['message', 'edited_channel_post', 'callback_query'] to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all updates regardless of type (default). If not specified, the previous setting will be used.""" """A JSON-serialized list of the update types you want your bot to receive. For example, specify ['message', 'edited_channel_post', 'callback_query'] to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all update types except *chat_member* (default). If not specified, the previous setting will be used."""
def build_request(self, bot: Bot) -> Request: def build_request(self, bot: Bot) -> Request:
data: Dict[str, Any] = self.dict() data: Dict[str, Any] = self.dict()

View file

@ -24,6 +24,8 @@ class KickChatMember(TelegramMethod[bool]):
"""Unique identifier of the target user""" """Unique identifier of the target user"""
until_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None until_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None
"""Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever. Applied for supergroups and channels only.""" """Date when the user will be unbanned, unix time. If user is banned for more than 366 days or less than 30 seconds from the current time they are considered to be banned forever. Applied for supergroups and channels only."""
revoke_messages: Optional[bool] = None
"""Pass :code:`True` to delete all messages from the chat for the user that is being removed. If :code:`False`, the user will be able to see messages in the group that were sent before the user was removed. Always :code:`True` for supergroups and channels."""
def build_request(self, bot: Bot) -> Request: def build_request(self, bot: Bot) -> Request:
data: Dict[str, Any] = self.dict() data: Dict[str, Any] = self.dict()

View file

@ -23,22 +23,26 @@ class PromoteChatMember(TelegramMethod[bool]):
"""Unique identifier of the target user""" """Unique identifier of the target user"""
is_anonymous: Optional[bool] = None is_anonymous: Optional[bool] = None
"""Pass :code:`True`, if the administrator's presence in the chat is hidden""" """Pass :code:`True`, if the administrator's presence in the chat is hidden"""
can_change_info: Optional[bool] = None can_manage_chat: Optional[bool] = None
"""Pass True, if the administrator can change chat title, photo and other settings""" """Pass True, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege"""
can_post_messages: Optional[bool] = None can_post_messages: Optional[bool] = None
"""Pass True, if the administrator can create channel posts, channels only""" """Pass True, if the administrator can create channel posts, channels only"""
can_edit_messages: Optional[bool] = None can_edit_messages: Optional[bool] = None
"""Pass True, if the administrator can edit messages of other users and can pin messages, channels only""" """Pass True, if the administrator can edit messages of other users and can pin messages, channels only"""
can_delete_messages: Optional[bool] = None can_delete_messages: Optional[bool] = None
"""Pass True, if the administrator can delete messages of other users""" """Pass True, if the administrator can delete messages of other users"""
can_invite_users: Optional[bool] = None can_manage_voice_chats: Optional[bool] = None
"""Pass True, if the administrator can invite new users to the chat""" """Pass True, if the administrator can manage voice chats"""
can_restrict_members: Optional[bool] = None can_restrict_members: Optional[bool] = None
"""Pass True, if the administrator can restrict, ban or unban chat members""" """Pass True, if the administrator can restrict, ban or unban chat members"""
can_pin_messages: Optional[bool] = None
"""Pass True, if the administrator can pin messages, supergroups only"""
can_promote_members: Optional[bool] = None can_promote_members: Optional[bool] = None
"""Pass True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him)""" """Pass True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him)"""
can_change_info: Optional[bool] = None
"""Pass True, if the administrator can change chat title, photo and other settings"""
can_invite_users: Optional[bool] = None
"""Pass True, if the administrator can invite new users to the chat"""
can_pin_messages: Optional[bool] = None
"""Pass True, if the administrator can pin messages, supergroups only"""
def build_request(self, bot: Bot) -> Request: def build_request(self, bot: Bot) -> Request:
data: Dict[str, Any] = self.dict() data: Dict[str, Any] = self.dict()

View file

@ -0,0 +1,29 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, Union
from ..types import ChatInviteLink
from .base import Request, TelegramMethod
if TYPE_CHECKING: # pragma: no cover
from ..client.bot import Bot
class RevokeChatInviteLink(TelegramMethod[ChatInviteLink]):
"""
Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the revoked invite link as :class:`aiogram.types.chat_invite_link.ChatInviteLink` object.
Source: https://core.telegram.org/bots/api#revokechatinvitelink
"""
__returning__ = ChatInviteLink
chat_id: Union[int, str]
"""Unique identifier of the target chat or username of the target channel (in the format :code:`@channelusername`)"""
invite_link: str
"""The invite link to revoke"""
def build_request(self, bot: Bot) -> Request:
data: Dict[str, Any] = self.dict()
return Request(method="revokeChatInviteLink", data=data)

View file

@ -27,7 +27,7 @@ class SendDice(TelegramMethod[Message]):
chat_id: Union[int, str] chat_id: Union[int, str]
"""Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)""" """Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)"""
emoji: Optional[str] = None emoji: Optional[str] = None
"""Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '', or '🎰'. Dice can have values 1-6 for '🎲' and '🎯', values 1-5 for '🏀' and '', and values 1-64 for '🎰'. Defaults to '🎲'""" """Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '', '🎳', or '🎰'. Dice can have values 1-6 for '🎲', '🎯' and '🎳', values 1-5 for '🏀' and '', and values 1-64 for '🎰'. Defaults to '🎲'"""
disable_notification: Optional[bool] = None disable_notification: Optional[bool] = None
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.""" """Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
reply_to_message_id: Optional[int] = None reply_to_message_id: Optional[int] = None

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, List, Optional from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from ..types import InlineKeyboardMarkup, LabeledPrice, Message from ..types import InlineKeyboardMarkup, LabeledPrice, Message
from .base import Request, TelegramMethod from .base import Request, TelegramMethod
@ -18,8 +18,8 @@ class SendInvoice(TelegramMethod[Message]):
__returning__ = Message __returning__ = Message
chat_id: int chat_id: Union[int, str]
"""Unique identifier for the target private chat""" """Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)"""
title: str title: str
"""Product name, 1-32 characters""" """Product name, 1-32 characters"""
description: str description: str
@ -28,12 +28,16 @@ class SendInvoice(TelegramMethod[Message]):
"""Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes.""" """Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes."""
provider_token: str provider_token: str
"""Payments provider token, obtained via `Botfather <https://t.me/botfather>`_""" """Payments provider token, obtained via `Botfather <https://t.me/botfather>`_"""
start_parameter: str
"""Unique deep-linking parameter that can be used to generate this invoice when used as a start parameter"""
currency: str currency: str
"""Three-letter ISO 4217 currency code, see `more on currencies <https://core.telegram.org/bots/payments#supported-currencies>`_""" """Three-letter ISO 4217 currency code, see `more on currencies <https://core.telegram.org/bots/payments#supported-currencies>`_"""
prices: List[LabeledPrice] prices: List[LabeledPrice]
"""Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)""" """Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)"""
max_tip_amount: Optional[int] = None
"""The maximum accepted amount for tips in the *smallest units* of the currency (integer, **not** float/double). For example, for a maximum tip of :code:`US$ 1.45` pass :code:`max_tip_amount = 145`. See the *exp* parameter in `currencies.json <https://core.telegram.org/bots/payments/currencies.json>`_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0"""
suggested_tip_amounts: Optional[List[int]] = None
"""A JSON-serialized array of suggested amounts of tips in the *smallest units* of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed *max_tip_amount*."""
start_parameter: Optional[str] = None
"""Unique deep-linking parameter. If left empty, **forwarded copies** of the sent message will have a *Pay* button, allowing multiple users to pay directly from the forwarded message, using the same invoice. If non-empty, forwarded copies of the sent message will have a *URL* button with a deep link to the bot (instead of a *Pay* button), with the value used as the start parameter"""
provider_data: Optional[str] = None provider_data: Optional[str] = None
"""A JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider.""" """A JSON-serialized data about the invoice, which will be shared with the payment provider. A detailed description of required fields should be provided by the payment provider."""
photo_url: Optional[str] = None photo_url: Optional[str] = None

View file

@ -37,7 +37,7 @@ class SetWebhook(TelegramMethod[bool]):
max_connections: Optional[int] = None max_connections: Optional[int] = None
"""Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to *40*. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput.""" """Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to *40*. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput."""
allowed_updates: Optional[List[str]] = None allowed_updates: Optional[List[str]] = None
"""A JSON-serialized list of the update types you want your bot to receive. For example, specify ['message', 'edited_channel_post', 'callback_query'] to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all updates regardless of type (default). If not specified, the previous setting will be used.""" """A JSON-serialized list of the update types you want your bot to receive. For example, specify ['message', 'edited_channel_post', 'callback_query'] to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all update types except *chat_member* (default). If not specified, the previous setting will be used."""
drop_pending_updates: Optional[bool] = None drop_pending_updates: Optional[bool] = None
"""Pass :code:`True` to drop all pending updates""" """Pass :code:`True` to drop all pending updates"""

View file

@ -5,8 +5,10 @@ from .bot_command import BotCommand
from .callback_game import CallbackGame from .callback_game import CallbackGame
from .callback_query import CallbackQuery from .callback_query import CallbackQuery
from .chat import Chat from .chat import Chat
from .chat_invite_link import ChatInviteLink
from .chat_location import ChatLocation from .chat_location import ChatLocation
from .chat_member import ChatMember from .chat_member import ChatMember
from .chat_member_updated import ChatMemberUpdated
from .chat_permissions import ChatPermissions from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto from .chat_photo import ChatPhoto
from .chosen_inline_result import ChosenInlineResult from .chosen_inline_result import ChosenInlineResult
@ -46,6 +48,7 @@ from .inline_query_result_video import InlineQueryResultVideo
from .inline_query_result_voice import InlineQueryResultVoice from .inline_query_result_voice import InlineQueryResultVoice
from .input_contact_message_content import InputContactMessageContent from .input_contact_message_content import InputContactMessageContent
from .input_file import BufferedInputFile, FSInputFile, InputFile, URLInputFile from .input_file import BufferedInputFile, FSInputFile, InputFile, URLInputFile
from .input_invoice_message_content import InputInvoiceMessageContent
from .input_location_message_content import InputLocationMessageContent from .input_location_message_content import InputLocationMessageContent
from .input_media import InputMedia from .input_media import InputMedia
from .input_media_animation import InputMediaAnimation from .input_media_animation import InputMediaAnimation
@ -64,6 +67,7 @@ from .location import Location
from .login_url import LoginUrl from .login_url import LoginUrl
from .mask_position import MaskPosition from .mask_position import MaskPosition
from .message import ContentType, Message from .message import ContentType, Message
from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged
from .message_entity import MessageEntity from .message_entity import MessageEntity
from .message_id import MessageId from .message_id import MessageId
from .order_info import OrderInfo from .order_info import OrderInfo
@ -101,6 +105,10 @@ from .venue import Venue
from .video import Video from .video import Video
from .video_note import VideoNote from .video_note import VideoNote
from .voice import Voice from .voice import Voice
from .voice_chat_ended import VoiceChatEnded
from .voice_chat_participants_invited import VoiceChatParticipantsInvited
from .voice_chat_scheduled import VoiceChatScheduled
from .voice_chat_started import VoiceChatStarted
from .webhook_info import WebhookInfo from .webhook_info import WebhookInfo
__all__ = ( __all__ = (
@ -133,6 +141,11 @@ __all__ = (
"Location", "Location",
"Venue", "Venue",
"ProximityAlertTriggered", "ProximityAlertTriggered",
"MessageAutoDeleteTimerChanged",
"VoiceChatScheduled",
"VoiceChatStarted",
"VoiceChatEnded",
"VoiceChatParticipantsInvited",
"UserProfilePhotos", "UserProfilePhotos",
"File", "File",
"ReplyKeyboardMarkup", "ReplyKeyboardMarkup",
@ -145,7 +158,9 @@ __all__ = (
"CallbackQuery", "CallbackQuery",
"ForceReply", "ForceReply",
"ChatPhoto", "ChatPhoto",
"ChatInviteLink",
"ChatMember", "ChatMember",
"ChatMemberUpdated",
"ChatPermissions", "ChatPermissions",
"ChatLocation", "ChatLocation",
"BotCommand", "BotCommand",
@ -187,6 +202,7 @@ __all__ = (
"InputLocationMessageContent", "InputLocationMessageContent",
"InputVenueMessageContent", "InputVenueMessageContent",
"InputContactMessageContent", "InputContactMessageContent",
"InputInvoiceMessageContent",
"ChosenInlineResult", "ChosenInlineResult",
"LabeledPrice", "LabeledPrice",
"Invoice", "Invoice",

View file

@ -19,7 +19,7 @@ class Chat(TelegramObject):
""" """
id: int id: int
"""Unique identifier for this chat. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.""" """Unique identifier for this chat. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or double-precision float type are safe for storing this identifier."""
type: str type: str
"""Type of chat, can be either 'private', 'group', 'supergroup' or 'channel'""" """Type of chat, can be either 'private', 'group', 'supergroup' or 'channel'"""
title: Optional[str] = None title: Optional[str] = None
@ -37,13 +37,15 @@ class Chat(TelegramObject):
description: Optional[str] = None description: Optional[str] = None
"""*Optional*. Description, for groups, supergroups and channel chats. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. Description, for groups, supergroups and channel chats. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
invite_link: Optional[str] = None invite_link: Optional[str] = None
"""*Optional*. Chat invite link, for groups, supergroups and channel chats. Each administrator in a chat generates their own invite links, so the bot must first generate the link using :class:`aiogram.methods.export_chat_invite_link.ExportChatInviteLink`. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. Primary invite link, for groups, supergroups and channel chats. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
pinned_message: Optional[Message] = None pinned_message: Optional[Message] = None
"""*Optional*. The most recent pinned message (by sending date). Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. The most recent pinned message (by sending date). Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
permissions: Optional[ChatPermissions] = None permissions: Optional[ChatPermissions] = None
"""*Optional*. Default chat member permissions, for groups and supergroups. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. Default chat member permissions, for groups and supergroups. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
slow_mode_delay: Optional[int] = None slow_mode_delay: Optional[int] = None
"""*Optional*. For supergroups, the minimum allowed delay between consecutive messages sent by each unpriviledged user. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. For supergroups, the minimum allowed delay between consecutive messages sent by each unpriviledged user. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
message_auto_delete_time: Optional[int] = None
"""*Optional*. The time after which all messages sent to the chat will be automatically deleted; in seconds. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
sticker_set_name: Optional[str] = None sticker_set_name: Optional[str] = None
"""*Optional*. For supergroups, name of group sticker set. Returned only in :class:`aiogram.methods.get_chat.GetChat`.""" """*Optional*. For supergroups, name of group sticker set. Returned only in :class:`aiogram.methods.get_chat.GetChat`."""
can_set_sticker_set: Optional[bool] = None can_set_sticker_set: Optional[bool] = None

View file

@ -0,0 +1,29 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from .base import TelegramObject
if TYPE_CHECKING: # pragma: no cover
from .user import User
class ChatInviteLink(TelegramObject):
"""
Represents an invite link for a chat.
Source: https://core.telegram.org/bots/api#chatinvitelink
"""
invite_link: str
"""The invite link. If the link was created by another chat administrator, then the second part of the link will be replaced with ''."""
creator: User
"""Creator of the link"""
is_primary: bool
"""True, if the link is primary"""
is_revoked: bool
"""True, if the link is revoked"""
expire_date: Optional[int] = None
"""*Optional*. Point in time (Unix timestamp) when the link will expire or has been expired"""
member_limit: Optional[int] = None
"""*Optional*. Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999"""

View file

@ -28,12 +28,16 @@ class ChatMember(TelegramObject):
"""*Optional*. Owner and administrators only. True, if the user's presence in the chat is hidden""" """*Optional*. Owner and administrators only. True, if the user's presence in the chat is hidden"""
can_be_edited: Optional[bool] = None can_be_edited: Optional[bool] = None
"""*Optional*. Administrators only. True, if the bot is allowed to edit administrator privileges of that user""" """*Optional*. Administrators only. True, if the bot is allowed to edit administrator privileges of that user"""
can_manage_chat: Optional[bool] = None
"""*Optional*. Administrators only. True, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege"""
can_post_messages: Optional[bool] = None can_post_messages: Optional[bool] = None
"""*Optional*. Administrators only. True, if the administrator can post in the channel; channels only""" """*Optional*. Administrators only. True, if the administrator can post in the channel; channels only"""
can_edit_messages: Optional[bool] = None can_edit_messages: Optional[bool] = None
"""*Optional*. Administrators only. True, if the administrator can edit messages of other users and can pin messages; channels only""" """*Optional*. Administrators only. True, if the administrator can edit messages of other users and can pin messages; channels only"""
can_delete_messages: Optional[bool] = None can_delete_messages: Optional[bool] = None
"""*Optional*. Administrators only. True, if the administrator can delete messages of other users""" """*Optional*. Administrators only. True, if the administrator can delete messages of other users"""
can_manage_voice_chats: Optional[bool] = None
"""*Optional*. Administrators only. True, if the administrator can manage voice chats"""
can_restrict_members: Optional[bool] = None can_restrict_members: Optional[bool] = None
"""*Optional*. Administrators only. True, if the administrator can restrict, ban or unban chat members""" """*Optional*. Administrators only. True, if the administrator can restrict, ban or unban chat members"""
can_promote_members: Optional[bool] = None can_promote_members: Optional[bool] = None
@ -58,7 +62,6 @@ class ChatMember(TelegramObject):
"""*Optional*. Restricted only. True, if the user is allowed to add web page previews to their messages""" """*Optional*. Restricted only. True, if the user is allowed to add web page previews to their messages"""
until_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None until_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None
"""*Optional*. Restricted and kicked only. Date when restrictions will be lifted for this user; unix time""" """*Optional*. Restricted and kicked only. Date when restrictions will be lifted for this user; unix time"""
"""Restricted and kicked only. Date when restrictions will be lifted for this user; unix time"""
@property @property
def is_chat_admin(self) -> bool: def is_chat_admin(self) -> bool:

View file

@ -0,0 +1,35 @@
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Optional
from pydantic import Field
from .base import TelegramObject
if TYPE_CHECKING: # pragma: no cover
from .chat import Chat
from .chat_invite_link import ChatInviteLink
from .chat_member import ChatMember
from .user import User
class ChatMemberUpdated(TelegramObject):
"""
This object represents changes in the status of a chat member.
Source: https://core.telegram.org/bots/api#chatmemberupdated
"""
chat: Chat
"""Chat the user belongs to"""
from_user: User = Field(..., alias="from")
"""Performer of the action, which resulted in the change"""
date: datetime.datetime
"""Date the change was done in Unix time"""
old_chat_member: ChatMember
"""Previous information about the chat member"""
new_chat_member: ChatMember
"""New information about the chat member"""
invite_link: Optional[ChatInviteLink] = None
"""*Optional*. Chat invite link, which was used by the user to join the chat; for joining by invite link events only."""

View file

@ -19,6 +19,6 @@ class Contact(TelegramObject):
last_name: Optional[str] = None last_name: Optional[str] = None
"""*Optional*. Contact's last name""" """*Optional*. Contact's last name"""
user_id: Optional[int] = None user_id: Optional[int] = None
"""*Optional*. Contact's user identifier in Telegram""" """*Optional*. Contact's user identifier in Telegram. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier."""
vcard: Optional[str] = None vcard: Optional[str] = None
"""*Optional*. Additional data about the contact in the form of a `vCard <https://en.wikipedia.org/wiki/VCard>`_""" """*Optional*. Additional data about the contact in the form of a `vCard <https://en.wikipedia.org/wiki/VCard>`_"""

View file

@ -13,7 +13,7 @@ class Dice(TelegramObject):
emoji: str emoji: str
"""Emoji on which the dice throw animation is based""" """Emoji on which the dice throw animation is based"""
value: int value: int
"""Value of the dice, 1-6 for '🎲' and '🎯' base emoji, 1-5 for '🏀' and '' base emoji, 1-64 for '🎰' base emoji""" """Value of the dice, 1-6 for '🎲', '🎯' and '🎳' base emoji, 1-5 for '🏀' and '' base emoji, 1-64 for '🎰' base emoji"""
class DiceEmoji: class DiceEmoji:
@ -22,3 +22,4 @@ class DiceEmoji:
BASKETBALL = "🏀" BASKETBALL = "🏀"
FOOTBALL = "" FOOTBALL = ""
SLOT_MACHINE = "🎰" SLOT_MACHINE = "🎰"
BOWLING = "🎳"

View file

@ -28,6 +28,8 @@ class InlineQuery(TelegramObject):
"""Text of the query (up to 256 characters)""" """Text of the query (up to 256 characters)"""
offset: str offset: str
"""Offset of the results to be returned, can be controlled by the bot""" """Offset of the results to be returned, can be controlled by the bot"""
chat_type: Optional[str] = None
"""*Optional*. Type of the chat, from which the inline query was sent. Can be either 'sender' for a private chat with the inline query sender, 'private', 'group', 'supergroup', or 'channel'. The chat type should be always known for requests sent from official clients and most third-party clients, unless the request was sent from a secret chat"""
location: Optional[Location] = None location: Optional[Location] = None
"""*Optional*. Sender location, only for bots that request user location""" """*Optional*. Sender location, only for bots that request user location"""

View file

@ -28,5 +28,7 @@ class InlineQueryResult(MutableTelegramObject):
- :class:`aiogram.types.inline_query_result_video.InlineQueryResultVideo` - :class:`aiogram.types.inline_query_result_video.InlineQueryResultVideo`
- :class:`aiogram.types.inline_query_result_voice.InlineQueryResultVoice` - :class:`aiogram.types.inline_query_result_voice.InlineQueryResultVoice`
**Note:** All URLs passed in inline query results will be available to end users and therefore must be assumed to be **public**.
Source: https://core.telegram.org/bots/api#inlinequeryresult Source: https://core.telegram.org/bots/api#inlinequeryresult
""" """

View file

@ -0,0 +1,57 @@
from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional
from .input_message_content import InputMessageContent
if TYPE_CHECKING: # pragma: no cover
from .labeled_price import LabeledPrice
class InputInvoiceMessageContent(InputMessageContent):
"""
Represents the `content <https://core.telegram.org/bots/api#inputmessagecontent>`_ of an invoice message to be sent as the result of an inline query.
Source: https://core.telegram.org/bots/api#inputinvoicemessagecontent
"""
title: str
"""Product name, 1-32 characters"""
description: str
"""Product description, 1-255 characters"""
payload: str
"""Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes."""
provider_token: str
"""Payment provider token, obtained via `Botfather <https://t.me/botfather>`_"""
currency: str
"""Three-letter ISO 4217 currency code, see `more on currencies <https://core.telegram.org/bots/payments#supported-currencies>`_"""
prices: List[LabeledPrice]
"""Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)"""
max_tip_amount: Optional[int] = None
"""*Optional*. The maximum accepted amount for tips in the *smallest units* of the currency (integer, **not** float/double). For example, for a maximum tip of :code:`US$ 1.45` pass :code:`max_tip_amount = 145`. See the *exp* parameter in `currencies.json <https://core.telegram.org/bots/payments/currencies.json>`_, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0"""
suggested_tip_amounts: Optional[List[int]] = None
"""*Optional*. A JSON-serialized array of suggested amounts of tip in the *smallest units* of the currency (integer, **not** float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed *max_tip_amount*."""
provider_data: Optional[str] = None
"""*Optional*. A JSON-serialized object for data about the invoice, which will be shared with the payment provider. A detailed description of the required fields should be provided by the payment provider."""
photo_url: Optional[str] = None
"""*Optional*. URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for."""
photo_size: Optional[int] = None
"""*Optional*. Photo size"""
photo_width: Optional[int] = None
"""*Optional*. Photo width"""
photo_height: Optional[int] = None
"""*Optional*. Photo height"""
need_name: Optional[bool] = None
"""*Optional*. Pass :code:`True`, if you require the user's full name to complete the order"""
need_phone_number: Optional[bool] = None
"""*Optional*. Pass :code:`True`, if you require the user's phone number to complete the order"""
need_email: Optional[bool] = None
"""*Optional*. Pass :code:`True`, if you require the user's email address to complete the order"""
need_shipping_address: Optional[bool] = None
"""*Optional*. Pass :code:`True`, if you require the user's shipping address to complete the order"""
send_phone_number_to_provider: Optional[bool] = None
"""*Optional*. Pass :code:`True`, if user's phone number should be sent to provider"""
send_email_to_provider: Optional[bool] = None
"""*Optional*. Pass :code:`True`, if user's email address should be sent to provider"""
is_flexible: Optional[bool] = None
"""*Optional*. Pass :code:`True`, if the final price depends on the shipping method"""

View file

@ -5,12 +5,13 @@ from .base import TelegramObject
class InputMessageContent(TelegramObject): class InputMessageContent(TelegramObject):
""" """
This object represents the content of a message to be sent as a result of an inline query. Telegram clients currently support the following 4 types: This object represents the content of a message to be sent as a result of an inline query. Telegram clients currently support the following 5 types:
- :class:`aiogram.types.input_text_message_content.InputTextMessageContent` - :class:`aiogram.types.input_text_message_content.InputTextMessageContent`
- :class:`aiogram.types.input_location_message_content.InputLocationMessageContent` - :class:`aiogram.types.input_location_message_content.InputLocationMessageContent`
- :class:`aiogram.types.input_venue_message_content.InputVenueMessageContent` - :class:`aiogram.types.input_venue_message_content.InputVenueMessageContent`
- :class:`aiogram.types.input_contact_message_content.InputContactMessageContent` - :class:`aiogram.types.input_contact_message_content.InputContactMessageContent`
- :class:`aiogram.types.input_invoice_message_content.InputInvoiceMessageContent`
Source: https://core.telegram.org/bots/api#inputmessagecontent Source: https://core.telegram.org/bots/api#inputmessagecontent
""" """

View file

@ -11,6 +11,7 @@ from .base import UNSET, TelegramObject
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from ..methods import ( from ..methods import (
CopyMessage,
SendAnimation, SendAnimation,
SendAudio, SendAudio,
SendContact, SendContact,
@ -44,6 +45,7 @@ if TYPE_CHECKING: # pragma: no cover
from .invoice import Invoice from .invoice import Invoice
from .labeled_price import LabeledPrice from .labeled_price import LabeledPrice
from .location import Location from .location import Location
from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged
from .message_entity import MessageEntity from .message_entity import MessageEntity
from .passport_data import PassportData from .passport_data import PassportData
from .photo_size import PhotoSize from .photo_size import PhotoSize
@ -58,6 +60,10 @@ if TYPE_CHECKING: # pragma: no cover
from .video import Video from .video import Video
from .video_note import VideoNote from .video_note import VideoNote
from .voice import Voice from .voice import Voice
from .voice_chat_ended import VoiceChatEnded
from .voice_chat_participants_invited import VoiceChatParticipantsInvited
from .voice_chat_scheduled import VoiceChatScheduled
from .voice_chat_started import VoiceChatStarted
class Message(TelegramObject): class Message(TelegramObject):
@ -151,10 +157,12 @@ class Message(TelegramObject):
"""*Optional*. Service message: the supergroup has been created. This field can't be received in a message coming through updates, because bot can't be a member of a supergroup when it is created. It can only be found in reply_to_message if someone replies to a very first message in a directly created supergroup.""" """*Optional*. Service message: the supergroup has been created. This field can't be received in a message coming through updates, because bot can't be a member of a supergroup when it is created. It can only be found in reply_to_message if someone replies to a very first message in a directly created supergroup."""
channel_chat_created: Optional[bool] = None channel_chat_created: Optional[bool] = None
"""*Optional*. Service message: the channel has been created. This field can't be received in a message coming through updates, because bot can't be a member of a channel when it is created. It can only be found in reply_to_message if someone replies to a very first message in a channel.""" """*Optional*. Service message: the channel has been created. This field can't be received in a message coming through updates, because bot can't be a member of a channel when it is created. It can only be found in reply_to_message if someone replies to a very first message in a channel."""
message_auto_delete_timer_changed: Optional[MessageAutoDeleteTimerChanged] = None
"""*Optional*. Service message: auto-delete timer settings changed in the chat"""
migrate_to_chat_id: Optional[int] = None migrate_to_chat_id: Optional[int] = None
"""*Optional*. The group has been migrated to a supergroup with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.""" """*Optional*. The group has been migrated to a supergroup with the specified identifier. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or double-precision float type are safe for storing this identifier."""
migrate_from_chat_id: Optional[int] = None migrate_from_chat_id: Optional[int] = None
"""*Optional*. The supergroup has been migrated from a group with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.""" """*Optional*. The supergroup has been migrated from a group with the specified identifier. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or double-precision float type are safe for storing this identifier."""
pinned_message: Optional[Message] = None pinned_message: Optional[Message] = None
"""*Optional*. Specified message was pinned. Note that the Message object in this field will not contain further *reply_to_message* fields even if it is itself a reply.""" """*Optional*. Specified message was pinned. Note that the Message object in this field will not contain further *reply_to_message* fields even if it is itself a reply."""
invoice: Optional[Invoice] = None invoice: Optional[Invoice] = None
@ -167,6 +175,14 @@ class Message(TelegramObject):
"""*Optional*. Telegram Passport data""" """*Optional*. Telegram Passport data"""
proximity_alert_triggered: Optional[ProximityAlertTriggered] = None proximity_alert_triggered: Optional[ProximityAlertTriggered] = None
"""*Optional*. Service message. A user in the chat triggered another user's proximity alert while sharing Live Location.""" """*Optional*. Service message. A user in the chat triggered another user's proximity alert while sharing Live Location."""
voice_chat_scheduled: Optional[VoiceChatScheduled] = None
"""*Optional*. Service message: voice chat scheduled"""
voice_chat_started: Optional[VoiceChatStarted] = None
"""*Optional*. Service message: voice chat started"""
voice_chat_ended: Optional[VoiceChatEnded] = None
"""*Optional*. Service message: voice chat ended"""
voice_chat_participants_invited: Optional[VoiceChatParticipantsInvited] = None
"""*Optional*. Service message: new participants invited to a voice chat"""
reply_markup: Optional[InlineKeyboardMarkup] = None reply_markup: Optional[InlineKeyboardMarkup] = None
"""*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.""" """*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons."""
@ -228,6 +244,14 @@ class Message(TelegramObject):
return ContentType.POLL return ContentType.POLL
if self.dice: if self.dice:
return ContentType.DICE return ContentType.DICE
if self.message_auto_delete_timer_changed:
return ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED
if self.voice_chat_started:
return ContentType.VOICE_CHAT_STARTED
if self.voice_chat_ended:
return ContentType.VOICE_CHAT_ENDED
if self.voice_chat_participants_invited:
return ContentType.VOICE_CHAT_PARTICIPANTS_INVITED
return ContentType.UNKNOWN return ContentType.UNKNOWN
@ -1517,6 +1541,179 @@ class Message(TelegramObject):
reply_markup=reply_markup, reply_markup=reply_markup,
) )
def send_copy(
self: Message,
chat_id: Union[str, int],
disable_notification: Optional[bool] = None,
reply_to_message_id: Optional[int] = None,
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None,
) -> Union[
SendAnimation,
SendAudio,
SendContact,
SendDocument,
SendLocation,
SendMessage,
SendPhoto,
SendPoll,
SendDice,
SendSticker,
SendVenue,
SendVideo,
SendVideoNote,
SendVoice,
]:
"""
Send copy of message.
Is similar to :meth:`aiogram.client.bot.Bot.copy_message`
but returning the sent message instead of :class:`aiogram.types.message_id.MessageId`
.. note::
This method don't use the API method named `copyMessage` and
historically implemented before the similar method is added to API
:param chat_id:
:param disable_notification:
:param reply_to_message_id:
:param reply_markup:
:return:
"""
from ..methods import (
SendAnimation,
SendAudio,
SendContact,
SendDice,
SendDocument,
SendLocation,
SendMessage,
SendPhoto,
SendPoll,
SendSticker,
SendVenue,
SendVideo,
SendVideoNote,
SendVoice,
)
kwargs = {
"chat_id": chat_id,
"reply_markup": reply_markup or self.reply_markup,
"disable_notification": disable_notification,
"reply_to_message_id": reply_to_message_id,
}
text = self.text or self.caption
entities = self.entities or self.caption_entities
if self.text:
return SendMessage(text=text, entities=entities, **kwargs)
elif self.audio:
return SendAudio(
audio=self.audio.file_id,
caption=text,
title=self.audio.title,
performer=self.audio.performer,
duration=self.audio.duration,
caption_entities=entities,
**kwargs,
)
elif self.animation:
return SendAnimation(
animation=self.animation.file_id, caption=text, caption_entities=entities, **kwargs
)
elif self.document:
return SendDocument(
document=self.document.file_id, caption=text, caption_entities=entities, **kwargs
)
elif self.photo:
return SendPhoto(
photo=self.photo[-1].file_id, caption=text, caption_entities=entities, **kwargs
)
elif self.sticker:
return SendSticker(sticker=self.sticker.file_id, **kwargs)
elif self.video:
return SendVideo(
video=self.video.file_id, caption=text, caption_entities=entities, **kwargs
)
elif self.video_note:
return SendVideoNote(video_note=self.video_note.file_id, **kwargs)
elif self.voice:
return SendVoice(voice=self.voice.file_id, **kwargs)
elif self.contact:
return SendContact(
phone_number=self.contact.phone_number,
first_name=self.contact.first_name,
last_name=self.contact.last_name,
vcard=self.contact.vcard,
**kwargs,
)
elif self.venue:
return SendVenue(
latitude=self.venue.location.latitude,
longitude=self.venue.location.longitude,
title=self.venue.title,
address=self.venue.address,
foursquare_id=self.venue.foursquare_id,
foursquare_type=self.venue.foursquare_type,
**kwargs,
)
elif self.location:
return SendLocation(
latitude=self.location.latitude, longitude=self.location.longitude, **kwargs
)
elif self.poll:
return SendPoll(
question=self.poll.question,
options=[option.text for option in self.poll.options],
**kwargs,
)
elif self.dice: # Dice value can't be controlled
return SendDice(**kwargs)
else:
raise TypeError("This type of message can't be copied.")
def copy_to(
self,
chat_id: Union[int, str],
caption: Optional[str] = None,
parse_mode: Optional[str] = None,
caption_entities: Optional[List[MessageEntity]] = None,
disable_notification: Optional[bool] = None,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: Optional[bool] = None,
reply_markup: Optional[
Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply]
] = None,
) -> CopyMessage:
"""
Copy message
:param chat_id:
:param caption:
:param parse_mode:
:param caption_entities:
:param disable_notification:
:param reply_to_message_id:
:param allow_sending_without_reply:
:param reply_markup:
:return:
"""
from ..methods import CopyMessage
return CopyMessage(
chat_id=chat_id,
from_chat_id=self.chat.id,
message_id=self.message_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id,
allow_sending_without_reply=allow_sending_without_reply,
reply_markup=reply_markup,
)
class ContentType(helper.Helper): class ContentType(helper.Helper):
mode = helper.HelperMode.snake_case mode = helper.HelperMode.snake_case
@ -1549,6 +1746,10 @@ class ContentType(helper.Helper):
PASSPORT_DATA = helper.Item() # passport_data PASSPORT_DATA = helper.Item() # passport_data
POLL = helper.Item() # poll POLL = helper.Item() # poll
DICE = helper.Item() # dice DICE = helper.Item() # dice
MESSAGE_AUTO_DELETE_TIMER_CHANGED = helper.Item() # message_auto_delete_timer_changed
VOICE_CHAT_STARTED = helper.Item() # voice_chat_started
VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended
VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited
UNKNOWN = helper.Item() # unknown UNKNOWN = helper.Item() # unknown
ANY = helper.Item() # any ANY = helper.Item() # any

View file

@ -0,0 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .base import TelegramObject
if TYPE_CHECKING: # pragma: no cover
pass
class MessageAutoDeleteTimerChanged(TelegramObject):
"""
This object represents a service message about a change in auto-delete timer settings.
Source: https://core.telegram.org/bots/api#messageautodeletetimerchanged
"""
message_auto_delete_time: int
"""New auto-delete time for messages in the chat"""

View file

@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from ..utils.text_decorations import add_surrogates, remove_surrogates
from .base import MutableTelegramObject from .base import MutableTelegramObject
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@ -27,3 +29,16 @@ class MessageEntity(MutableTelegramObject):
"""*Optional*. For 'text_mention' only, the mentioned user""" """*Optional*. For 'text_mention' only, the mentioned user"""
language: Optional[str] = None language: Optional[str] = None
"""*Optional*. For 'pre' only, the programming language of the entity text""" """*Optional*. For 'pre' only, the programming language of the entity text"""
def extract(self, text: str) -> str:
return remove_surrogates(
add_surrogates(text)[self.offset * 2 : (self.offset + self.length) * 2]
)
def get_text(self, text: str) -> str:
warnings.warn(
"Method `MessageEntity.get_text(...)` deprecated and will be removed in 3.2.\n"
" Use `MessageEntity.extract(...)` instead.",
DeprecationWarning,
)
return self.extract(text=text)

View file

@ -13,6 +13,6 @@ class ResponseParameters(TelegramObject):
""" """
migrate_to_chat_id: Optional[int] = None migrate_to_chat_id: Optional[int] = None
"""*Optional*. The group has been migrated to a supergroup with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier.""" """*Optional*. The group has been migrated to a supergroup with the specified identifier. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or double-precision float type are safe for storing this identifier."""
retry_after: Optional[int] = None retry_after: Optional[int] = None
"""*Optional*. In case of exceeding flood control, the number of seconds left to wait before the request can be repeated""" """*Optional*. In case of exceeding flood control, the number of seconds left to wait before the request can be repeated"""

View file

@ -6,6 +6,7 @@ from .base import TelegramObject
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from .callback_query import CallbackQuery from .callback_query import CallbackQuery
from .chat_member_updated import ChatMemberUpdated
from .chosen_inline_result import ChosenInlineResult from .chosen_inline_result import ChosenInlineResult
from .inline_query import InlineQuery from .inline_query import InlineQuery
from .message import Message from .message import Message
@ -48,3 +49,7 @@ class Update(TelegramObject):
"""*Optional*. New poll state. Bots receive only updates about stopped polls and polls, which are sent by the bot""" """*Optional*. New poll state. Bots receive only updates about stopped polls and polls, which are sent by the bot"""
poll_answer: Optional[PollAnswer] = None poll_answer: Optional[PollAnswer] = None
"""*Optional*. A user changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself.""" """*Optional*. A user changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself."""
my_chat_member: Optional[ChatMemberUpdated] = None
"""*Optional*. The bot's chat member status was updated in a chat. For private chats, this update is received only when the bot is blocked or unblocked by the user."""
chat_member: Optional[ChatMemberUpdated] = None
"""*Optional*. A chat member's status was updated in a chat. The bot must be an administrator in the chat and must explicitly specify 'chat_member' in the list of *allowed_updates* to receive these updates."""

View file

@ -13,7 +13,7 @@ class User(TelegramObject):
""" """
id: int id: int
"""Unique identifier for this user or bot""" """Unique identifier for this user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier."""
is_bot: bool is_bot: bool
"""True, if this user is a bot""" """True, if this user is a bot"""
first_name: str first_name: str

View file

@ -0,0 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .base import TelegramObject
if TYPE_CHECKING: # pragma: no cover
pass
class VoiceChatEnded(TelegramObject):
"""
This object represents a service message about a voice chat ended in the chat.
Source: https://core.telegram.org/bots/api#voicechatended
"""
duration: int
"""Voice chat duration; in seconds"""

View file

@ -0,0 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional
from .base import TelegramObject
if TYPE_CHECKING: # pragma: no cover
from .user import User
class VoiceChatParticipantsInvited(TelegramObject):
"""
This object represents a service message about new members invited to a voice chat.
Source: https://core.telegram.org/bots/api#voicechatparticipantsinvited
"""
users: Optional[List[User]] = None
"""*Optional*. New members that were invited to the voice chat"""

View file

@ -0,0 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .base import TelegramObject
if TYPE_CHECKING: # pragma: no cover
pass
class VoiceChatScheduled(TelegramObject):
"""
This object represents a service message about a voice chat scheduled in the chat.
Source: https://core.telegram.org/bots/api#voicechatscheduled
"""
start_date: int
"""Point in time (Unix timestamp) when the voice chat is supposed to be started by a chat administrator"""

View file

@ -0,0 +1,16 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .base import TelegramObject
if TYPE_CHECKING: # pragma: no cover
pass
class VoiceChatStarted(TelegramObject):
"""
This object represents a service message about a voice chat started in the chat. Currently holds no information.
Source: https://core.telegram.org/bots/api#voicechatstarted
"""

View file

@ -27,4 +27,4 @@ class WebhookInfo(TelegramObject):
max_connections: Optional[int] = None max_connections: Optional[int] = None
"""*Optional*. Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery""" """*Optional*. Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery"""
allowed_updates: Optional[List[str]] = None allowed_updates: Optional[List[str]] = None
"""*Optional*. A list of update types the bot is subscribed to. Defaults to all update types""" """*Optional*. A list of update types the bot is subscribed to. Defaults to all update types except *chat_member*"""

View file

View file

@ -0,0 +1,48 @@
from abc import ABC, abstractmethod
from collections import Generator
from typing import Dict, List
from aiogram.utils.help.record import CommandRecord
class BaseHelpBackend(ABC):
@abstractmethod
def add(self, record: CommandRecord) -> None:
pass
@abstractmethod
def search(self, value: str) -> CommandRecord:
pass
@abstractmethod
def all(self) -> Generator[CommandRecord, None, None]:
pass
def __getitem__(self, item: str) -> CommandRecord:
return self.search(item)
def __iter__(self) -> Generator[CommandRecord, None, None]:
return self.all()
class MappingBackend(BaseHelpBackend):
def __init__(self, search_empty_prefix: bool = True) -> None:
self._records: List[CommandRecord] = []
self._mapping: Dict[str, CommandRecord] = {}
self.search_empty_prefix = search_empty_prefix
def search(self, value: str) -> CommandRecord:
return self._mapping[value]
def add(self, record: CommandRecord) -> None:
new_records = {}
for key in record.as_keys(with_empty_prefix=self.search_empty_prefix):
if key in self._mapping:
raise ValueError(f"Key '{key}' is already indexed")
new_records[key] = record
self._mapping.update(new_records)
self._records.append(record)
self._records.sort(key=lambda rec: (rec.priority, rec.commands[0]))
def all(self) -> Generator[CommandRecord, None, None]:
yield from self._records

View file

@ -0,0 +1,113 @@
from typing import Any, Optional, Tuple
from aiogram import Bot, Router
from aiogram.dispatcher.filters import Command, CommandObject
from aiogram.types import BotCommand, Message
from aiogram.utils.help.engine import BaseHelpBackend, MappingBackend
from aiogram.utils.help.record import DEFAULT_PREFIXES, CommandRecord
from aiogram.utils.help.render import BaseHelpRenderer, SimpleRenderer
class HelpManager:
def __init__(
self,
backend: Optional[BaseHelpBackend] = None,
renderer: Optional[BaseHelpRenderer] = None,
) -> None:
if backend is None:
backend = MappingBackend()
if renderer is None:
renderer = SimpleRenderer()
self._backend = backend
self._renderer = renderer
def add(
self,
*commands: str,
help: str,
description: Optional[str] = None,
prefix: str = DEFAULT_PREFIXES,
ignore_case: bool = False,
ignore_mention: bool = False,
priority: int = 0,
) -> CommandRecord:
record = CommandRecord(
commands=commands,
help=help,
description=description,
prefix=prefix,
ignore_case=ignore_case,
ignore_mention=ignore_mention,
priority=priority,
)
self._backend.add(record)
return record
def command(
self,
*commands: str,
help: str,
description: Optional[str] = None,
prefix: str = DEFAULT_PREFIXES,
ignore_case: bool = False,
ignore_mention: bool = False,
priority: int = 0,
) -> Command:
record = self.add(
*commands,
help=help,
description=description,
prefix=prefix,
ignore_case=ignore_case,
ignore_mention=ignore_mention,
priority=priority,
)
return record.as_filter()
def mount_help(
self,
router: Router,
*commands: str,
prefix: str = "/",
help: str = "Help",
description: str = "Show help for the commands\n"
"Also you can use '/help command' for get help for specific command",
as_reply: bool = True,
filters: Tuple[Any, ...] = (),
**kw_filters: Any,
) -> Any:
if not commands:
commands = ("help",)
help_filter = self.command(*commands, prefix=prefix, help=help, description=description)
async def handle(message: Message, command: CommandObject, **kwargs: Any) -> Any:
return await self._handle_help(
message=message, command=command, as_reply=as_reply, **kwargs
)
return router.message.register(handle, help_filter, *filters, **kw_filters)
async def _handle_help(
self,
message: Message,
bot: Bot,
command: CommandObject,
as_reply: bool = True,
**kwargs: Any,
) -> Any:
lines = self._renderer.render(backend=self._backend, command=command, **kwargs)
text = "\n".join(line or "" for line in lines)
return await bot.send_message(
chat_id=message.chat.id,
text=text,
reply_to_message_id=message.message_id if as_reply else None,
)
async def set_bot_commands(self, bot: Bot) -> bool:
return await bot.set_my_commands(
commands=[
BotCommand(command=record.commands[0], description=record.help)
for record in self._backend
if "/" in record.prefix
]
)

View file

@ -0,0 +1,33 @@
from dataclasses import dataclass
from itertools import product
from typing import Generator, Optional, Sequence
from aiogram.dispatcher.filters import Command
DEFAULT_PREFIXES = "/"
@dataclass
class CommandRecord:
commands: Sequence[str]
help: str
description: Optional[str] = None
prefix: str = DEFAULT_PREFIXES
ignore_case: bool = False
ignore_mention: bool = False
priority: int = 0
def as_filter(self) -> Command:
return Command(commands=self.commands, commands_prefix=self.prefix)
def as_keys(self, with_empty_prefix: bool = False) -> Generator[str, None, None]:
for command in self.commands:
yield command
for prefix in self.prefix:
yield f"{prefix}{command}"
def as_command(self) -> str:
return f"{self.prefix[0]}{self.commands[0]}"
def as_aliases(self) -> str:
return ", ".join(f"{p}{c}" for c, p in product(self.commands, self.prefix))

View file

@ -0,0 +1,64 @@
from abc import ABC, abstractmethod
from typing import Any, Generator, Optional
from aiogram.dispatcher.filters import CommandObject
from aiogram.utils.help.engine import BaseHelpBackend
class BaseHelpRenderer(ABC):
@abstractmethod
def render(
self, backend: BaseHelpBackend, command: CommandObject, **kwargs: Any
) -> Generator[Optional[str], None, None]:
pass
class SimpleRenderer(BaseHelpRenderer):
def __init__(
self,
help_title: str = "Commands list:",
help_footer: str = "",
aliases_line: str = "Aliases",
command_title: str = "Help for command:",
unknown_command: str = "Command not found",
):
self.help_title = help_title
self.help_footer = help_footer
self.aliases_line = aliases_line
self.command_title = command_title
self.unknown_command = unknown_command
def render_help(self, backend: BaseHelpBackend) -> Generator[Optional[str], None, None]:
yield self.help_title
for command in backend:
yield f"{command.prefix[0]}{command.commands[0]} - {command.help}"
if self.help_footer:
yield None
yield self.help_footer
def render_command_help(
self, backend: BaseHelpBackend, target: str
) -> Generator[Optional[str], None, None]:
try:
record = backend[target]
except KeyError:
yield f"{self.command_title} {target}"
yield self.unknown_command
return
yield f"{self.command_title} {record.as_command()}"
if len(record.commands) > 1 or len(record.prefix) > 1:
yield f"{self.aliases_line}: {record.as_aliases()}"
yield record.help
yield None
yield record.description
def render(
self, backend: BaseHelpBackend, command: CommandObject, **kwargs: Any
) -> Generator[Optional[str], None, None]:
if command.args:
yield from self.render_command_help(backend=backend, target=command.args)
else:
yield from self.render_help(backend=backend)

224
aiogram/utils/markup.py Normal file
View file

@ -0,0 +1,224 @@
from itertools import chain
from itertools import cycle as repeat_all
from typing import Any, Generator, Generic, Iterable, List, Optional, Type, TypeVar
from aiogram.types import InlineKeyboardButton, KeyboardButton
ButtonType = TypeVar("ButtonType", InlineKeyboardButton, KeyboardButton)
T = TypeVar("T")
MAX_WIDTH = 8
MIN_WIDTH = 1
MAX_BUTTONS = 100
class MarkupConstructor(Generic[ButtonType]):
def __init__(
self, button_type: Type[ButtonType], markup: Optional[List[List[ButtonType]]] = None
) -> None:
if not issubclass(button_type, (InlineKeyboardButton, KeyboardButton)):
raise ValueError(f"Button type {button_type} are not allowed here")
self._button_type: Type[ButtonType] = button_type
if markup:
self._validate_markup(markup)
else:
markup = []
self._markup: List[List[ButtonType]] = markup
@property
def buttons(self) -> Generator[ButtonType, None, None]:
"""
Get flatten set of all buttons
:return:
"""
yield from chain.from_iterable(self.export())
def _validate_button(self, button: ButtonType) -> bool:
"""
Check that button item has correct type
:param button:
:return:
"""
allowed = self._button_type
if not isinstance(button, allowed):
raise ValueError(
f"{button!r} should be type {allowed.__name__!r} not {type(button).__name__!r}"
)
return True
def _validate_buttons(self, *buttons: ButtonType) -> bool:
"""
Check that all passed button has correct type
:param buttons:
:return:
"""
return all(map(self._validate_button, buttons))
def _validate_row(self, row: List[ButtonType]) -> bool:
"""
Check that row of buttons are correct
Row can be only list of allowed button types and has length 0 <= n <= 8
:param row:
:return:
"""
if not isinstance(row, list):
raise ValueError(
f"Row {row!r} should be type 'List[{self._button_type.__name__}]' not type {type(row).__name__}"
)
if len(row) > MAX_WIDTH:
raise ValueError(f"Row {row!r} is too long (MAX_WIDTH={MAX_WIDTH})")
self._validate_buttons(*row)
return True
def _validate_markup(self, markup: List[List[ButtonType]]) -> bool:
"""
Check that passed markup has correct data structure
Markup is list of lists of buttons
:param markup:
:return:
"""
count = 0
if not isinstance(markup, list):
raise ValueError(
f"Markup should be type 'List[List[{self._button_type.__name__}]]' not type {type(markup).__name__!r}"
)
for row in markup:
self._validate_row(row)
count += len(row)
if count > MAX_BUTTONS:
raise ValueError(f"Too much buttons detected Max allowed count - {MAX_BUTTONS}")
return True
def _validate_size(self, size: Any) -> int:
"""
Validate that passed size is legit
:param size:
:return:
"""
if not isinstance(size, int):
raise ValueError("Only int sizes are allowed")
if size not in range(MIN_WIDTH, MAX_WIDTH + 1):
raise ValueError(f"Row size {size} are not allowed")
return size
def copy(self: "MarkupConstructor[ButtonType]") -> "MarkupConstructor[ButtonType]":
"""
Make full copy of current constructor with markup
:return:
"""
return self.__class__(self._button_type, markup=self.export())
def export(self) -> List[List[ButtonType]]:
"""
Export configured markup as list of lists of buttons
.. code-block:: python
>>> constructor = MarkupConstructor(button_type=InlineKeyboardButton)
>>> ... # Add buttons to constructor
>>> markup = InlineKeyboardMarkup(inline_keyboard=constructor.export())
:return:
"""
return self._markup.copy()
def add(self, *buttons: ButtonType) -> "MarkupConstructor[ButtonType]":
"""
Add one or many buttons to markup.
:param buttons:
:return:
"""
self._validate_buttons(*buttons)
markup = self.export()
# Try to add new buttons to the end of last row if it possible
if markup and len(markup[-1]) < MAX_WIDTH:
last_row = markup[-1]
pos = MAX_WIDTH - len(last_row)
head, buttons = buttons[:pos], buttons[pos:]
last_row.extend(head)
# Separate buttons to exclusive rows with max possible row width
while buttons:
row, buttons = buttons[:MAX_WIDTH], buttons[MAX_WIDTH:]
markup.append(list(row))
self._markup = markup
return self
def row(self, *buttons: ButtonType, width: int = MAX_WIDTH) -> "MarkupConstructor[ButtonType]":
"""
Add row to markup
When too much buttons is passed it will be separated to many rows
:param buttons:
:param width:
:return:
"""
self._validate_size(width)
self._validate_buttons(*buttons)
self._markup.extend(
list(buttons[pos : pos + width]) for pos in range(0, len(buttons), width)
)
return self
def adjust(self, *sizes: int, repeat: bool = False) -> "MarkupConstructor[ButtonType]":
"""
Adjust previously added buttons to specific row sizes.
By default when the sum of passed sizes is lower than buttons count the last
one size will be used for tail of the markup.
If repeat=True is passed - all sizes will be cycled when available more buttons count than all sizes
:param sizes:
:param repeat:
:return:
"""
if not sizes:
sizes = (MAX_WIDTH,)
validated_sizes = map(self._validate_size, sizes)
sizes_iter = repeat_all(validated_sizes) if repeat else repeat_last(validated_sizes)
size = next(sizes_iter)
markup = []
row: List[ButtonType] = []
for button in self.buttons:
if len(row) >= size:
markup.append(row)
size = next(sizes_iter)
row = []
row.append(button)
if row:
markup.append(row)
self._markup = markup
return self
def button(self, **kwargs: Any) -> "MarkupConstructor[ButtonType]":
button = self._button_type(**kwargs)
return self.add(button)
def repeat_last(items: Iterable[T]) -> Generator[T, None, None]:
items_iter = iter(items)
try:
value = next(items_iter)
except StopIteration:
return
yield value
finished = False
while True:
if not finished:
try:
value = next(items_iter)
except StopIteration:
finished = True
yield value

View file

@ -17,6 +17,14 @@ __all__ = (
) )
def add_surrogates(text: str) -> bytes:
return text.encode("utf-16-le")
def remove_surrogates(text: bytes) -> str:
return text.decode("utf-16-le")
class TextDecoration(ABC): class TextDecoration(ABC):
def apply_entity(self, entity: MessageEntity, text: str) -> str: def apply_entity(self, entity: MessageEntity, text: str) -> str:
""" """
@ -57,7 +65,7 @@ class TextDecoration(ABC):
""" """
result = "".join( result = "".join(
self._unparse_entities( self._unparse_entities(
self._add_surrogates(text), add_surrogates(text),
sorted(entities, key=lambda item: item.offset) if entities else [], sorted(entities, key=lambda item: item.offset) if entities else [],
) )
) )
@ -78,7 +86,7 @@ class TextDecoration(ABC):
if entity.offset * 2 < offset: if entity.offset * 2 < offset:
continue continue
if entity.offset * 2 > offset: if entity.offset * 2 > offset:
yield self.quote(self._remove_surrogates(text[offset : entity.offset * 2])) yield self.quote(remove_surrogates(text[offset : entity.offset * 2]))
start = entity.offset * 2 start = entity.offset * 2
offset = entity.offset * 2 + entity.length * 2 offset = entity.offset * 2 + entity.length * 2
@ -91,15 +99,7 @@ class TextDecoration(ABC):
) )
if offset < length: if offset < length:
yield self.quote(self._remove_surrogates(text[offset:length])) yield self.quote(remove_surrogates(text[offset:length]))
@staticmethod
def _add_surrogates(text: str) -> bytes:
return text.encode("utf-16-le")
@staticmethod
def _remove_surrogates(text: bytes) -> str:
return text.decode("utf-16-le")
@abstractmethod @abstractmethod
def link(self, value: str, link: str) -> str: # pragma: no cover def link(self, value: str, link: str) -> str: # pragma: no cover

View file

@ -1 +1 @@
4.9 5.1

View file

@ -1 +1 @@
3.0.0a6 3.0.0a7

View file

@ -0,0 +1,51 @@
####################
createChatInviteLink
####################
Returns: :obj:`ChatInviteLink`
.. automodule:: aiogram.methods.create_chat_invite_link
:members:
:member-order: bysource
:undoc-members: True
Usage
=====
As bot method
-------------
.. code-block::
result: ChatInviteLink = await bot.create_chat_invite_link(...)
Method as object
----------------
Imports:
- :code:`from aiogram.methods.create_chat_invite_link import CreateChatInviteLink`
- alias: :code:`from aiogram.methods import CreateChatInviteLink`
In handlers with current bot
----------------------------
.. code-block:: python
result: ChatInviteLink = await CreateChatInviteLink(...)
With specific bot
~~~~~~~~~~~~~~~~~
.. code-block:: python
result: ChatInviteLink = await bot(CreateChatInviteLink(...))
As reply into Webhook in handler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
return CreateChatInviteLink(...)

View file

@ -0,0 +1,51 @@
##################
editChatInviteLink
##################
Returns: :obj:`ChatInviteLink`
.. automodule:: aiogram.methods.edit_chat_invite_link
:members:
:member-order: bysource
:undoc-members: True
Usage
=====
As bot method
-------------
.. code-block::
result: ChatInviteLink = await bot.edit_chat_invite_link(...)
Method as object
----------------
Imports:
- :code:`from aiogram.methods.edit_chat_invite_link import EditChatInviteLink`
- alias: :code:`from aiogram.methods import EditChatInviteLink`
In handlers with current bot
----------------------------
.. code-block:: python
result: ChatInviteLink = await EditChatInviteLink(...)
With specific bot
~~~~~~~~~~~~~~~~~
.. code-block:: python
result: ChatInviteLink = await bot(EditChatInviteLink(...))
As reply into Webhook in handler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
return EditChatInviteLink(...)

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: Chat = await bot(GetChat(...)) result: Chat = await bot(GetChat(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: List[ChatMember] = await bot(GetChatAdministrators(...)) result: List[ChatMember] = await bot(GetChatAdministrators(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: ChatMember = await bot(GetChatMember(...)) result: ChatMember = await bot(GetChatMember(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: int = await bot(GetChatMembersCount(...)) result: int = await bot(GetChatMembersCount(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: File = await bot(GetFile(...)) result: File = await bot(GetFile(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: List[GameHighScore] = await bot(GetGameHighScores(...)) result: List[GameHighScore] = await bot(GetGameHighScores(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: User = await bot(GetMe(...)) result: User = await bot(GetMe(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: List[BotCommand] = await bot(GetMyCommands(...)) result: List[BotCommand] = await bot(GetMyCommands(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: StickerSet = await bot(GetStickerSet(...)) result: StickerSet = await bot(GetStickerSet(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: List[Update] = await bot(GetUpdates(...)) result: List[Update] = await bot(GetUpdates(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: UserProfilePhotos = await bot(GetUserProfilePhotos(...)) result: UserProfilePhotos = await bot(GetUserProfilePhotos(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: WebhookInfo = await bot(GetWebhookInfo(...)) result: WebhookInfo = await bot(GetWebhookInfo(...))

View file

@ -55,6 +55,9 @@ Available methods
set_chat_administrator_custom_title set_chat_administrator_custom_title
set_chat_permissions set_chat_permissions
export_chat_invite_link export_chat_invite_link
create_chat_invite_link
edit_chat_invite_link
revoke_chat_invite_link
set_chat_photo set_chat_photo
delete_chat_photo delete_chat_photo
set_chat_title set_chat_title
@ -136,4 +139,3 @@ Games
send_game send_game
set_game_score set_game_score
get_game_high_scores get_game_high_scores

View file

@ -0,0 +1,51 @@
####################
revokeChatInviteLink
####################
Returns: :obj:`ChatInviteLink`
.. automodule:: aiogram.methods.revoke_chat_invite_link
:members:
:member-order: bysource
:undoc-members: True
Usage
=====
As bot method
-------------
.. code-block::
result: ChatInviteLink = await bot.revoke_chat_invite_link(...)
Method as object
----------------
Imports:
- :code:`from aiogram.methods.revoke_chat_invite_link import RevokeChatInviteLink`
- alias: :code:`from aiogram.methods import RevokeChatInviteLink`
In handlers with current bot
----------------------------
.. code-block:: python
result: ChatInviteLink = await RevokeChatInviteLink(...)
With specific bot
~~~~~~~~~~~~~~~~~
.. code-block:: python
result: ChatInviteLink = await bot(RevokeChatInviteLink(...))
As reply into Webhook in handler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
return RevokeChatInviteLink(...)

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: bool = await bot(SetChatPhoto(...)) result: bool = await bot(SetChatPhoto(...))

View file

@ -42,4 +42,3 @@ With specific bot
.. code-block:: python .. code-block:: python
result: File = await bot(UploadStickerFile(...)) result: File = await bot(UploadStickerFile(...))

View file

@ -0,0 +1,9 @@
##############
ChatInviteLink
##############
.. automodule:: aiogram.types.chat_invite_link
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
#################
ChatMemberUpdated
#################
.. automodule:: aiogram.types.chat_member_updated
:members:
:member-order: bysource
:undoc-members: True

View file

@ -41,6 +41,11 @@ Available types
location location
venue venue
proximity_alert_triggered proximity_alert_triggered
message_auto_delete_timer_changed
voice_chat_scheduled
voice_chat_started
voice_chat_ended
voice_chat_participants_invited
user_profile_photos user_profile_photos
file file
reply_keyboard_markup reply_keyboard_markup
@ -53,7 +58,9 @@ Available types
callback_query callback_query
force_reply force_reply
chat_photo chat_photo
chat_invite_link
chat_member chat_member
chat_member_updated
chat_permissions chat_permissions
chat_location chat_location
bot_command bot_command
@ -111,6 +118,7 @@ Inline mode
input_location_message_content input_location_message_content
input_venue_message_content input_venue_message_content
input_contact_message_content input_contact_message_content
input_invoice_message_content
chosen_inline_result chosen_inline_result
Payments Payments
@ -158,4 +166,3 @@ Games
game game
callback_game callback_game
game_high_score game_high_score

View file

@ -0,0 +1,9 @@
##########################
InputInvoiceMessageContent
##########################
.. automodule:: aiogram.types.input_invoice_message_content
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
#############################
MessageAutoDeleteTimerChanged
#############################
.. automodule:: aiogram.types.message_auto_delete_timer_changed
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
##############
VoiceChatEnded
##############
.. automodule:: aiogram.types.voice_chat_ended
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
############################
VoiceChatParticipantsInvited
############################
.. automodule:: aiogram.types.voice_chat_participants_invited
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
##################
VoiceChatScheduled
##################
.. automodule:: aiogram.types.voice_chat_scheduled
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
################
VoiceChatStarted
################
.. automodule:: aiogram.types.voice_chat_started
:members:
:member-order: bysource
:undoc-members: True

View file

@ -6,7 +6,7 @@ BaseHandler
Base handler is generic abstract class and should be used in all other class-based handlers. Base handler is generic abstract class and should be used in all other class-based handlers.
Import: :code:`from aiogram.hanler import BaseHandler` Import: :code:`from aiogram.handler import BaseHandler`
By default you will need to override only method :code:`async def handle(self) -> Any: ...` By default you will need to override only method :code:`async def handle(self) -> Any: ...`

View file

@ -1,27 +1,9 @@
==================== ####################
CallbackQueryHandler CallbackQueryHandler
==================== ####################
There is base class for callback query handlers.
Simple usage
============
.. code-block:: python
from aiogram.handlers import CallbackQueryHandler
...
@router.callback_query()
class MyHandler(CallbackQueryHandler):
async def handle(self) -> Any: ...
Extension .. automodule:: aiogram.dispatcher.handler.callback_query
========= :members:
:member-order: bysource
This base handler is subclass of :ref:`BaseHandler <cbh-base-handler>` with some extensions: :undoc-members: True
- :code:`self.from_user` is alias for :code:`self.event.from_user`
- :code:`self.message` is alias for :code:`self.event.message`
- :code:`self.callback_data` is alias for :code:`self.event.data`

View file

@ -0,0 +1,28 @@
=================
ChatMemberHandler
=================
There is base class for chat member updated events.
Simple usage
============
.. code-block:: python
from aiogram.handlers import ChatMemberHandler
...
@router.chat_member()
@router.my_chat_member()
class MyHandler(ChatMemberHandler):
async def handle(self) -> Any: ...
Extension
=========
This base handler is subclass of :ref:`BaseHandler <cbh-base-handler>` with some extensions:
- :code:`self.chat` is alias for :code:`self.event.chat`

View file

@ -20,3 +20,4 @@ There are some base class based handlers what you need to use in your own handle
poll poll
pre_checkout_query pre_checkout_query
shipping_query shipping_query
chat_member

986
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "aiogram" name = "aiogram"
version = "3.0.0-alpha.6" version = "3.0.0-alpha.7"
description = "Modern and fully asynchronous framework for Telegram Bot API" description = "Modern and fully asynchronous framework for Telegram Bot API"
authors = ["Alex Root Junior <jroot.junior@gmail.com>"] authors = ["Alex Root Junior <jroot.junior@gmail.com>"]
license = "MIT" license = "MIT"
@ -33,15 +33,14 @@ classifiers = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^3.7"
aiohttp = "^3.6" aiohttp = "^3.7.4"
pydantic = "^1.5" pydantic = "^1.8.1"
Babel = "^2.7" Babel = "^2.9.1"
aiofiles = "^0.6.0" aiofiles = "^0.6.0"
uvloop = { version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'", optional = true } async_lru = "^1.0.2"
async_lru = "^1.0"
aiohttp-socks = { version = "^0.5.5", optional = true } aiohttp-socks = { version = "^0.5.5", optional = true }
typing-extensions = { version = "^3.7.4", python = "<3.8" } typing-extensions = { version = "^3.7.4", python = "<3.8" }
magic-filter = "^0.1.2" magic-filter = {version = "1.0.0a1", allow-prereleases = true}
sphinx = { version = "^3.1.0", optional = true } sphinx = { version = "^3.1.0", optional = true }
sphinx-intl = { version = "^2.0.1", optional = true } sphinx-intl = { version = "^2.0.1", optional = true }
sphinx-autobuild = { version = "^2020.9.1", optional = true } sphinx-autobuild = { version = "^2020.9.1", optional = true }
@ -51,28 +50,26 @@ sphinx-prompt = { version = "^1.3.0", optional = true }
Sphinx-Substitution-Extensions = { version = "^2020.9.30", optional = true } Sphinx-Substitution-Extensions = { version = "^2020.9.30", optional = true }
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
uvloop = { version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'" } aiohttp-socks = "^0.5"
pytest = "^6.1" ipython = "^7.22.0"
pytest-html = "^3.1" uvloop = { version = "^0.15.2", markers = "sys_platform == 'darwin' or sys_platform == 'linux'" }
pytest-asyncio = "^0.14.0" black = "^21.4b2"
pytest-mypy = "^0.8" isort = "^5.8.0"
pytest-mock = "^3.3" flake8 = "^3.9.1"
pytest-cov = "^2.8" flake8-html = "^0.4.1"
aresponses = "^2.0" mypy = "^0.812"
asynctest = { version = "^0.13.0", python = "<3.8" } pytest = "^6.2.3"
isort = "^5.6" pytest-html = "^3.1.1"
flake8 = "^3.7" pytest-asyncio = "^0.15.1"
flake8-html = "^0.4.0" pytest-mypy = "^0.8.1"
mypy = "^0.800" pytest-mock = "^3.6.0"
mkdocs = "^1.0" pytest-cov = "^2.11.1"
mkdocs-material = "^6.1" aresponses = "^2.1.4"
mkautodoc = "^0.1.0" asynctest = "^0.13.0"
toml = "^0.10.2"
pygments = "^2.4" pygments = "^2.4"
pymdown-extensions = "^8.0" pymdown-extensions = "^8.0"
lxml = "^4.4"
ipython = "^7.10"
markdown-include = "^0.6" markdown-include = "^0.6"
aiohttp-socks = "^0.5"
pre-commit = "^2.3.0" pre-commit = "^2.3.0"
packaging = "^20.3" packaging = "^20.3"
typing-extensions = "^3.7.4" typing-extensions = "^3.7.4"
@ -83,8 +80,6 @@ sphinx-copybutton = "^0.3.1"
furo = "^2020.11.15-beta.17" furo = "^2020.11.15-beta.17"
sphinx-prompt = "^1.3.0" sphinx-prompt = "^1.3.0"
Sphinx-Substitution-Extensions = "^2020.9.30" Sphinx-Substitution-Extensions = "^2020.9.30"
black = "^20.8b1"
toml = "^0.10.2"
[tool.poetry.extras] [tool.poetry.extras]
fast = ["uvloop"] fast = ["uvloop"]

View file

@ -1,5 +1,5 @@
import datetime import datetime
from typing import Any, Dict, Type, Union from typing import Any, Dict, Type, Union, Optional
import pytest import pytest
@ -21,6 +21,8 @@ from aiogram.methods import (
SendVideo, SendVideo,
SendVideoNote, SendVideoNote,
SendVoice, SendVoice,
CopyMessage,
TelegramMethod,
) )
from aiogram.types import ( from aiogram.types import (
Animation, Animation,
@ -44,156 +46,100 @@ from aiogram.types import (
Video, Video,
VideoNote, VideoNote,
Voice, Voice,
MessageAutoDeleteTimerChanged,
VoiceChatStarted,
VoiceChatEnded,
VoiceChatParticipantsInvited,
) )
from aiogram.types.message import ContentType, Message from aiogram.types.message import ContentType, Message
TEST_MESSAGE_TEXT = Message(
class TestMessage:
@pytest.mark.parametrize(
"message,content_type",
[
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
text="test", text="test",
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.TEXT, TEST_MESSAGE_AUDIO = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
audio=Audio(file_id="file id", file_unique_id="file id", duration=42), audio=Audio(file_id="file id", file_unique_id="file id", duration=42),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.AUDIO, TEST_MESSAGE_ANIMATION = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
animation=Animation( animation=Animation(
file_id="file id", file_id="file id", file_unique_id="file id", width=42, height=42, duration=0,
file_unique_id="file id",
width=42,
height=42,
duration=0,
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.ANIMATION, TEST_MESSAGE_DOCUMENT = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
document=Document(file_id="file id", file_unique_id="file id"), document=Document(file_id="file id", file_unique_id="file id"),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.DOCUMENT, TEST_MESSAGE_GAME = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
game=Game( game=Game(
title="title", title="title",
description="description", description="description",
photo=[ photo=[PhotoSize(file_id="file id", file_unique_id="file id", width=42, height=42)],
PhotoSize(
file_id="file id", file_unique_id="file id", width=42, height=42
)
],
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.GAME, TEST_MESSAGE_PHOTO = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
photo=[ photo=[PhotoSize(file_id="file id", file_unique_id="file id", width=42, height=42)],
PhotoSize(file_id="file id", file_unique_id="file id", width=42, height=42)
],
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.PHOTO,
], TEST_MESSAGE_STICKER = Message(
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
sticker=Sticker( sticker=Sticker(
file_id="file id", file_id="file id", file_unique_id="file id", width=42, height=42, is_animated=False,
file_unique_id="file id",
width=42,
height=42,
is_animated=False,
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.STICKER, TEST_MESSAGE_VIDEO = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
video=Video( video=Video(file_id="file id", file_unique_id="file id", width=42, height=42, duration=0,),
file_id="file id",
file_unique_id="file id",
width=42,
height=42,
duration=0,
),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.VIDEO, TEST_MESSAGE_VIDEO_NOTE = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
video_note=VideoNote( video_note=VideoNote(file_id="file id", file_unique_id="file id", length=0, duration=0),
file_id="file id", file_unique_id="file id", length=0, duration=0
),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.VIDEO_NOTE, TEST_MESSAGE_VOICE = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
voice=Voice(file_id="file id", file_unique_id="file id", duration=0), voice=Voice(file_id="file id", file_unique_id="file id", duration=0),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.VOICE, TEST_MESSAGE_CONTACT = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
contact=Contact(phone_number="911", first_name="911"), contact=Contact(phone_number="911", first_name="911"),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.CONTACT, TEST_MESSAGE_VENUE = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
venue=Venue( venue=Venue(
@ -204,41 +150,29 @@ class TestMessage:
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.VENUE, TEST_MESSAGE_LOCATION = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
location=Location(longitude=3.14, latitude=3.14), location=Location(longitude=3.14, latitude=3.14),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.LOCATION, TEST_MESSAGE_NEW_CHAT_MEMBERS = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
new_chat_members=[User(id=42, is_bot=False, first_name="Test")], new_chat_members=[User(id=42, is_bot=False, first_name="Test")],
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.NEW_CHAT_MEMBERS, TEST_MESSAGE_LEFT_CHAT_MEMBER = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
left_chat_member=User(id=42, is_bot=False, first_name="Test"), left_chat_member=User(id=42, is_bot=False, first_name="Test"),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.LEFT_CHAT_MEMBER, TEST_MESSAGE_INVOICE = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
invoice=Invoice( invoice=Invoice(
@ -250,11 +184,8 @@ class TestMessage:
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.INVOICE, TEST_MESSAGE_SUCCESSFUL_PAYMENT = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
successful_payment=SuccessfulPayment( successful_payment=SuccessfulPayment(
@ -266,41 +197,29 @@ class TestMessage:
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.SUCCESSFUL_PAYMENT, TEST_MESSAGE_CONNECTED_WEBSITE = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
connected_website="token", connected_website="token",
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.CONNECTED_WEBSITE, TEST_MESSAGE_MIGRATE_FROM_CHAT_ID = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
migrate_from_chat_id=42, migrate_from_chat_id=42,
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.MIGRATE_FROM_CHAT_ID, TEST_MESSAGE_MIGRATE_TO_CHAT_ID = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
migrate_to_chat_id=42, migrate_to_chat_id=42,
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.MIGRATE_TO_CHAT_ID, TEST_MESSAGE_PINNED_MESSAGE = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
pinned_message=Message( pinned_message=Message(
@ -312,75 +231,51 @@ class TestMessage:
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.PINNED_MESSAGE, TEST_MESSAGE_NEW_CHAT_TITLE = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
new_chat_title="test", new_chat_title="test",
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.NEW_CHAT_TITLE, TEST_MESSAGE_NEW_CHAT_PHOTO = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
new_chat_photo=[ new_chat_photo=[PhotoSize(file_id="file id", file_unique_id="file id", width=42, height=42)],
PhotoSize(file_id="file id", file_unique_id="file id", width=42, height=42)
],
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.NEW_CHAT_PHOTO, TEST_MESSAGE_DELETE_CHAT_PHOTO = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
delete_chat_photo=True, delete_chat_photo=True,
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.DELETE_CHAT_PHOTO, TEST_MESSAGE_GROUP_CHAT_CREATED = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
group_chat_created=True, group_chat_created=True,
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.GROUP_CHAT_CREATED, TEST_MESSAGE_PASSPORT_DATA = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
passport_data=PassportData( passport_data=PassportData(
data=[], data=[], credentials=EncryptedCredentials(data="test", hash="test", secret="test"),
credentials=EncryptedCredentials(data="test", hash="test", secret="test"),
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.PASSPORT_DATA, TEST_MESSAGE_POLL = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
poll=Poll( poll=Poll(
id="QA", id="QA",
question="Q", question="Q",
options=[ options=[PollOption(text="A", voter_count=0), PollOption(text="B", voter_count=0),],
PollOption(text="A", voter_count=0),
PollOption(text="B", voter_count=0),
],
is_closed=False, is_closed=False,
is_anonymous=False, is_anonymous=False,
type="quiz", type="quiz",
@ -390,28 +285,95 @@ class TestMessage:
), ),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
)
TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED = Message(
message_id=42,
date=datetime.datetime.now(),
chat=Chat(id=42, type="private"),
message_auto_delete_timer_changed=MessageAutoDeleteTimerChanged(message_auto_delete_time=42),
from_user=User(id=42, is_bot=False, first_name="Test"),
)
TEST_MESSAGE_VOICE_CHAT_STARTED = Message(
message_id=42,
date=datetime.datetime.now(),
chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"),
voice_chat_started=VoiceChatStarted(),
)
TEST_MESSAGE_VOICE_CHAT_ENDED = Message(
message_id=42,
date=datetime.datetime.now(),
chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"),
voice_chat_ended=VoiceChatEnded(duration=42),
)
TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED = Message(
message_id=42,
date=datetime.datetime.now(),
chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"),
voice_chat_participants_invited=VoiceChatParticipantsInvited(
users=[User(id=69, is_bot=False, first_name="Test")]
), ),
ContentType.POLL, )
], TEST_MESSAGE_DICE = Message(
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
dice=Dice(value=6, emoji="X"), dice=Dice(value=6, emoji="X"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.DICE, TEST_MESSAGE_UNKNOWN = Message(
],
[
Message(
message_id=42, message_id=42,
date=datetime.datetime.now(), date=datetime.datetime.now(),
chat=Chat(id=42, type="private"), chat=Chat(id=42, type="private"),
from_user=User(id=42, is_bot=False, first_name="Test"), from_user=User(id=42, is_bot=False, first_name="Test"),
), )
ContentType.UNKNOWN,
class TestMessage:
@pytest.mark.parametrize(
"message,content_type",
[
[TEST_MESSAGE_TEXT, ContentType.TEXT],
[TEST_MESSAGE_AUDIO, ContentType.AUDIO],
[TEST_MESSAGE_ANIMATION, ContentType.ANIMATION],
[TEST_MESSAGE_DOCUMENT, ContentType.DOCUMENT],
[TEST_MESSAGE_GAME, ContentType.GAME],
[TEST_MESSAGE_PHOTO, ContentType.PHOTO],
[TEST_MESSAGE_STICKER, ContentType.STICKER],
[TEST_MESSAGE_VIDEO, ContentType.VIDEO],
[TEST_MESSAGE_VIDEO_NOTE, ContentType.VIDEO_NOTE],
[TEST_MESSAGE_VOICE, ContentType.VOICE],
[TEST_MESSAGE_CONTACT, ContentType.CONTACT],
[TEST_MESSAGE_VENUE, ContentType.VENUE],
[TEST_MESSAGE_LOCATION, ContentType.LOCATION],
[TEST_MESSAGE_NEW_CHAT_MEMBERS, ContentType.NEW_CHAT_MEMBERS],
[TEST_MESSAGE_LEFT_CHAT_MEMBER, ContentType.LEFT_CHAT_MEMBER],
[TEST_MESSAGE_INVOICE, ContentType.INVOICE],
[TEST_MESSAGE_SUCCESSFUL_PAYMENT, ContentType.SUCCESSFUL_PAYMENT],
[TEST_MESSAGE_CONNECTED_WEBSITE, ContentType.CONNECTED_WEBSITE],
[TEST_MESSAGE_MIGRATE_FROM_CHAT_ID, ContentType.MIGRATE_FROM_CHAT_ID],
[TEST_MESSAGE_MIGRATE_TO_CHAT_ID, ContentType.MIGRATE_TO_CHAT_ID],
[TEST_MESSAGE_PINNED_MESSAGE, ContentType.PINNED_MESSAGE],
[TEST_MESSAGE_NEW_CHAT_TITLE, ContentType.NEW_CHAT_TITLE],
[TEST_MESSAGE_NEW_CHAT_PHOTO, ContentType.NEW_CHAT_PHOTO],
[TEST_MESSAGE_DELETE_CHAT_PHOTO, ContentType.DELETE_CHAT_PHOTO],
[TEST_MESSAGE_GROUP_CHAT_CREATED, ContentType.GROUP_CHAT_CREATED],
[TEST_MESSAGE_PASSPORT_DATA, ContentType.PASSPORT_DATA],
[TEST_MESSAGE_POLL, ContentType.POLL],
[
TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED,
ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED,
], ],
[TEST_MESSAGE_VOICE_CHAT_STARTED, ContentType.VOICE_CHAT_STARTED],
[TEST_MESSAGE_VOICE_CHAT_ENDED, ContentType.VOICE_CHAT_ENDED],
[
TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED,
ContentType.VOICE_CHAT_PARTICIPANTS_INVITED,
],
[TEST_MESSAGE_DICE, ContentType.DICE],
[TEST_MESSAGE_UNKNOWN, ContentType.UNKNOWN],
], ],
) )
def test_content_type(self, message: Message, content_type: str): def test_content_type(self, message: Message, content_type: str):
@ -448,12 +410,7 @@ class TestMessage:
["sticker", dict(sticker="sticker"), SendSticker], ["sticker", dict(sticker="sticker"), SendSticker],
[ [
"venue", "venue",
dict( dict(latitude=0.42, longitude=0.42, title="title", address="address",),
latitude=0.42,
longitude=0.42,
title="title",
address="address",
),
SendVenue, SendVenue,
], ],
["video", dict(video="video"), SendVideo], ["video", dict(video="video"), SendVideo],
@ -508,3 +465,62 @@ class TestMessage:
for key, value in kwargs.items(): for key, value in kwargs.items():
assert getattr(api_method, key) == value assert getattr(api_method, key) == value
def test_copy_to(self):
message = Message(
message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
)
method = message.copy_to(chat_id=message.chat.id)
assert isinstance(method, CopyMessage)
assert method.chat_id == message.chat.id
@pytest.mark.parametrize(
"message,expected_method",
[
[TEST_MESSAGE_TEXT, SendMessage],
[TEST_MESSAGE_AUDIO, SendAudio],
[TEST_MESSAGE_ANIMATION, SendAnimation],
[TEST_MESSAGE_DOCUMENT, SendDocument],
[TEST_MESSAGE_GAME, None],
[TEST_MESSAGE_PHOTO, SendPhoto],
[TEST_MESSAGE_STICKER, SendSticker],
[TEST_MESSAGE_VIDEO, SendVideo],
[TEST_MESSAGE_VIDEO_NOTE, SendVideoNote],
[TEST_MESSAGE_VOICE, SendVoice],
[TEST_MESSAGE_CONTACT, SendContact],
[TEST_MESSAGE_VENUE, SendVenue],
[TEST_MESSAGE_LOCATION, SendLocation],
[TEST_MESSAGE_NEW_CHAT_MEMBERS, None],
[TEST_MESSAGE_LEFT_CHAT_MEMBER, None],
[TEST_MESSAGE_INVOICE, None],
[TEST_MESSAGE_SUCCESSFUL_PAYMENT, None],
[TEST_MESSAGE_CONNECTED_WEBSITE, None],
[TEST_MESSAGE_MIGRATE_FROM_CHAT_ID, None],
[TEST_MESSAGE_MIGRATE_TO_CHAT_ID, None],
[TEST_MESSAGE_PINNED_MESSAGE, None],
[TEST_MESSAGE_NEW_CHAT_TITLE, None],
[TEST_MESSAGE_NEW_CHAT_PHOTO, None],
[TEST_MESSAGE_DELETE_CHAT_PHOTO, None],
[TEST_MESSAGE_GROUP_CHAT_CREATED, None],
[TEST_MESSAGE_PASSPORT_DATA, None],
[TEST_MESSAGE_POLL, SendPoll],
[TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED, None],
[TEST_MESSAGE_VOICE_CHAT_STARTED, None],
[TEST_MESSAGE_VOICE_CHAT_ENDED, None],
[TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED, None],
[TEST_MESSAGE_DICE, SendDice],
[TEST_MESSAGE_UNKNOWN, None],
],
)
def test_send_copy(
self, message: Message, expected_method: Optional[Type[TelegramMethod]],
):
if expected_method is None:
with pytest.raises(TypeError, match="This type of message can't be copied."):
message.send_copy(chat_id=42)
return
method = message.send_copy(chat_id=42)
if method:
assert isinstance(method, expected_method)
# TODO: Check additional fields

Some files were not shown because too many files have changed in this diff Show more