mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'dev-2.x' into bug/#562-default-locale-unused
This commit is contained in:
commit
d63dc49562
37 changed files with 984 additions and 320 deletions
|
|
@ -6,7 +6,7 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://docs.aiogram.dev/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
|
@ -29,9 +29,10 @@
|
|||
import asyncio
|
||||
from aiogram import Bot
|
||||
|
||||
BOT_TOKEN = ""
|
||||
|
||||
async def main():
|
||||
bot = Bot(token=BOT-TOKEN)
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
|
||||
try:
|
||||
me = await bot.get_me()
|
||||
|
|
@ -48,6 +49,8 @@ asyncio.run(main())
|
|||
import asyncio
|
||||
from aiogram import Bot, Dispatcher, types
|
||||
|
||||
BOT_TOKEN = ""
|
||||
|
||||
async def start_handler(event: types.Message):
|
||||
await event.answer(
|
||||
f"Hello, {event.from_user.get_mention(as_html=True)} 👋!",
|
||||
|
|
@ -55,7 +58,7 @@ async def start_handler(event: types.Message):
|
|||
)
|
||||
|
||||
async def main():
|
||||
bot = Bot(token=BOT-TOKEN)
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
try:
|
||||
disp = Dispatcher(bot=bot)
|
||||
disp.register_message_handler(start_handler, commands={"start", "restart"})
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ AIOGramBot
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.1-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
|
|
|
|||
|
|
@ -43,5 +43,5 @@ __all__ = (
|
|||
'utils',
|
||||
)
|
||||
|
||||
__version__ = '2.12.1'
|
||||
__api_version__ = '5.1'
|
||||
__version__ = '2.14.1'
|
||||
__api_version__ = '5.3'
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ class Methods(Helper):
|
|||
"""
|
||||
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
|
||||
|
||||
List is updated to Bot API 5.1
|
||||
List is updated to Bot API 5.3
|
||||
"""
|
||||
mode = HelperMode.lowerCamelCase
|
||||
|
||||
|
|
@ -225,6 +225,7 @@ class Methods(Helper):
|
|||
GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos
|
||||
GET_FILE = Item() # getFile
|
||||
KICK_CHAT_MEMBER = Item() # kickChatMember
|
||||
BAN_CHAT_MEMBER = Item() # banChatMember
|
||||
UNBAN_CHAT_MEMBER = Item() # unbanChatMember
|
||||
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
|
||||
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
|
||||
|
|
@ -244,12 +245,14 @@ class Methods(Helper):
|
|||
LEAVE_CHAT = Item() # leaveChat
|
||||
GET_CHAT = Item() # getChat
|
||||
GET_CHAT_ADMINISTRATORS = Item() # getChatAdministrators
|
||||
GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount
|
||||
GET_CHAT_MEMBER_COUNT = Item() # getChatMemberCount
|
||||
GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount (renamed to getChatMemberCount)
|
||||
GET_CHAT_MEMBER = Item() # getChatMember
|
||||
SET_CHAT_STICKER_SET = Item() # setChatStickerSet
|
||||
DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet
|
||||
ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery
|
||||
SET_MY_COMMANDS = Item() # setMyCommands
|
||||
DELETE_MY_COMMANDS = Item() # deleteMyCommands
|
||||
GET_MY_COMMANDS = Item() # getMyCommands
|
||||
|
||||
# Updating messages
|
||||
|
|
|
|||
|
|
@ -1484,19 +1484,36 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
action: base.String) -> base.Boolean:
|
||||
"""
|
||||
Use this method when you need to tell the user that something is happening on the bot's side.
|
||||
The status is set for 5 seconds or less
|
||||
(when a message arrives from your bot, Telegram clients clear its typing status).
|
||||
Use this method when you need to tell the user that something is
|
||||
happening on the bot's side. The status is set for 5 seconds or
|
||||
less (when a message arrives from your bot, Telegram clients
|
||||
clear its typing status). Returns True on success.
|
||||
|
||||
We only recommend using this method when a response from the bot will take
|
||||
a noticeable amount of time to arrive.
|
||||
Example: The ImageBot needs some time to process a request and
|
||||
upload the image. Instead of sending a text message along the
|
||||
lines of “Retrieving image, please wait…”, the bot may use
|
||||
sendChatAction with action = upload_photo. The user will see a
|
||||
“sending photo” status for the bot.
|
||||
|
||||
We only recommend using this method when a response from the bot
|
||||
will take a noticeable amount of time to arrive.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendchataction
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel
|
||||
:param chat_id: Unique identifier for the target chat or
|
||||
username of the target channel (in the format
|
||||
@channelusername)
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
:param action: Type of action to broadcast
|
||||
|
||||
:param action: Type of action to broadcast. Choose one,
|
||||
depending on what the user is about to receive: `typing` for
|
||||
text messages, `upload_photo` for photos, `record_video` or
|
||||
`upload_video` for videos, `record_voice` or `upload_voice`
|
||||
for voice notes, `upload_document` for general files,
|
||||
`find_location` for location data, `record_video_note` or
|
||||
`upload_video_note` for video notes.
|
||||
:type action: :obj:`base.String`
|
||||
|
||||
:return: Returns True on success
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
|
|
@ -1545,41 +1562,42 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
result = await self.request(api.Methods.GET_FILE, payload)
|
||||
return types.File(**result)
|
||||
|
||||
async def kick_chat_member(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
user_id: base.Integer,
|
||||
until_date: typing.Union[base.Integer, datetime.datetime,
|
||||
datetime.timedelta, None] = None,
|
||||
revoke_messages: typing.Optional[base.Boolean] = None,
|
||||
) -> base.Boolean:
|
||||
async def ban_chat_member(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
user_id: base.Integer,
|
||||
until_date: typing.Union[base.Integer, datetime.datetime,
|
||||
datetime.timedelta, None] = None,
|
||||
revoke_messages: typing.Optional[base.Boolean] = None,
|
||||
) -> base.Boolean:
|
||||
"""
|
||||
Use this method to kick a user from a group, a supergroup or a channel.
|
||||
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., unless unbanned first.
|
||||
Use this method to ban a user in a group, a supergroup or a
|
||||
channel. 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., unless unbanned first. The bot must be an
|
||||
administrator in the chat for this to work and must have the
|
||||
appropriate admin rights. Returns True on success.
|
||||
|
||||
The bot must be an administrator in the chat for this to work and must have
|
||||
the appropriate admin rights.
|
||||
Source: https://core.telegram.org/bots/api#banchatmember
|
||||
|
||||
Source: https://core.telegram.org/bots/api#kickchatmember
|
||||
|
||||
:param chat_id: Unique identifier for the target group or username of the
|
||||
target supergroup or channel (in the format @channelusername)
|
||||
:param chat_id: Unique identifier for the target group or
|
||||
username of the target supergroup or channel (in the format
|
||||
@channelusername)
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
|
||||
:param user_id: Unique identifier of the target user
|
||||
:type user_id: :obj:`base.Integer`
|
||||
|
||||
:param until_date: Date when the user will be unbanned. 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.
|
||||
:type until_date: :obj:`typing.Union[base.Integer, datetime.datetime,
|
||||
datetime.timedelta, None]`
|
||||
: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.
|
||||
:type until_date: :obj:`typing.Union[base.Integer,
|
||||
datetime.datetime, datetime.timedelta, None]`
|
||||
|
||||
:param revoke_messages: Pass True to delete all messages from the chat for
|
||||
the user that is being removed. If False, the user will be able to see
|
||||
messages in the group that were sent before the user was removed. Always
|
||||
True for supergroups and channels.
|
||||
:param revoke_messages: Pass True to delete all messages from
|
||||
the chat for the user that is being removed. If False, the user
|
||||
will be able to see messages in the group that were sent before
|
||||
the user was removed. Always True for supergroups and channels.
|
||||
:type revoke_messages: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
:return: Returns True on success
|
||||
|
|
@ -1588,7 +1606,22 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
until_date = prepare_arg(until_date)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.KICK_CHAT_MEMBER, payload)
|
||||
return await self.request(api.Methods.BAN_CHAT_MEMBER, payload)
|
||||
|
||||
async def kick_chat_member(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
user_id: base.Integer,
|
||||
until_date: typing.Union[base.Integer, datetime.datetime,
|
||||
datetime.timedelta, None] = None,
|
||||
revoke_messages: typing.Optional[base.Boolean] = None,
|
||||
) -> base.Boolean:
|
||||
"""Renamed to ban_chat_member."""
|
||||
return await self.ban_chat_member(
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
until_date=until_date,
|
||||
revoke_messages=revoke_messages,
|
||||
)
|
||||
|
||||
async def unban_chat_member(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
|
|
@ -1834,7 +1867,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
expire_date = prepare_arg(expire_date)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload)
|
||||
result = await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload)
|
||||
return types.ChatInviteLink(**result)
|
||||
|
||||
async def edit_chat_invite_link(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
|
|
@ -1870,7 +1904,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
expire_date = prepare_arg(expire_date)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload)
|
||||
result = await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload)
|
||||
return types.ChatInviteLink(**result)
|
||||
|
||||
async def revoke_chat_invite_link(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
|
|
@ -1891,7 +1926,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload)
|
||||
result = await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload)
|
||||
return types.ChatInviteLink(**result)
|
||||
|
||||
async def set_chat_photo(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
photo: base.InputFile) -> base.Boolean:
|
||||
|
|
@ -2110,13 +2146,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
payload = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.GET_CHAT_ADMINISTRATORS, payload)
|
||||
return [types.ChatMember(**chatmember) for chatmember in result]
|
||||
return [types.ChatMember.resolve(**chat_member) for chat_member in result]
|
||||
|
||||
async def get_chat_members_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer:
|
||||
async def get_chat_member_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer:
|
||||
"""
|
||||
Use this method to get the number of members in a chat.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#getchatmemberscount
|
||||
Source: https://core.telegram.org/bots/api#getchatmembercount
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target supergroup or channel
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
|
|
@ -2125,7 +2161,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
"""
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.GET_CHAT_MEMBERS_COUNT, payload)
|
||||
return await self.request(api.Methods.GET_CHAT_MEMBER_COUNT, payload)
|
||||
|
||||
async def get_chat_members_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer:
|
||||
"""Renamed to get_chat_member_count."""
|
||||
return await self.get_chat_member_count(chat_id)
|
||||
|
||||
async def get_chat_member(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
user_id: base.Integer) -> types.ChatMember:
|
||||
|
|
@ -2144,7 +2184,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
payload = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.GET_CHAT_MEMBER, payload)
|
||||
return types.ChatMember(**result)
|
||||
return types.ChatMember.resolve(**result)
|
||||
|
||||
async def set_chat_sticker_set(self, chat_id: typing.Union[base.Integer, base.String],
|
||||
sticker_set_name: base.String) -> base.Boolean:
|
||||
|
|
@ -2221,31 +2261,95 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
|
||||
return await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload)
|
||||
|
||||
async def set_my_commands(self, commands: typing.List[types.BotCommand]) -> base.Boolean:
|
||||
async def set_my_commands(self,
|
||||
commands: typing.List[types.BotCommand],
|
||||
scope: typing.Optional[types.BotCommandScope] = None,
|
||||
language_code: typing.Optional[base.String] = None,
|
||||
) -> base.Boolean:
|
||||
"""
|
||||
Use this method to change the list of the bot's commands.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#setmycommands
|
||||
|
||||
:param commands: A JSON-serialized list of bot commands to be set as the list of the bot's commands.
|
||||
At most 100 commands can be specified.
|
||||
:param commands: A JSON-serialized list of bot commands to be
|
||||
set as the list of the bot's commands. At most 100 commands
|
||||
can be specified.
|
||||
:type commands: :obj: `typing.List[types.BotCommand]`
|
||||
|
||||
:param scope: A JSON-serialized object, describing scope of
|
||||
users for which the commands are relevant. Defaults to
|
||||
BotCommandScopeDefault.
|
||||
:type scope: :obj: `typing.Optional[types.BotCommandScope]`
|
||||
|
||||
:param language_code: A two-letter ISO 639-1 language code. If
|
||||
empty, commands will be applied to all users from the given
|
||||
scope, for whose language there are no dedicated commands
|
||||
:type language_code: :obj: `typing.Optional[base.String]`
|
||||
|
||||
:return: Returns True on success.
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
commands = prepare_arg(commands)
|
||||
scope = prepare_arg(scope)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.SET_MY_COMMANDS, payload)
|
||||
|
||||
async def get_my_commands(self) -> typing.List[types.BotCommand]:
|
||||
async def delete_my_commands(self,
|
||||
scope: typing.Optional[types.BotCommandScope] = None,
|
||||
language_code: typing.Optional[base.String] = None,
|
||||
) -> base.Boolean:
|
||||
"""
|
||||
Use this method to get the current list of the bot's commands.
|
||||
Use this method to delete the list of the bot's commands for the
|
||||
given scope and user language. After deletion, higher level
|
||||
commands will be shown to affected users.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#deletemycommands
|
||||
|
||||
:param scope: A JSON-serialized object, describing scope of
|
||||
users for which the commands are relevant. Defaults to
|
||||
BotCommandScopeDefault.
|
||||
:type scope: :obj: `typing.Optional[types.BotCommandScope]`
|
||||
|
||||
:param language_code: A two-letter ISO 639-1 language code. If
|
||||
empty, commands will be applied to all users from the given
|
||||
scope, for whose language there are no dedicated commands
|
||||
:type language_code: :obj: `typing.Optional[base.String]`
|
||||
|
||||
:return: Returns True on success.
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
scope = prepare_arg(scope)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
return await self.request(api.Methods.DELETE_MY_COMMANDS, payload)
|
||||
|
||||
async def get_my_commands(self,
|
||||
scope: typing.Optional[types.BotCommandScope] = None,
|
||||
language_code: typing.Optional[base.String] = None,
|
||||
) -> typing.List[types.BotCommand]:
|
||||
"""
|
||||
Use this method to get the current list of the bot's commands
|
||||
for the given scope and user language. Returns Array of
|
||||
BotCommand on success. If commands aren't set, an empty list is
|
||||
returned.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#getmycommands
|
||||
:return: Returns Array of BotCommand on success.
|
||||
|
||||
:param scope: A JSON-serialized object, describing scope of
|
||||
users for which the commands are relevant. Defaults to
|
||||
BotCommandScopeDefault.
|
||||
:type scope: :obj: `typing.Optional[types.BotCommandScope]`
|
||||
|
||||
:param language_code: A two-letter ISO 639-1 language code. If
|
||||
empty, commands will be applied to all users from the given
|
||||
scope, for whose language there are no dedicated commands
|
||||
:type language_code: :obj: `typing.Optional[base.String]`
|
||||
|
||||
:return: Returns Array of BotCommand on success or empty list.
|
||||
:rtype: :obj:`typing.List[types.BotCommand]`
|
||||
"""
|
||||
scope = prepare_arg(scope)
|
||||
payload = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.GET_MY_COMMANDS, payload)
|
||||
|
|
@ -2780,10 +2884,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
# === Payments ===
|
||||
# https://core.telegram.org/bots/api#payments
|
||||
|
||||
async def send_invoice(self, chat_id: base.Integer, title: base.String,
|
||||
description: base.String, payload: base.String,
|
||||
provider_token: base.String, start_parameter: base.String,
|
||||
currency: base.String, prices: typing.List[types.LabeledPrice],
|
||||
async def send_invoice(self,
|
||||
chat_id: typing.Union[base.Integer, base.String],
|
||||
title: base.String,
|
||||
description: base.String,
|
||||
payload: base.String,
|
||||
provider_token: base.String,
|
||||
currency: base.String,
|
||||
prices: typing.List[types.LabeledPrice],
|
||||
max_tip_amount: typing.Optional[base.Integer] = None,
|
||||
suggested_tip_amounts: typing.Optional[
|
||||
typing.List[base.Integer]
|
||||
] = None,
|
||||
start_parameter: typing.Optional[base.String] = None,
|
||||
provider_data: typing.Optional[typing.Dict] = None,
|
||||
photo_url: typing.Optional[base.String] = None,
|
||||
photo_size: typing.Optional[base.Integer] = None,
|
||||
|
|
@ -2799,14 +2912,17 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
disable_notification: typing.Optional[base.Boolean] = None,
|
||||
reply_to_message_id: typing.Optional[base.Integer] = None,
|
||||
allow_sending_without_reply: typing.Optional[base.Boolean] = None,
|
||||
reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None) -> types.Message:
|
||||
reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None,
|
||||
) -> types.Message:
|
||||
"""
|
||||
Use this method to send invoices.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendinvoice
|
||||
|
||||
:param chat_id: Unique identifier for the target private chat
|
||||
:type chat_id: :obj:`base.Integer`
|
||||
:param chat_id: Unique identifier for the target chat or
|
||||
username of the target channel (in the format
|
||||
@channelusername)
|
||||
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
|
||||
|
||||
:param title: Product name, 1-32 characters
|
||||
:type title: :obj:`base.String`
|
||||
|
|
@ -2821,10 +2937,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
:param provider_token: Payments provider token, obtained via Botfather
|
||||
:type provider_token: :obj:`base.String`
|
||||
|
||||
:param start_parameter: Unique deep-linking parameter that can be used to generate this
|
||||
invoice when used as a start parameter
|
||||
:type start_parameter: :obj:`base.String`
|
||||
|
||||
:param currency: Three-letter ISO 4217 currency code, see more on currencies
|
||||
:type currency: :obj:`base.String`
|
||||
|
||||
|
|
@ -2832,6 +2944,32 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
(e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)
|
||||
:type prices: :obj:`typing.List[types.LabeledPrice]`
|
||||
|
||||
: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 US$ 1.45
|
||||
pass max_tip_amount = 145. See the exp parameter in
|
||||
currencies.json, it shows the number of digits past the
|
||||
decimal point for each currency (2 for the majority of
|
||||
currencies). Defaults to 0
|
||||
:type max_tip_amount: :obj:`typing.Optional[base.Integer]`
|
||||
|
||||
: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.
|
||||
:type suggested_tip_amounts: :obj:`typing.Optional[typing.List[base.Integer]]`
|
||||
|
||||
: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
|
||||
:type start_parameter: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param provider_data: JSON-encoded data about the invoice, which will be shared with the payment provider
|
||||
:type provider_data: :obj:`typing.Optional[typing.Dict]`
|
||||
|
||||
|
|
@ -2887,6 +3025,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
|
|||
"""
|
||||
prices = prepare_arg([price.to_python() if hasattr(price, 'to_python') else price for price in prices])
|
||||
reply_markup = prepare_arg(reply_markup)
|
||||
provider_data = prepare_arg(provider_data)
|
||||
payload_ = generate_payload(**locals())
|
||||
|
||||
result = await self.request(api.Methods.SEND_INVOICE, payload_)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class MemoryStorage(BaseStorage):
|
|||
user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
chat, user = self.resolve_address(chat=chat, user=user)
|
||||
return self.data[chat][user]['state']
|
||||
return self.data[chat][user].get("state", self.resolve_state(default))
|
||||
|
||||
async def get_data(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
|
|
@ -58,7 +58,7 @@ class MemoryStorage(BaseStorage):
|
|||
user: typing.Union[str, int, None] = None,
|
||||
state: typing.AnyStr = None):
|
||||
chat, user = self.resolve_address(chat=chat, user=user)
|
||||
self.data[chat][user]['state'] = state
|
||||
self.data[chat][user]['state'] = self.resolve_state(state)
|
||||
|
||||
async def set_data(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
|
|
@ -66,6 +66,7 @@ class MemoryStorage(BaseStorage):
|
|||
data: typing.Dict = None):
|
||||
chat, user = self.resolve_address(chat=chat, user=user)
|
||||
self.data[chat][user]['data'] = copy.deepcopy(data)
|
||||
self._cleanup(chat, user)
|
||||
|
||||
async def reset_state(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
|
|
@ -74,6 +75,7 @@ class MemoryStorage(BaseStorage):
|
|||
await self.set_state(chat=chat, user=user, state=None)
|
||||
if with_data:
|
||||
await self.set_data(chat=chat, user=user, data={})
|
||||
self._cleanup(chat, user)
|
||||
|
||||
def has_bucket(self):
|
||||
return True
|
||||
|
|
@ -91,6 +93,7 @@ class MemoryStorage(BaseStorage):
|
|||
bucket: typing.Dict = None):
|
||||
chat, user = self.resolve_address(chat=chat, user=user)
|
||||
self.data[chat][user]['bucket'] = copy.deepcopy(bucket)
|
||||
self._cleanup(chat, user)
|
||||
|
||||
async def update_bucket(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
|
|
@ -100,3 +103,10 @@ class MemoryStorage(BaseStorage):
|
|||
bucket = {}
|
||||
chat, user = self.resolve_address(chat=chat, user=user)
|
||||
self.data[chat][user]['bucket'].update(bucket, **kwargs)
|
||||
|
||||
def _cleanup(self, chat, user):
|
||||
chat, user = self.resolve_address(chat=chat, user=user)
|
||||
if self.data[chat][user] == {'state': None, 'data': {}, 'bucket': {}}:
|
||||
del self.data[chat][user]
|
||||
if not self.data[chat]:
|
||||
del self.data[chat]
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class MongoStorage(BaseStorage):
|
|||
try:
|
||||
self._mongo = AsyncIOMotorClient(self._uri)
|
||||
except pymongo.errors.ConfigurationError as e:
|
||||
if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]:
|
||||
if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]:
|
||||
import logging
|
||||
logger = logging.getLogger("aiogram")
|
||||
logger.warning("Run `pip install dnspython==1.16.0` in order to fix ConfigurationError. More information: https://github.com/mongodb/mongo-python-driver/pull/423#issuecomment-528998245")
|
||||
|
|
@ -114,7 +114,9 @@ class MongoStorage(BaseStorage):
|
|||
async def wait_closed(self):
|
||||
return True
|
||||
|
||||
async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
async def set_state(self, *,
|
||||
chat: Union[str, int, None] = None,
|
||||
user: Union[str, int, None] = None,
|
||||
state: Optional[AnyStr] = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
|
|
@ -122,8 +124,11 @@ class MongoStorage(BaseStorage):
|
|||
if state is None:
|
||||
await db[STATE].delete_one(filter={'chat': chat, 'user': user})
|
||||
else:
|
||||
await db[STATE].update_one(filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'state': state}}, upsert=True)
|
||||
await db[STATE].update_one(
|
||||
filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'state': self.resolve_state(state)}},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
default: Optional[str] = None) -> Optional[str]:
|
||||
|
|
@ -131,15 +136,17 @@ class MongoStorage(BaseStorage):
|
|||
db = await self.get_db()
|
||||
result = await db[STATE].find_one(filter={'chat': chat, 'user': user})
|
||||
|
||||
return result.get('state') if result else default
|
||||
return result.get('state') if result else self.resolve_state(default)
|
||||
|
||||
async def set_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
data: Dict = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
db = await self.get_db()
|
||||
|
||||
await db[DATA].update_one(filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'data': data}}, upsert=True)
|
||||
if not data:
|
||||
await db[DATA].delete_one(filter={'chat': chat, 'user': user})
|
||||
else:
|
||||
await db[DATA].update_one(filter={'chat': chat, 'user': user},
|
||||
update={'$set': {'data': data}}, upsert=True)
|
||||
|
||||
async def get_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None,
|
||||
default: Optional[dict] = None) -> Dict:
|
||||
|
|
|
|||
|
|
@ -110,24 +110,29 @@ class RedisStorage(BaseStorage):
|
|||
chat, user = self.check_address(chat=chat, user=user)
|
||||
addr = f"fsm:{chat}:{user}"
|
||||
|
||||
record = {'state': state, 'data': data, 'bucket': bucket}
|
||||
|
||||
conn = await self.redis()
|
||||
await conn.execute('SET', addr, json.dumps(record))
|
||||
if state is None and data == bucket == {}:
|
||||
await conn.execute('DEL', addr)
|
||||
else:
|
||||
record = {'state': state, 'data': data, 'bucket': bucket}
|
||||
await conn.execute('SET', addr, json.dumps(record))
|
||||
|
||||
async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
record = await self.get_record(chat=chat, user=user)
|
||||
return record['state']
|
||||
return record.get('state', self.resolve_state(default))
|
||||
|
||||
async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[str] = None) -> typing.Dict:
|
||||
record = await self.get_record(chat=chat, user=user)
|
||||
return record['data']
|
||||
|
||||
async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
async def set_state(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
state: typing.Optional[typing.AnyStr] = None):
|
||||
record = await self.get_record(chat=chat, user=user)
|
||||
state = self.resolve_state(state)
|
||||
await self.set_record(chat=chat, user=user, state=state, data=record['data'])
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
|
|
@ -219,7 +224,7 @@ class RedisStorage2(BaseStorage):
|
|||
await dp.storage.wait_closed()
|
||||
|
||||
"""
|
||||
def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
|
||||
def __init__(self, host: str = 'localhost', port=6379, db=None, password=None,
|
||||
ssl=None, pool_size=10, loop=None, prefix='fsm',
|
||||
state_ttl: int = 0,
|
||||
data_ttl: int = 0,
|
||||
|
|
@ -274,7 +279,7 @@ class RedisStorage2(BaseStorage):
|
|||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_KEY)
|
||||
redis = await self.redis()
|
||||
return await redis.get(key, encoding='utf8') or None
|
||||
return await redis.get(key, encoding='utf8') or self.resolve_state(default)
|
||||
|
||||
async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[dict] = None) -> typing.Dict:
|
||||
|
|
@ -294,14 +299,17 @@ class RedisStorage2(BaseStorage):
|
|||
if state is None:
|
||||
await redis.delete(key)
|
||||
else:
|
||||
await redis.set(key, state, expire=self._state_ttl)
|
||||
await redis.set(key, self.resolve_state(state), expire=self._state_ttl)
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None):
|
||||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_DATA_KEY)
|
||||
redis = await self.redis()
|
||||
await redis.set(key, json.dumps(data), expire=self._data_ttl)
|
||||
if data:
|
||||
await redis.set(key, json.dumps(data), expire=self._data_ttl)
|
||||
else:
|
||||
await redis.delete(key)
|
||||
|
||||
async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None, **kwargs):
|
||||
|
|
@ -329,7 +337,10 @@ class RedisStorage2(BaseStorage):
|
|||
chat, user = self.check_address(chat=chat, user=user)
|
||||
key = self.generate_key(chat, user, STATE_BUCKET_KEY)
|
||||
redis = await self.redis()
|
||||
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
|
||||
if bucket:
|
||||
await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl)
|
||||
else:
|
||||
await redis.delete(key)
|
||||
|
||||
async def update_bucket(self, *, chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ class RethinkDBStorage(BaseStorage):
|
|||
default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
chat, user = map(str, self.check_address(chat=chat, user=user))
|
||||
async with self.connection() as conn:
|
||||
return await r.table(self._table).get(chat)[user]['state'].default(default or None).run(conn)
|
||||
return await r.table(self._table).get(chat)[user]['state'].default(
|
||||
self.resolve_state(default) or None
|
||||
).run(conn)
|
||||
|
||||
async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
default: typing.Optional[str] = None) -> typing.Dict:
|
||||
|
|
@ -103,11 +105,16 @@ class RethinkDBStorage(BaseStorage):
|
|||
async with self.connection() as conn:
|
||||
return await r.table(self._table).get(chat)[user]['data'].default(default or {}).run(conn)
|
||||
|
||||
async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
async def set_state(self, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None,
|
||||
state: typing.Optional[typing.AnyStr] = None):
|
||||
chat, user = map(str, self.check_address(chat=chat, user=user))
|
||||
async with self.connection() as conn:
|
||||
await r.table(self._table).insert({'id': chat, user: {'state': state}}, conflict="update").run(conn)
|
||||
await r.table(self._table).insert(
|
||||
{'id': chat, user: {'state': self.resolve_state(state)}},
|
||||
conflict="update",
|
||||
).run(conn)
|
||||
|
||||
async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
|
||||
data: typing.Dict = None):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import gettext
|
||||
import os
|
||||
from contextvars import ContextVar
|
||||
from typing import Any, Dict, Tuple
|
||||
from typing import Any, Dict, Tuple, Optional
|
||||
|
||||
from babel import Locale
|
||||
from babel.support import LazyProxy
|
||||
|
|
@ -119,17 +119,18 @@ class I18nMiddleware(BaseMiddleware):
|
|||
return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache)
|
||||
|
||||
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
||||
async def get_user_locale(self, action: str, args: Tuple[Any]) -> str:
|
||||
async def get_user_locale(self, action: str, args: Tuple[Any]) -> Optional[str]:
|
||||
"""
|
||||
User locale getter
|
||||
You can override the method if you want to use different way of getting user language.
|
||||
You can override the method if you want to use different way of
|
||||
getting user language.
|
||||
|
||||
:param action: event name
|
||||
:param args: event arguments
|
||||
:return: locale name
|
||||
:return: locale name or None
|
||||
"""
|
||||
user: types.User = types.User.get_current()
|
||||
locale: Locale = user.locale
|
||||
user: Optional[types.User] = types.User.get_current()
|
||||
locale: Optional[Locale] = user.locale if user else None
|
||||
|
||||
if locale and locale.language in self.locales:
|
||||
*_, data = args
|
||||
|
|
|
|||
|
|
@ -340,7 +340,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
limit=None,
|
||||
reset_webhook=None,
|
||||
fast: typing.Optional[bool] = True,
|
||||
error_sleep: int = 5):
|
||||
error_sleep: int = 5,
|
||||
allowed_updates: typing.Optional[typing.List[str]] = None):
|
||||
"""
|
||||
Start long-polling
|
||||
|
||||
|
|
@ -349,6 +350,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
:param limit:
|
||||
:param reset_webhook:
|
||||
:param fast:
|
||||
:param error_sleep:
|
||||
:param allowed_updates:
|
||||
:return:
|
||||
"""
|
||||
if self._polling:
|
||||
|
|
@ -377,10 +380,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
while self._polling:
|
||||
try:
|
||||
with self.bot.request_timeout(request_timeout):
|
||||
updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout)
|
||||
updates = await self.bot.get_updates(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
timeout=timeout,
|
||||
allowed_updates=allowed_updates
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except:
|
||||
except Exception as e:
|
||||
log.exception('Cause exception while getting updates.')
|
||||
await asyncio.sleep(error_sleep)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import typing
|
|||
import warnings
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Iterable, Optional, Union
|
||||
from typing import Any, Dict, Iterable, List, Optional, Union
|
||||
|
||||
from babel.support import LazyProxy
|
||||
|
||||
|
|
@ -110,7 +110,8 @@ class Command(Filter):
|
|||
if not text:
|
||||
return False
|
||||
|
||||
full_command = text.split()[0]
|
||||
full_command, *args_list = text.split(maxsplit=1)
|
||||
args = args_list[0] if args_list else None
|
||||
prefix, (command, _, mention) = full_command[0], full_command[1:].partition('@')
|
||||
|
||||
if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower():
|
||||
|
|
@ -120,7 +121,7 @@ class Command(Filter):
|
|||
if (command.lower() if ignore_case else command) not in commands:
|
||||
return False
|
||||
|
||||
return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention)}
|
||||
return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention, args=args)}
|
||||
|
||||
@dataclass
|
||||
class CommandObj:
|
||||
|
|
|
|||
|
|
@ -40,24 +40,27 @@ class BaseStorage:
|
|||
@classmethod
|
||||
def check_address(cls, *,
|
||||
chat: typing.Union[str, int, None] = None,
|
||||
user: typing.Union[str, int, None] = None) -> (typing.Union[str, int], typing.Union[str, int]):
|
||||
user: typing.Union[str, int, None] = None,
|
||||
) -> (typing.Union[str, int], typing.Union[str, int]):
|
||||
"""
|
||||
In all storage's methods chat or user is always required.
|
||||
If one of them is not provided, you have to set missing value based on the provided one.
|
||||
|
||||
This method performs the check described above.
|
||||
|
||||
:param chat:
|
||||
:param user:
|
||||
:param chat: chat_id
|
||||
:param user: user_id
|
||||
:return:
|
||||
"""
|
||||
if chat is None and user is None:
|
||||
raise ValueError('`user` or `chat` parameter is required but no one is provided!')
|
||||
|
||||
if user is None and chat is not None:
|
||||
if user is None:
|
||||
user = chat
|
||||
elif user is not None and chat is None:
|
||||
|
||||
elif chat is None:
|
||||
chat = user
|
||||
|
||||
return chat, user
|
||||
|
||||
async def get_state(self, *,
|
||||
|
|
@ -270,6 +273,21 @@ class BaseStorage:
|
|||
"""
|
||||
await self.set_data(chat=chat, user=user, data={})
|
||||
|
||||
@staticmethod
|
||||
def resolve_state(value):
|
||||
from .filters.state import State
|
||||
|
||||
if value is None:
|
||||
return
|
||||
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
|
||||
if isinstance(value, State):
|
||||
return value.state
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
class FSMContext:
|
||||
def __init__(self, storage, chat, user):
|
||||
|
|
@ -279,20 +297,8 @@ class FSMContext:
|
|||
def proxy(self):
|
||||
return FSMContextProxy(self)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_state(value):
|
||||
from .filters.state import State
|
||||
|
||||
if value is None:
|
||||
return
|
||||
elif isinstance(value, str):
|
||||
return value
|
||||
elif isinstance(value, State):
|
||||
return value.state
|
||||
return str(value)
|
||||
|
||||
async def get_state(self, default: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
return await self.storage.get_state(chat=self.chat, user=self.user, default=self._resolve_state(default))
|
||||
return await self.storage.get_state(chat=self.chat, user=self.user, default=default)
|
||||
|
||||
async def get_data(self, default: typing.Optional[str] = None) -> typing.Dict:
|
||||
return await self.storage.get_data(chat=self.chat, user=self.user, default=default)
|
||||
|
|
@ -301,7 +307,7 @@ class FSMContext:
|
|||
await self.storage.update_data(chat=self.chat, user=self.user, data=data, **kwargs)
|
||||
|
||||
async def set_state(self, state: typing.Optional[typing.AnyStr] = None):
|
||||
await self.storage.set_state(chat=self.chat, user=self.user, state=self._resolve_state(state))
|
||||
await self.storage.set_state(chat=self.chat, user=self.user, state=state)
|
||||
|
||||
async def set_data(self, data: typing.Dict = None):
|
||||
await self.storage.set_data(chat=self.chat, user=self.user, data=data)
|
||||
|
|
|
|||
|
|
@ -619,7 +619,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for
|
||||
Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data.
|
||||
:param caption: String (Optional) - Photo caption (may also be used when resending photos by file_id),
|
||||
0-200 characters
|
||||
0-1024 characters after entities parsing
|
||||
:param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive
|
||||
a notification with no sound.
|
||||
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
|
||||
|
|
@ -672,7 +672,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL
|
||||
as a String for Telegram to get an audio file from the Internet, or upload a new one
|
||||
using multipart/form-data.
|
||||
:param caption: String (Optional) - Audio caption, 0-200 characters
|
||||
:param caption: String (Optional) - Audio caption, 0-1024 characters after entities parsing
|
||||
:param duration: Integer (Optional) - Duration of the audio in seconds
|
||||
:param performer: String (Optional) - Performer
|
||||
:param title: String (Optional) - Track name
|
||||
|
|
@ -731,7 +731,7 @@ class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
as a String for Telegram to get a file from the Internet, or upload a new one
|
||||
using multipart/form-data.
|
||||
:param caption: String (Optional) - Document caption
|
||||
(may also be used when resending documents by file_id), 0-200 characters
|
||||
(may also be used when resending documents by file_id), 0-1024 characters after entities parsing
|
||||
:param disable_notification: Boolean (Optional) - Sends the message silently.
|
||||
Users will receive a notification with no sound.
|
||||
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
|
||||
|
|
@ -788,7 +788,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
:param width: Integer (Optional) - Video width
|
||||
:param height: Integer (Optional) - Video height
|
||||
:param caption: String (Optional) - Video caption (may also be used when resending videos by file_id),
|
||||
0-200 characters
|
||||
0-1024 characters after entities parsing
|
||||
:param disable_notification: Boolean (Optional) - Sends the message silently.
|
||||
Users will receive a notification with no sound.
|
||||
:param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message
|
||||
|
|
@ -845,7 +845,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
|
|||
to send a file that exists on the Telegram servers (recommended), pass an HTTP URL
|
||||
as a String for Telegram to get a file from the Internet, or upload a new one
|
||||
using multipart/form-data.
|
||||
:param caption: String (Optional) - Voice message caption, 0-200 characters
|
||||
:param caption: String (Optional) - Voice message caption, 0-1024 characters after entities parsing
|
||||
:param duration: Integer (Optional) - Duration of the voice message in seconds
|
||||
:param disable_notification: Boolean (Optional) - Sends the message silently.
|
||||
Users will receive a notification with no sound.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ from .animation import Animation
|
|||
from .audio import Audio
|
||||
from .auth_widget_data import AuthWidgetData
|
||||
from .bot_command import BotCommand
|
||||
from .bot_command_scope import BotCommandScope, BotCommandScopeAllChatAdministrators, \
|
||||
BotCommandScopeAllGroupChats, BotCommandScopeAllPrivateChats, BotCommandScopeChat, \
|
||||
BotCommandScopeChatAdministrators, BotCommandScopeChatMember, \
|
||||
BotCommandScopeDefault, BotCommandScopeType
|
||||
from .callback_game import CallbackGame
|
||||
from .callback_query import CallbackQuery
|
||||
from .chat import Chat, ChatActions, ChatType
|
||||
|
|
@ -35,7 +39,7 @@ from .input_file import InputFile
|
|||
from .input_media import InputMedia, InputMediaAnimation, InputMediaAudio, InputMediaDocument, InputMediaPhoto, \
|
||||
InputMediaVideo, MediaGroup
|
||||
from .input_message_content import InputContactMessageContent, InputLocationMessageContent, InputMessageContent, \
|
||||
InputTextMessageContent, InputVenueMessageContent
|
||||
InputTextMessageContent, InputVenueMessageContent, InputInvoiceMessageContent
|
||||
from .invoice import Invoice
|
||||
from .labeled_price import LabeledPrice
|
||||
from .location import Location
|
||||
|
|
@ -72,6 +76,7 @@ from .video_note import VideoNote
|
|||
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
|
||||
|
||||
|
|
@ -81,6 +86,15 @@ __all__ = (
|
|||
'Audio',
|
||||
'AuthWidgetData',
|
||||
'BotCommand',
|
||||
'BotCommandScope',
|
||||
'BotCommandScopeAllChatAdministrators',
|
||||
'BotCommandScopeAllGroupChats',
|
||||
'BotCommandScopeAllPrivateChats',
|
||||
'BotCommandScopeChat',
|
||||
'BotCommandScopeChatAdministrators',
|
||||
'BotCommandScopeChatMember',
|
||||
'BotCommandScopeDefault',
|
||||
'BotCommandScopeType',
|
||||
'CallbackGame',
|
||||
'CallbackQuery',
|
||||
'Chat',
|
||||
|
|
@ -131,6 +145,7 @@ __all__ = (
|
|||
'InlineQueryResultVideo',
|
||||
'InlineQueryResultVoice',
|
||||
'InputContactMessageContent',
|
||||
'InputInvoiceMessageContent',
|
||||
'InputFile',
|
||||
'InputLocationMessageContent',
|
||||
'InputMedia',
|
||||
|
|
@ -191,6 +206,7 @@ __all__ = (
|
|||
'Voice',
|
||||
'VoiceChatEnded',
|
||||
'VoiceChatParticipantsInvited',
|
||||
'VoiceChatScheduled',
|
||||
'VoiceChatStarted',
|
||||
'WebhookInfo',
|
||||
'base',
|
||||
|
|
|
|||
121
aiogram/types/bot_command_scope.py
Normal file
121
aiogram/types/bot_command_scope.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import typing
|
||||
|
||||
from . import base, fields
|
||||
from ..utils import helper
|
||||
|
||||
|
||||
class BotCommandScopeType(helper.Helper):
|
||||
mode = helper.HelperMode.snake_case
|
||||
|
||||
DEFAULT = helper.Item() # default
|
||||
ALL_PRIVATE_CHATS = helper.Item() # all_private_chats
|
||||
ALL_GROUP_CHATS = helper.Item() # all_group_chats
|
||||
ALL_CHAT_ADMINISTRATORS = helper.Item() # all_chat_administrators
|
||||
CHAT = helper.Item() # chat
|
||||
CHAT_ADMINISTRATORS = helper.Item() # chat_administrators
|
||||
CHAT_MEMBER = helper.Item() # chat_member
|
||||
|
||||
|
||||
class BotCommandScope(base.TelegramObject):
|
||||
"""
|
||||
This object represents the scope to which bot commands are applied.
|
||||
Currently, the following 7 scopes are supported:
|
||||
BotCommandScopeDefault
|
||||
BotCommandScopeAllPrivateChats
|
||||
BotCommandScopeAllGroupChats
|
||||
BotCommandScopeAllChatAdministrators
|
||||
BotCommandScopeChat
|
||||
BotCommandScopeChatAdministrators
|
||||
BotCommandScopeChatMember
|
||||
|
||||
https://core.telegram.org/bots/api#botcommandscope
|
||||
"""
|
||||
type: base.String = fields.Field()
|
||||
|
||||
@classmethod
|
||||
def from_type(cls, type: str, **kwargs: typing.Any):
|
||||
if type == BotCommandScopeType.DEFAULT:
|
||||
return BotCommandScopeDefault(type=type, **kwargs)
|
||||
if type == BotCommandScopeType.ALL_PRIVATE_CHATS:
|
||||
return BotCommandScopeAllPrivateChats(type=type, **kwargs)
|
||||
if type == BotCommandScopeType.ALL_GROUP_CHATS:
|
||||
return BotCommandScopeAllGroupChats(type=type, **kwargs)
|
||||
if type == BotCommandScopeType.ALL_CHAT_ADMINISTRATORS:
|
||||
return BotCommandScopeAllChatAdministrators(type=type, **kwargs)
|
||||
if type == BotCommandScopeType.CHAT:
|
||||
return BotCommandScopeChat(type=type, **kwargs)
|
||||
if type == BotCommandScopeType.CHAT_ADMINISTRATORS:
|
||||
return BotCommandScopeChatAdministrators(type=type, **kwargs)
|
||||
if type == BotCommandScopeType.CHAT_MEMBER:
|
||||
return BotCommandScopeChatMember(type=type, **kwargs)
|
||||
raise ValueError(f"Unknown BotCommandScope type {type!r}")
|
||||
|
||||
|
||||
class BotCommandScopeDefault(BotCommandScope):
|
||||
"""
|
||||
Represents the default scope of bot commands.
|
||||
Default commands are used if no commands with a narrower scope are
|
||||
specified for the user.
|
||||
"""
|
||||
type = fields.Field(default=BotCommandScopeType.DEFAULT)
|
||||
|
||||
|
||||
class BotCommandScopeAllPrivateChats(BotCommandScope):
|
||||
"""
|
||||
Represents the scope of bot commands, covering all private chats.
|
||||
"""
|
||||
type = fields.Field(default=BotCommandScopeType.ALL_PRIVATE_CHATS)
|
||||
|
||||
|
||||
class BotCommandScopeAllGroupChats(BotCommandScope):
|
||||
"""
|
||||
Represents the scope of bot commands, covering all group and
|
||||
supergroup chats.
|
||||
"""
|
||||
type = fields.Field(default=BotCommandScopeType.ALL_GROUP_CHATS)
|
||||
|
||||
|
||||
class BotCommandScopeAllChatAdministrators(BotCommandScope):
|
||||
"""
|
||||
Represents the scope of bot commands, covering all group and
|
||||
supergroup chat administrators.
|
||||
"""
|
||||
type = fields.Field(default=BotCommandScopeType.ALL_CHAT_ADMINISTRATORS)
|
||||
|
||||
|
||||
class BotCommandScopeChat(BotCommandScope):
|
||||
"""
|
||||
Represents the scope of bot commands, covering a specific chat.
|
||||
"""
|
||||
type = fields.Field(default=BotCommandScopeType.CHAT)
|
||||
chat_id: typing.Union[base.String, base.Integer] = fields.Field()
|
||||
|
||||
def __init__(self, chat_id: typing.Union[base.String, base.Integer], **kwargs):
|
||||
super().__init__(chat_id=chat_id, **kwargs)
|
||||
|
||||
|
||||
class BotCommandScopeChatAdministrators(BotCommandScopeChat):
|
||||
"""
|
||||
Represents the scope of bot commands, covering all administrators
|
||||
of a specific group or supergroup chat.
|
||||
"""
|
||||
type = fields.Field(default=BotCommandScopeType.CHAT_ADMINISTRATORS)
|
||||
chat_id: typing.Union[base.String, base.Integer] = fields.Field()
|
||||
|
||||
|
||||
class BotCommandScopeChatMember(BotCommandScopeChat):
|
||||
"""
|
||||
Represents the scope of bot commands, covering a specific member of
|
||||
a group or supergroup chat.
|
||||
"""
|
||||
type = fields.Field(default=BotCommandScopeType.CHAT_MEMBER)
|
||||
chat_id: typing.Union[base.String, base.Integer] = fields.Field()
|
||||
user_id: base.Integer = fields.Field()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chat_id: typing.Union[base.String, base.Integer],
|
||||
user_id: base.Integer,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(chat_id=chat_id, user_id=user_id, **kwargs)
|
||||
|
|
@ -301,7 +301,7 @@ class Chat(base.TelegramObject):
|
|||
can_send_other_messages=can_send_other_messages,
|
||||
can_add_web_page_previews=can_add_web_page_previews)
|
||||
|
||||
async def promote(self,
|
||||
async def promote(self,
|
||||
user_id: base.Integer,
|
||||
is_anonymous: typing.Optional[base.Boolean] = None,
|
||||
can_change_info: typing.Optional[base.Boolean] = None,
|
||||
|
|
@ -321,36 +321,36 @@ class Chat(base.TelegramObject):
|
|||
|
||||
:param user_id: Unique identifier of the target user
|
||||
:type user_id: :obj:`base.Integer`
|
||||
|
||||
|
||||
:param is_anonymous: Pass True, if the administrator's presence in the chat is hidden
|
||||
:type is_anonymous: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:param can_change_info: Pass True, if the administrator can change chat title, photo and other settings
|
||||
:type can_change_info: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:param can_post_messages: Pass True, if the administrator can create channel posts, channels only
|
||||
:type can_post_messages: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:param can_edit_messages: Pass True, if the administrator can edit messages of other users, channels only
|
||||
:type can_edit_messages: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:param can_delete_messages: Pass True, if the administrator can delete messages of other users
|
||||
:type can_delete_messages: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:param can_invite_users: Pass True, if the administrator can invite new users to the chat
|
||||
:type can_invite_users: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members
|
||||
:type can_restrict_members: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only
|
||||
:type can_pin_messages: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:param can_promote_members: Pass True, if the administrator can add new administrators
|
||||
with a subset of his own privileges or demote administrators that he has promoted,
|
||||
directly or indirectly (promoted by administrators that were appointed by him)
|
||||
:type can_promote_members: :obj:`typing.Optional[base.Boolean]`
|
||||
|
||||
|
||||
:return: Returns True on success.
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
|
|
@ -484,16 +484,20 @@ class Chat(base.TelegramObject):
|
|||
"""
|
||||
return await self.bot.get_chat_administrators(self.id)
|
||||
|
||||
async def get_members_count(self) -> base.Integer:
|
||||
async def get_member_count(self) -> base.Integer:
|
||||
"""
|
||||
Use this method to get the number of members in a chat.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#getchatmemberscount
|
||||
Source: https://core.telegram.org/bots/api#getchatmembercount
|
||||
|
||||
:return: Returns Int on success.
|
||||
:rtype: :obj:`base.Integer`
|
||||
"""
|
||||
return await self.bot.get_chat_members_count(self.id)
|
||||
return await self.bot.get_chat_member_count(self.id)
|
||||
|
||||
async def get_members_count(self) -> base.Integer:
|
||||
"""Renamed to get_member_count."""
|
||||
return await self.get_member_count(self.id)
|
||||
|
||||
async def get_member(self, user_id: base.Integer) -> ChatMember:
|
||||
"""
|
||||
|
|
@ -732,6 +736,8 @@ class ChatActions(helper.Helper):
|
|||
UPLOAD_VIDEO: str = helper.Item() # upload_video
|
||||
RECORD_AUDIO: str = helper.Item() # record_audio
|
||||
UPLOAD_AUDIO: str = helper.Item() # upload_audio
|
||||
RECORD_VOICE: str = helper.Item() # record_voice
|
||||
UPLOAD_VOICE: str = helper.Item() # upload_voice
|
||||
UPLOAD_DOCUMENT: str = helper.Item() # upload_document
|
||||
FIND_LOCATION: str = helper.Item() # find_location
|
||||
RECORD_VIDEO_NOTE: str = helper.Item() # record_video_note
|
||||
|
|
@ -817,6 +823,26 @@ class ChatActions(helper.Helper):
|
|||
"""
|
||||
await cls._do(cls.UPLOAD_AUDIO, sleep)
|
||||
|
||||
@classmethod
|
||||
async def record_voice(cls, sleep=None):
|
||||
"""
|
||||
Do record voice
|
||||
|
||||
:param sleep: sleep timeout
|
||||
:return:
|
||||
"""
|
||||
await cls._do(cls.RECORD_VOICE, sleep)
|
||||
|
||||
@classmethod
|
||||
async def upload_voice(cls, sleep=None):
|
||||
"""
|
||||
Do upload voice
|
||||
|
||||
:param sleep: sleep timeout
|
||||
:return:
|
||||
"""
|
||||
await cls._do(cls.UPLOAD_VOICE, sleep)
|
||||
|
||||
@classmethod
|
||||
async def upload_document(cls, sleep=None):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,53 +1,11 @@
|
|||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
from . import base, fields
|
||||
from .user import User
|
||||
from ..utils import helper
|
||||
|
||||
|
||||
class ChatMember(base.TelegramObject):
|
||||
"""
|
||||
This object contains information about one member of a chat.
|
||||
|
||||
https://core.telegram.org/bots/api#chatmember
|
||||
"""
|
||||
user: User = fields.Field(base=User)
|
||||
status: base.String = fields.Field()
|
||||
custom_title: base.String = fields.Field()
|
||||
is_anonymous: base.Boolean = fields.Field()
|
||||
can_be_edited: base.Boolean = fields.Field()
|
||||
can_manage_chat: base.Boolean = fields.Field()
|
||||
can_post_messages: base.Boolean = fields.Field()
|
||||
can_edit_messages: base.Boolean = fields.Field()
|
||||
can_delete_messages: base.Boolean = fields.Field()
|
||||
can_manage_voice_chats: base.Boolean = fields.Field()
|
||||
can_restrict_members: base.Boolean = fields.Field()
|
||||
can_promote_members: base.Boolean = fields.Field()
|
||||
can_change_info: base.Boolean = fields.Field()
|
||||
can_invite_users: base.Boolean = fields.Field()
|
||||
can_pin_messages: base.Boolean = fields.Field()
|
||||
is_member: base.Boolean = fields.Field()
|
||||
can_send_messages: base.Boolean = fields.Field()
|
||||
can_send_media_messages: base.Boolean = fields.Field()
|
||||
can_send_polls: base.Boolean = fields.Field()
|
||||
can_send_other_messages: base.Boolean = fields.Field()
|
||||
can_add_web_page_previews: base.Boolean = fields.Field()
|
||||
until_date: datetime.datetime = fields.DateTimeField()
|
||||
|
||||
def is_chat_creator(self) -> bool:
|
||||
return ChatMemberStatus.is_chat_creator(self.status)
|
||||
|
||||
def is_chat_admin(self) -> bool:
|
||||
return ChatMemberStatus.is_chat_admin(self.status)
|
||||
|
||||
def is_chat_member(self) -> bool:
|
||||
return ChatMemberStatus.is_chat_member(self.status)
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.user.id
|
||||
|
||||
|
||||
class ChatMemberStatus(helper.Helper):
|
||||
"""
|
||||
Chat member status
|
||||
|
|
@ -55,11 +13,13 @@ class ChatMemberStatus(helper.Helper):
|
|||
mode = helper.HelperMode.lowercase
|
||||
|
||||
CREATOR = helper.Item() # creator
|
||||
OWNER = CREATOR # creator
|
||||
ADMINISTRATOR = helper.Item() # administrator
|
||||
MEMBER = helper.Item() # member
|
||||
RESTRICTED = helper.Item() # restricted
|
||||
LEFT = helper.Item() # left
|
||||
KICKED = helper.Item() # kicked
|
||||
BANNED = KICKED # kicked
|
||||
|
||||
@classmethod
|
||||
def is_chat_creator(cls, role: str) -> bool:
|
||||
|
|
@ -72,3 +32,141 @@ class ChatMemberStatus(helper.Helper):
|
|||
@classmethod
|
||||
def is_chat_member(cls, role: str) -> bool:
|
||||
return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED)
|
||||
|
||||
@classmethod
|
||||
def get_class_by_status(cls, status: str) -> Optional["ChatMember"]:
|
||||
return {
|
||||
cls.OWNER: ChatMemberOwner,
|
||||
cls.ADMINISTRATOR: ChatMemberAdministrator,
|
||||
cls.MEMBER: ChatMemberMember,
|
||||
cls.RESTRICTED: ChatMemberRestricted,
|
||||
cls.LEFT: ChatMemberLeft,
|
||||
cls.BANNED: ChatMemberBanned,
|
||||
}.get(status)
|
||||
|
||||
|
||||
class ChatMember(base.TelegramObject):
|
||||
"""
|
||||
This object contains information about one member of a chat.
|
||||
Currently, the following 6 types of chat members are supported:
|
||||
ChatMemberOwner
|
||||
ChatMemberAdministrator
|
||||
ChatMemberMember
|
||||
ChatMemberRestricted
|
||||
ChatMemberLeft
|
||||
ChatMemberBanned
|
||||
|
||||
https://core.telegram.org/bots/api#chatmember
|
||||
"""
|
||||
status: base.String = fields.Field()
|
||||
user: User = fields.Field(base=User)
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.user.id
|
||||
|
||||
@classmethod
|
||||
def resolve(cls, **kwargs) -> "ChatMember":
|
||||
status = kwargs.get("status")
|
||||
mapping = {
|
||||
ChatMemberStatus.OWNER: ChatMemberOwner,
|
||||
ChatMemberStatus.ADMINISTRATOR: ChatMemberAdministrator,
|
||||
ChatMemberStatus.MEMBER: ChatMemberMember,
|
||||
ChatMemberStatus.RESTRICTED: ChatMemberRestricted,
|
||||
ChatMemberStatus.LEFT: ChatMemberLeft,
|
||||
ChatMemberStatus.BANNED: ChatMemberBanned,
|
||||
}
|
||||
class_ = mapping.get(status)
|
||||
if class_ is None:
|
||||
raise ValueError(f"Can't find `ChatMember` class for status `{status}`")
|
||||
|
||||
return class_(**kwargs)
|
||||
|
||||
|
||||
class ChatMemberOwner(ChatMember):
|
||||
"""
|
||||
Represents a chat member that owns the chat and has all
|
||||
administrator privileges.
|
||||
https://core.telegram.org/bots/api#chatmemberowner
|
||||
"""
|
||||
status: base.String = fields.Field(default=ChatMemberStatus.OWNER)
|
||||
user: User = fields.Field(base=User)
|
||||
custom_title: base.String = fields.Field()
|
||||
is_anonymous: base.Boolean = fields.Field()
|
||||
|
||||
|
||||
class ChatMemberAdministrator(ChatMember):
|
||||
"""
|
||||
Represents a chat member that has some additional privileges.
|
||||
|
||||
https://core.telegram.org/bots/api#chatmemberadministrator
|
||||
"""
|
||||
status: base.String = fields.Field(default=ChatMemberStatus.ADMINISTRATOR)
|
||||
user: User = fields.Field(base=User)
|
||||
can_be_edited: base.Boolean = fields.Field()
|
||||
custom_title: base.String = fields.Field()
|
||||
is_anonymous: base.Boolean = fields.Field()
|
||||
can_manage_chat: base.Boolean = fields.Field()
|
||||
can_post_messages: base.Boolean = fields.Field()
|
||||
can_edit_messages: base.Boolean = fields.Field()
|
||||
can_delete_messages: base.Boolean = fields.Field()
|
||||
can_manage_voice_chats: base.Boolean = fields.Field()
|
||||
can_restrict_members: base.Boolean = fields.Field()
|
||||
can_promote_members: base.Boolean = fields.Field()
|
||||
can_change_info: base.Boolean = fields.Field()
|
||||
can_invite_users: base.Boolean = fields.Field()
|
||||
can_pin_messages: base.Boolean = fields.Field()
|
||||
|
||||
|
||||
class ChatMemberMember(ChatMember):
|
||||
"""
|
||||
Represents a chat member that has no additional privileges or
|
||||
restrictions.
|
||||
|
||||
https://core.telegram.org/bots/api#chatmembermember
|
||||
"""
|
||||
status: base.String = fields.Field(default=ChatMemberStatus.MEMBER)
|
||||
user: User = fields.Field(base=User)
|
||||
|
||||
|
||||
class ChatMemberRestricted(ChatMember):
|
||||
"""
|
||||
Represents a chat member that is under certain restrictions in the
|
||||
chat. Supergroups only.
|
||||
|
||||
https://core.telegram.org/bots/api#chatmemberrestricted
|
||||
"""
|
||||
status: base.String = fields.Field(default=ChatMemberStatus.RESTRICTED)
|
||||
user: User = fields.Field(base=User)
|
||||
is_member: base.Boolean = fields.Field()
|
||||
can_change_info: base.Boolean = fields.Field()
|
||||
can_invite_users: base.Boolean = fields.Field()
|
||||
can_pin_messages: base.Boolean = fields.Field()
|
||||
can_send_messages: base.Boolean = fields.Field()
|
||||
can_send_media_messages: base.Boolean = fields.Field()
|
||||
can_send_polls: base.Boolean = fields.Field()
|
||||
can_send_other_messages: base.Boolean = fields.Field()
|
||||
can_add_web_page_previews: base.Boolean = fields.Field()
|
||||
until_date: datetime.datetime = fields.DateTimeField()
|
||||
|
||||
|
||||
class ChatMemberLeft(ChatMember):
|
||||
"""
|
||||
Represents a chat member that isn't currently a member of the chat,
|
||||
but may join it themselves.
|
||||
|
||||
https://core.telegram.org/bots/api#chatmemberleft
|
||||
"""
|
||||
status: base.String = fields.Field(default=ChatMemberStatus.LEFT)
|
||||
user: User = fields.Field(base=User)
|
||||
|
||||
|
||||
class ChatMemberBanned(ChatMember):
|
||||
"""
|
||||
Represents a chat member that was banned in the chat and can't
|
||||
return to the chat or view chat messages.
|
||||
|
||||
https://core.telegram.org/bots/api#chatmemberbanned
|
||||
"""
|
||||
status: base.String = fields.Field(default=ChatMemberStatus.BANNED)
|
||||
user: User = fields.Field(base=User)
|
||||
until_date: datetime.datetime = fields.DateTimeField()
|
||||
|
|
|
|||
|
|
@ -6,31 +6,28 @@ from . import fields
|
|||
|
||||
class ForceReply(base.TelegramObject):
|
||||
"""
|
||||
Upon receiving a message with this object,
|
||||
Telegram clients will display a reply interface to the user
|
||||
(act as if the user has selected the bot‘s message and tapped ’Reply').
|
||||
This can be extremely useful if you want to create user-friendly step-by-step
|
||||
Upon receiving a message with this object, Telegram clients will
|
||||
display a reply interface to the user (act as if the user has
|
||||
selected the bot's message and tapped 'Reply'). This can be
|
||||
extremely useful if you want to create user-friendly step-by-step
|
||||
interfaces without having to sacrifice privacy mode.
|
||||
|
||||
Example: A poll bot for groups runs in privacy mode
|
||||
(only receives commands, replies to its messages and mentions).
|
||||
There could be two ways to create a new poll
|
||||
|
||||
The last option is definitely more attractive.
|
||||
And if you use ForceReply in your bot‘s questions, it will receive the user’s answers even
|
||||
if it only receives replies, commands and mentions — without any extra work for the user.
|
||||
|
||||
https://core.telegram.org/bots/api#forcereply
|
||||
"""
|
||||
force_reply: base.Boolean = fields.Field(default=True)
|
||||
input_field_placeholder: base.String = fields.Field()
|
||||
selective: base.Boolean = fields.Field()
|
||||
|
||||
@classmethod
|
||||
def create(cls, selective: typing.Optional[base.Boolean] = None):
|
||||
def create(cls,
|
||||
input_field_placeholder: typing.Optional[base.String] = None,
|
||||
selective: typing.Optional[base.Boolean] = None,
|
||||
) -> 'ForceReply':
|
||||
"""
|
||||
Create new force reply
|
||||
|
||||
:param selective:
|
||||
:param input_field_placeholder:
|
||||
:return:
|
||||
"""
|
||||
return cls(selective=selective)
|
||||
return cls(selective=selective, input_field_placeholder=input_field_placeholder)
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ class InlineQuery(base.TelegramObject):
|
|||
"""
|
||||
id: base.String = fields.Field()
|
||||
from_user: User = fields.Field(alias='from', base=User)
|
||||
location: Location = fields.Field(base=Location)
|
||||
query: base.String = fields.Field()
|
||||
offset: base.String = fields.Field()
|
||||
chat_type: base.String = fields.Field()
|
||||
location: Location = fields.Field(base=Location)
|
||||
|
||||
async def answer(self,
|
||||
results: typing.List[InlineQueryResult],
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class InputMedia(base.TelegramObject):
|
|||
thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed')
|
||||
caption: base.String = fields.Field()
|
||||
parse_mode: base.String = fields.Field()
|
||||
caption_entities: typing.List[MessageEntity] = fields.ListField(base=MessageEntity)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._thumb_file = None
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import typing
|
|||
from . import base
|
||||
from . import fields
|
||||
from .message_entity import MessageEntity
|
||||
from .labeled_price import LabeledPrice
|
||||
from ..utils.payload import generate_payload
|
||||
|
||||
|
||||
class InputMessageContent(base.TelegramObject):
|
||||
|
|
@ -44,6 +46,66 @@ class InputContactMessageContent(InputMessageContent):
|
|||
)
|
||||
|
||||
|
||||
class InputInvoiceMessageContent(InputMessageContent):
|
||||
"""
|
||||
Represents the content of an invoice message to be sent as the
|
||||
result of an inline query.
|
||||
|
||||
https://core.telegram.org/bots/api#inputinvoicemessagecontent
|
||||
"""
|
||||
|
||||
title: base.String = fields.Field()
|
||||
description: base.String = fields.Field()
|
||||
payload: base.String = fields.Field()
|
||||
provider_token: base.String = fields.Field()
|
||||
currency: base.String = fields.Field()
|
||||
prices: typing.List[LabeledPrice] = fields.ListField(base=LabeledPrice)
|
||||
max_tip_amount: typing.Optional[base.Integer] = fields.Field()
|
||||
suggested_tip_amounts: typing.Optional[
|
||||
typing.List[base.Integer]
|
||||
] = fields.ListField(base=base.Integer)
|
||||
provider_data: typing.Optional[base.String] = fields.Field()
|
||||
photo_url: typing.Optional[base.String] = fields.Field()
|
||||
photo_size: typing.Optional[base.Integer] = fields.Field()
|
||||
photo_width: typing.Optional[base.Integer] = fields.Field()
|
||||
photo_height: typing.Optional[base.Integer] = fields.Field()
|
||||
need_name: typing.Optional[base.Boolean] = fields.Field()
|
||||
need_phone_number: typing.Optional[base.Boolean] = fields.Field()
|
||||
need_email: typing.Optional[base.Boolean] = fields.Field()
|
||||
need_shipping_address: typing.Optional[base.Boolean] = fields.Field()
|
||||
send_phone_number_to_provider: typing.Optional[base.Boolean] = fields.Field()
|
||||
send_email_to_provider: typing.Optional[base.Boolean] = fields.Field()
|
||||
is_flexible: typing.Optional[base.Boolean] = fields.Field()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: base.String,
|
||||
description: base.String,
|
||||
payload: base.String,
|
||||
provider_token: base.String,
|
||||
currency: base.String,
|
||||
prices: typing.List[LabeledPrice] = None,
|
||||
max_tip_amount: typing.Optional[base.Integer] = None,
|
||||
suggested_tip_amounts: typing.Optional[typing.List[base.Integer]] = None,
|
||||
provider_data: typing.Optional[base.String] = None,
|
||||
photo_url: typing.Optional[base.String] = None,
|
||||
photo_size: typing.Optional[base.Integer] = None,
|
||||
photo_width: typing.Optional[base.Integer] = None,
|
||||
photo_height: typing.Optional[base.Integer] = None,
|
||||
need_name: typing.Optional[base.Boolean] = None,
|
||||
need_phone_number: typing.Optional[base.Boolean] = None,
|
||||
need_email: typing.Optional[base.Boolean] = None,
|
||||
need_shipping_address: typing.Optional[base.Boolean] = None,
|
||||
send_phone_number_to_provider: typing.Optional[base.Boolean] = None,
|
||||
send_email_to_provider: typing.Optional[base.Boolean] = None,
|
||||
is_flexible: typing.Optional[base.Boolean] = None,
|
||||
):
|
||||
if prices is None:
|
||||
prices = []
|
||||
payload = generate_payload(**locals())
|
||||
super().__init__(**payload)
|
||||
|
||||
|
||||
class InputLocationMessageContent(InputMessageContent):
|
||||
"""
|
||||
Represents the content of a location message to be sent as the result of an inline query.
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from .video_note import VideoNote
|
|||
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 ..utils import helper
|
||||
from ..utils import markdown as md
|
||||
|
|
@ -98,6 +99,7 @@ class Message(base.TelegramObject):
|
|||
connected_website: base.String = fields.Field()
|
||||
passport_data: PassportData = fields.Field(base=PassportData)
|
||||
proximity_alert_triggered: ProximityAlertTriggered = fields.Field(base=ProximityAlertTriggered)
|
||||
voice_chat_scheduled: VoiceChatScheduled = fields.Field(base=VoiceChatScheduled)
|
||||
voice_chat_started: VoiceChatStarted = fields.Field(base=VoiceChatStarted)
|
||||
voice_chat_ended: VoiceChatEnded = fields.Field(base=VoiceChatEnded)
|
||||
voice_chat_participants_invited: VoiceChatParticipantsInvited = fields.Field(base=VoiceChatParticipantsInvited)
|
||||
|
|
@ -166,6 +168,8 @@ class Message(base.TelegramObject):
|
|||
return ContentType.PASSPORT_DATA
|
||||
if self.proximity_alert_triggered:
|
||||
return ContentType.PROXIMITY_ALERT_TRIGGERED
|
||||
if self.voice_chat_scheduled:
|
||||
return ContentType.VOICE_CHAT_SCHEDULED
|
||||
if self.voice_chat_started:
|
||||
return ContentType.VOICE_CHAT_STARTED
|
||||
if self.voice_chat_ended:
|
||||
|
|
@ -190,7 +194,8 @@ class Message(base.TelegramObject):
|
|||
|
||||
:return: bool
|
||||
"""
|
||||
return self.text and self.text.startswith("/")
|
||||
text = self.text or self.caption
|
||||
return text and text.startswith("/")
|
||||
|
||||
def get_full_command(self) -> typing.Optional[typing.Tuple[str, str]]:
|
||||
"""
|
||||
|
|
@ -199,8 +204,9 @@ class Message(base.TelegramObject):
|
|||
:return: tuple of (command, args)
|
||||
"""
|
||||
if self.is_command():
|
||||
command, *args = self.text.split(maxsplit=1)
|
||||
args = args[-1] if args else ""
|
||||
text = self.text or self.caption
|
||||
command, *args = text.split(maxsplit=1)
|
||||
args = args[0] if args else ""
|
||||
return command, args
|
||||
|
||||
def get_command(self, pure=False) -> typing.Optional[str]:
|
||||
|
|
@ -267,7 +273,8 @@ class Message(base.TelegramObject):
|
|||
|
||||
:return: str
|
||||
"""
|
||||
if ChatType.is_private(self.chat):
|
||||
|
||||
if self.chat.type == ChatType.PRIVATE:
|
||||
raise TypeError("Invalid chat type!")
|
||||
url = "https://t.me/"
|
||||
if self.chat.username:
|
||||
|
|
@ -461,7 +468,7 @@ class Message(base.TelegramObject):
|
|||
:param audio: Audio file to send.
|
||||
:type audio: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
|
||||
:param caption: Audio caption, 0-200 characters
|
||||
:param caption: Audio caption, 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -738,7 +745,7 @@ class Message(base.TelegramObject):
|
|||
A thumbnail‘s width and height should not exceed 320.
|
||||
:type thumb: :obj:`typing.Union[base.InputFile, base.String, None]`
|
||||
|
||||
:param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters
|
||||
:param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -817,7 +824,7 @@ class Message(base.TelegramObject):
|
|||
:param voice: Audio file to send.
|
||||
:type voice: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
|
||||
:param caption: Voice message caption, 0-200 characters
|
||||
:param caption: Voice message caption, 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -1415,7 +1422,7 @@ class Message(base.TelegramObject):
|
|||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
)
|
||||
|
||||
|
||||
async def answer_chat_action(
|
||||
self,
|
||||
action: base.String,
|
||||
|
|
@ -1602,7 +1609,7 @@ class Message(base.TelegramObject):
|
|||
:param audio: Audio file to send.
|
||||
:type audio: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
|
||||
:param caption: Audio caption, 0-200 characters
|
||||
:param caption: Audio caption, 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -1879,7 +1886,7 @@ class Message(base.TelegramObject):
|
|||
A thumbnail‘s width and height should not exceed 320.
|
||||
:type thumb: :obj:`typing.Union[base.InputFile, base.String, None]`
|
||||
|
||||
:param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters
|
||||
:param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -1958,7 +1965,7 @@ class Message(base.TelegramObject):
|
|||
:param voice: Audio file to send.
|
||||
:type voice: :obj:`typing.Union[base.InputFile, base.String]`
|
||||
|
||||
:param caption: Voice message caption, 0-200 characters
|
||||
:param caption: Voice message caption, 0-1024 characters after entities parsing
|
||||
:type caption: :obj:`typing.Optional[base.String]`
|
||||
|
||||
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
|
||||
|
|
@ -2926,7 +2933,7 @@ class Message(base.TelegramObject):
|
|||
question=self.poll.question,
|
||||
options=[option.text for option in self.poll.options],
|
||||
is_anonymous=self.poll.is_anonymous,
|
||||
allows_multiple_answers=self.poll.allows_multiple_answers
|
||||
allows_multiple_answers=self.poll.allows_multiple_answers,
|
||||
**kwargs,
|
||||
)
|
||||
elif self.dice:
|
||||
|
|
@ -3032,6 +3039,7 @@ class ContentType(helper.Helper):
|
|||
GROUP_CHAT_CREATED = helper.Item() # group_chat_created
|
||||
PASSPORT_DATA = helper.Item() # passport_data
|
||||
PROXIMITY_ALERT_TRIGGERED = helper.Item() # proximity_alert_triggered
|
||||
VOICE_CHAT_SCHEDULED = helper.Item() # voice_chat_scheduled
|
||||
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
|
||||
|
|
|
|||
|
|
@ -18,23 +18,32 @@ class KeyboardButtonPollType(base.TelegramObject):
|
|||
|
||||
class ReplyKeyboardMarkup(base.TelegramObject):
|
||||
"""
|
||||
This object represents a custom keyboard with reply options (see Introduction to bots for details and examples).
|
||||
This object represents a custom keyboard with reply options
|
||||
(see https://core.telegram.org/bots#keyboards to bots for details
|
||||
and examples).
|
||||
|
||||
https://core.telegram.org/bots/api#replykeyboardmarkup
|
||||
"""
|
||||
keyboard: 'typing.List[typing.List[KeyboardButton]]' = fields.ListOfLists(base='KeyboardButton', default=[])
|
||||
resize_keyboard: base.Boolean = fields.Field()
|
||||
one_time_keyboard: base.Boolean = fields.Field()
|
||||
input_field_placeholder: base.String = fields.Field()
|
||||
selective: base.Boolean = fields.Field()
|
||||
|
||||
def __init__(self, keyboard: 'typing.List[typing.List[KeyboardButton]]' = None,
|
||||
resize_keyboard: base.Boolean = None,
|
||||
one_time_keyboard: base.Boolean = None,
|
||||
input_field_placeholder: base.String = None,
|
||||
selective: base.Boolean = None,
|
||||
row_width: base.Integer = 3):
|
||||
super(ReplyKeyboardMarkup, self).__init__(keyboard=keyboard, resize_keyboard=resize_keyboard,
|
||||
one_time_keyboard=one_time_keyboard, selective=selective,
|
||||
conf={'row_width': row_width})
|
||||
super().__init__(
|
||||
keyboard=keyboard,
|
||||
resize_keyboard=resize_keyboard,
|
||||
one_time_keyboard=one_time_keyboard,
|
||||
input_field_placeholder=input_field_placeholder,
|
||||
selective=selective,
|
||||
conf={'row_width': row_width},
|
||||
)
|
||||
|
||||
@property
|
||||
def row_width(self):
|
||||
|
|
|
|||
|
|
@ -72,3 +72,7 @@ class AllowedUpdates(helper.Helper):
|
|||
"Use `CHOSEN_INLINE_RESULT`",
|
||||
new_value_getter=lambda cls: cls.CHOSEN_INLINE_RESULT,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
return []
|
||||
|
|
|
|||
15
aiogram/types/voice_chat_scheduled.py
Normal file
15
aiogram/types/voice_chat_scheduled.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from datetime import datetime
|
||||
|
||||
from . import base
|
||||
from . import fields
|
||||
from .user import User
|
||||
|
||||
|
||||
class VoiceChatScheduled(base.TelegramObject):
|
||||
"""
|
||||
This object represents a service message about a voice chat scheduled in the chat.
|
||||
|
||||
https://core.telegram.org/bots/api#voicechatscheduled
|
||||
"""
|
||||
|
||||
start_date: datetime = fields.DateTimeField()
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
"""
|
||||
Deep linking
|
||||
|
||||
Telegram bots have a deep linking mechanism, that allows for passing additional
|
||||
parameters to the bot on startup. It could be a command that launches the bot — or
|
||||
an auth token to connect the user's Telegram account to their account on some
|
||||
external service.
|
||||
Telegram bots have a deep linking mechanism, that allows for passing
|
||||
additional parameters to the bot on startup. It could be a command that
|
||||
launches the bot — or an auth token to connect the user's Telegram
|
||||
account to their account on some external service.
|
||||
|
||||
You can read detailed description in the source:
|
||||
https://core.telegram.org/bots#deep-linking
|
||||
|
|
@ -16,86 +16,123 @@ Basic link example:
|
|||
.. code-block:: python
|
||||
|
||||
from aiogram.utils.deep_linking import get_start_link
|
||||
link = await get_start_link('foo') # result: 'https://t.me/MyBot?start=foo'
|
||||
link = await get_start_link('foo')
|
||||
|
||||
# result: 'https://t.me/MyBot?start=foo'
|
||||
|
||||
Encoded link example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from aiogram.utils.deep_linking import get_start_link, decode_payload
|
||||
link = await get_start_link('foo', encode=True) # result: 'https://t.me/MyBot?start=Zm9v'
|
||||
# and decode it back:
|
||||
payload = decode_payload('Zm9v') # result: 'foo'
|
||||
from aiogram.utils.deep_linking import get_start_link
|
||||
|
||||
link = await get_start_link('foo', encode=True)
|
||||
# result: 'https://t.me/MyBot?start=Zm9v'
|
||||
|
||||
Decode it back example:
|
||||
.. code-block:: python
|
||||
|
||||
from aiogram.utils.deep_linking import decode_payload
|
||||
from aiogram.types import Message
|
||||
|
||||
@dp.message_handler(commands=["start"])
|
||||
async def handler(message: Message):
|
||||
args = message.get_args()
|
||||
payload = decode_payload(args)
|
||||
await message.answer(f"Your payload: {payload}")
|
||||
|
||||
"""
|
||||
import re
|
||||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||
|
||||
from ..bot import Bot
|
||||
|
||||
BAD_PATTERN = re.compile(r"[^_A-z0-9-]")
|
||||
|
||||
|
||||
async def get_start_link(payload: str, encode=False) -> str:
|
||||
"""
|
||||
Use this method to handy get 'start' deep link with your payload.
|
||||
If you need to encode payload or pass special characters - set encode as True
|
||||
Get 'start' deep link with your payload.
|
||||
|
||||
If you need to encode payload or pass special characters -
|
||||
set encode as True
|
||||
|
||||
:param payload: args passed with /start
|
||||
:param encode: encode payload with base64url
|
||||
:return: link
|
||||
"""
|
||||
return await _create_link('start', payload, encode)
|
||||
return await _create_link(
|
||||
link_type="start",
|
||||
payload=payload,
|
||||
encode=encode,
|
||||
)
|
||||
|
||||
|
||||
async def get_startgroup_link(payload: str, encode=False) -> str:
|
||||
"""
|
||||
Use this method to handy get 'startgroup' deep link with your payload.
|
||||
If you need to encode payload or pass special characters - set encode as True
|
||||
Get 'startgroup' deep link with your payload.
|
||||
|
||||
If you need to encode payload or pass special characters -
|
||||
set encode as True
|
||||
|
||||
:param payload: args passed with /start
|
||||
:param encode: encode payload with base64url
|
||||
:return: link
|
||||
"""
|
||||
return await _create_link('startgroup', payload, encode)
|
||||
return await _create_link(
|
||||
link_type="startgroup",
|
||||
payload=payload,
|
||||
encode=encode,
|
||||
)
|
||||
|
||||
|
||||
async def _create_link(link_type, payload: str, encode=False):
|
||||
"""
|
||||
Create deep link.
|
||||
|
||||
:param link_type: `start` or `startgroup`
|
||||
:param payload: any string-convertible data
|
||||
:param encode: pass True to encode the payload
|
||||
:return: deeplink
|
||||
"""
|
||||
bot = await _get_bot_user()
|
||||
payload = filter_payload(payload)
|
||||
if encode:
|
||||
payload = encode_payload(payload)
|
||||
return f'https://t.me/{bot.username}?{link_type}={payload}'
|
||||
|
||||
|
||||
def encode_payload(payload: str) -> str:
|
||||
""" Encode payload with URL-safe base64url. """
|
||||
from base64 import urlsafe_b64encode
|
||||
result: bytes = urlsafe_b64encode(payload.encode())
|
||||
return result.decode()
|
||||
|
||||
|
||||
def decode_payload(payload: str) -> str:
|
||||
""" Decode payload with URL-safe base64url. """
|
||||
from base64 import urlsafe_b64decode
|
||||
result: bytes = urlsafe_b64decode(payload + '=' * (4 - len(payload) % 4))
|
||||
return result.decode()
|
||||
|
||||
|
||||
def filter_payload(payload: str) -> str:
|
||||
""" Convert payload to text and search for not allowed symbols. """
|
||||
import re
|
||||
|
||||
# convert to string
|
||||
if not isinstance(payload, str):
|
||||
payload = str(payload)
|
||||
|
||||
# search for not allowed characters
|
||||
if re.search(r'[^_A-z0-9-]', payload):
|
||||
message = ('Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. '
|
||||
'We recommend to encode parameters with binary and other '
|
||||
'types of content.')
|
||||
if encode:
|
||||
payload = encode_payload(payload)
|
||||
|
||||
if re.search(BAD_PATTERN, payload):
|
||||
message = (
|
||||
"Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. "
|
||||
"Pass `encode=True` or encode payload manually."
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
return payload
|
||||
if len(payload) > 64:
|
||||
message = "Payload must be up to 64 characters long."
|
||||
raise ValueError(message)
|
||||
|
||||
return f"https://t.me/{bot.username}?{link_type}={payload}"
|
||||
|
||||
|
||||
def encode_payload(payload: str) -> str:
|
||||
"""Encode payload with URL-safe base64url."""
|
||||
payload = str(payload)
|
||||
bytes_payload: bytes = urlsafe_b64encode(payload.encode())
|
||||
str_payload = bytes_payload.decode()
|
||||
return str_payload.replace("=", "")
|
||||
|
||||
|
||||
def decode_payload(payload: str) -> str:
|
||||
"""Decode payload with URL-safe base64url."""
|
||||
payload += "=" * (4 - len(payload) % 4)
|
||||
result: bytes = urlsafe_b64decode(payload)
|
||||
return result.decode()
|
||||
|
||||
|
||||
async def _get_bot_user():
|
||||
""" Get current user of bot. """
|
||||
from ..bot import Bot
|
||||
"""Get current user of bot."""
|
||||
bot = Bot.get_current()
|
||||
return await bot.me
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@ class WrongFileIdentifier(BadRequest):
|
|||
|
||||
|
||||
class GroupDeactivated(BadRequest):
|
||||
match = 'group is deactivated'
|
||||
match = 'Group chat was deactivated'
|
||||
|
||||
|
||||
class PhotoAsInputFileRequired(BadRequest):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
import datetime
|
||||
import functools
|
||||
import secrets
|
||||
from typing import Callable, Union, Optional, Any
|
||||
from typing import Callable, Union, Optional, Any, List
|
||||
from warnings import warn
|
||||
|
||||
from aiohttp import web
|
||||
|
|
@ -23,7 +23,8 @@ def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None):
|
|||
|
||||
|
||||
def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True,
|
||||
on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True):
|
||||
on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True,
|
||||
allowed_updates: Optional[List[str]] = None):
|
||||
"""
|
||||
Start bot in long-polling mode
|
||||
|
||||
|
|
@ -34,11 +35,20 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr
|
|||
:param on_startup:
|
||||
:param on_shutdown:
|
||||
:param timeout:
|
||||
:param relax:
|
||||
:param fast:
|
||||
:param allowed_updates:
|
||||
"""
|
||||
executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop)
|
||||
_setup_callbacks(executor, on_startup, on_shutdown)
|
||||
|
||||
executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast)
|
||||
executor.start_polling(
|
||||
reset_webhook=reset_webhook,
|
||||
timeout=timeout,
|
||||
relax=relax,
|
||||
fast=fast,
|
||||
allowed_updates=allowed_updates
|
||||
)
|
||||
|
||||
|
||||
def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
|
|
@ -295,7 +305,8 @@ class Executor:
|
|||
self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name)
|
||||
self.run_app(**kwargs)
|
||||
|
||||
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True):
|
||||
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True,
|
||||
allowed_updates: Optional[List[str]] = None):
|
||||
"""
|
||||
Start bot in long-polling mode
|
||||
|
||||
|
|
@ -308,7 +319,7 @@ class Executor:
|
|||
try:
|
||||
loop.run_until_complete(self._startup_polling())
|
||||
loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout,
|
||||
relax=relax, fast=fast))
|
||||
relax=relax, fast=fast, allowed_updates=allowed_updates))
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
# loop.stop()
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class MarkdownDecoration(TextDecoration):
|
|||
return f"`{value}`"
|
||||
|
||||
def pre(self, value: str) -> str:
|
||||
return f"```{value}```"
|
||||
return f"```\n{value}\n```"
|
||||
|
||||
def pre_language(self, value: str, language: str) -> str:
|
||||
return f"```{language}\n{value}\n```"
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ aiohttp-socks>=0.3.4
|
|||
rethinkdb>=2.4.1
|
||||
coverage==4.5.3
|
||||
motor>=2.2.0
|
||||
pytest-lazy-fixture==0.6.*
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Welcome to aiogram's documentation!
|
|||
:target: https://pypi.python.org/pypi/aiogram
|
||||
:alt: Supported python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.1-blue.svg?style=flat-square&logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram
|
||||
:target: https://core.telegram.org/bots/api
|
||||
:alt: Telegram Bot API
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.contrib.fsm_storage.redis import RedisStorage2
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def store(redis_options):
|
||||
s = RedisStorage2(**redis_options)
|
||||
try:
|
||||
yield s
|
||||
finally:
|
||||
conn = await s.redis()
|
||||
await conn.flushdb()
|
||||
await s.close()
|
||||
await s.wait_closed()
|
||||
|
||||
|
||||
@pytest.mark.redis
|
||||
class TestRedisStorage2:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_get(self, store):
|
||||
assert await store.get_data(chat='1234') == {}
|
||||
await store.set_data(chat='1234', data={'foo': 'bar'})
|
||||
assert await store.get_data(chat='1234') == {'foo': 'bar'}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_and_open_connection(self, store):
|
||||
await store.set_data(chat='1234', data={'foo': 'bar'})
|
||||
assert await store.get_data(chat='1234') == {'foo': 'bar'}
|
||||
pool_id = id(store._redis)
|
||||
await store.close()
|
||||
assert await store.get_data(chat='1234') == {'foo': 'bar'} # new pool was opened at this point
|
||||
assert id(store._redis) != pool_id
|
||||
79
tests/contrib/fsm_storage/test_storage.py
Normal file
79
tests/contrib/fsm_storage/test_storage.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.contrib.fsm_storage.memory import MemoryStorage
|
||||
from aiogram.contrib.fsm_storage.redis import RedisStorage2, RedisStorage
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.mark.redis
|
||||
async def redis_store(redis_options):
|
||||
s = RedisStorage(**redis_options)
|
||||
try:
|
||||
yield s
|
||||
finally:
|
||||
conn = await s.redis()
|
||||
await conn.execute('FLUSHDB')
|
||||
await s.close()
|
||||
await s.wait_closed()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.mark.redis
|
||||
async def redis_store2(redis_options):
|
||||
s = RedisStorage2(**redis_options)
|
||||
try:
|
||||
yield s
|
||||
finally:
|
||||
conn = await s.redis()
|
||||
await conn.flushdb()
|
||||
await s.close()
|
||||
await s.wait_closed()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
async def memory_store():
|
||||
yield MemoryStorage()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"store", [
|
||||
pytest.lazy_fixture('redis_store'),
|
||||
pytest.lazy_fixture('redis_store2'),
|
||||
pytest.lazy_fixture('memory_store'),
|
||||
]
|
||||
)
|
||||
class TestStorage:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_get(self, store):
|
||||
assert await store.get_data(chat='1234') == {}
|
||||
await store.set_data(chat='1234', data={'foo': 'bar'})
|
||||
assert await store.get_data(chat='1234') == {'foo': 'bar'}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset(self, store):
|
||||
await store.set_data(chat='1234', data={'foo': 'bar'})
|
||||
await store.reset_data(chat='1234')
|
||||
assert await store.get_data(chat='1234') == {}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_empty(self, store):
|
||||
await store.reset_data(chat='1234')
|
||||
assert await store.get_data(chat='1234') == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"store", [
|
||||
pytest.lazy_fixture('redis_store'),
|
||||
pytest.lazy_fixture('redis_store2'),
|
||||
]
|
||||
)
|
||||
class TestRedisStorage2:
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_and_open_connection(self, store):
|
||||
await store.set_data(chat='1234', data={'foo': 'bar'})
|
||||
assert await store.get_data(chat='1234') == {'foo': 'bar'}
|
||||
pool_id = id(store._redis)
|
||||
await store.close()
|
||||
assert await store.get_data(chat='1234') == {
|
||||
'foo': 'bar'} # new pool was opened at this point
|
||||
assert id(store._redis) != pool_id
|
||||
|
|
@ -427,7 +427,7 @@ async def test_get_chat_administrators(bot: Bot):
|
|||
""" getChatAdministrators method test """
|
||||
from .types.dataset import CHAT, CHAT_MEMBER
|
||||
chat = types.Chat(**CHAT)
|
||||
member = types.ChatMember(**CHAT_MEMBER)
|
||||
member = types.ChatMember.resolve(**CHAT_MEMBER)
|
||||
|
||||
async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER]):
|
||||
result = await bot.get_chat_administrators(chat_id=chat.id)
|
||||
|
|
@ -435,14 +435,14 @@ async def test_get_chat_administrators(bot: Bot):
|
|||
assert len(result) == 2
|
||||
|
||||
|
||||
async def test_get_chat_members_count(bot: Bot):
|
||||
async def test_get_chat_member_count(bot: Bot):
|
||||
""" getChatMembersCount method test """
|
||||
from .types.dataset import CHAT
|
||||
chat = types.Chat(**CHAT)
|
||||
count = 5
|
||||
|
||||
async with FakeTelegram(message_data=count):
|
||||
result = await bot.get_chat_members_count(chat_id=chat.id)
|
||||
result = await bot.get_chat_member_count(chat_id=chat.id)
|
||||
assert result == count
|
||||
|
||||
|
||||
|
|
@ -450,7 +450,7 @@ async def test_get_chat_member(bot: Bot):
|
|||
""" getChatMember method test """
|
||||
from .types.dataset import CHAT, CHAT_MEMBER
|
||||
chat = types.Chat(**CHAT)
|
||||
member = types.ChatMember(**CHAT_MEMBER)
|
||||
member = types.ChatMember.resolve(**CHAT_MEMBER)
|
||||
|
||||
async with FakeTelegram(message_data=CHAT_MEMBER):
|
||||
result = await bot.get_chat_member(chat_id=chat.id, user_id=member.user.id)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import pytest
|
||||
|
||||
from aiogram.utils.deep_linking import decode_payload, encode_payload, filter_payload
|
||||
from aiogram.utils.deep_linking import get_start_link, get_startgroup_link
|
||||
from aiogram.utils.deep_linking import (
|
||||
decode_payload,
|
||||
encode_payload,
|
||||
get_start_link,
|
||||
get_startgroup_link,
|
||||
)
|
||||
from tests.types import dataset
|
||||
|
||||
# enable asyncio mode
|
||||
|
|
@ -17,9 +21,11 @@ PAYLOADS = [
|
|||
|
||||
WRONG_PAYLOADS = [
|
||||
'@BotFather',
|
||||
"Some:special$characters#=",
|
||||
'spaces spaces spaces',
|
||||
1234567890123456789.0,
|
||||
]
|
||||
USERNAME = dataset.USER["username"]
|
||||
|
||||
|
||||
@pytest.fixture(params=PAYLOADS, name='payload')
|
||||
|
|
@ -47,7 +53,7 @@ def get_bot_user_fixture(monkeypatch):
|
|||
class TestDeepLinking:
|
||||
async def test_get_start_link(self, payload):
|
||||
link = await get_start_link(payload)
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?start={payload}'
|
||||
assert link == f'https://t.me/{USERNAME}?start={payload}'
|
||||
|
||||
async def test_wrong_symbols(self, wrong_payload):
|
||||
with pytest.raises(ValueError):
|
||||
|
|
@ -55,20 +61,29 @@ class TestDeepLinking:
|
|||
|
||||
async def test_get_startgroup_link(self, payload):
|
||||
link = await get_startgroup_link(payload)
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?startgroup={payload}'
|
||||
assert link == f'https://t.me/{USERNAME}?startgroup={payload}'
|
||||
|
||||
async def test_filter_encode_and_decode(self, payload):
|
||||
_payload = filter_payload(payload)
|
||||
encoded = encode_payload(_payload)
|
||||
encoded = encode_payload(payload)
|
||||
decoded = decode_payload(encoded)
|
||||
assert decoded == str(payload)
|
||||
|
||||
async def test_get_start_link_with_encoding(self, payload):
|
||||
async def test_get_start_link_with_encoding(self, wrong_payload):
|
||||
# define link
|
||||
link = await get_start_link(payload, encode=True)
|
||||
link = await get_start_link(wrong_payload, encode=True)
|
||||
|
||||
# define reference link
|
||||
payload = filter_payload(payload)
|
||||
encoded_payload = encode_payload(payload)
|
||||
encoded_payload = encode_payload(wrong_payload)
|
||||
|
||||
assert link == f'https://t.me/{dataset.USER["username"]}?start={encoded_payload}'
|
||||
assert link == f'https://t.me/{USERNAME}?start={encoded_payload}'
|
||||
|
||||
async def test_64_len_payload(self):
|
||||
payload = "p" * 64
|
||||
link = await get_start_link(payload)
|
||||
assert link
|
||||
|
||||
async def test_too_long_payload(self):
|
||||
payload = "p" * 65
|
||||
print(payload, len(payload))
|
||||
with pytest.raises(ValueError):
|
||||
await get_start_link(payload)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from aiogram import types
|
||||
from .dataset import CHAT_MEMBER
|
||||
|
||||
chat_member = types.ChatMember(**CHAT_MEMBER)
|
||||
chat_member = types.ChatMember.resolve(**CHAT_MEMBER)
|
||||
|
||||
|
||||
def test_export():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue