Merge branch 'dev-2.x' into bug/#562-default-locale-unused

This commit is contained in:
Alex Root Junior 2021-07-06 01:12:33 +03:00 committed by GitHub
commit d63dc49562
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 984 additions and 320 deletions

View file

@ -6,7 +6,7 @@
[![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.1-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api)
[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest)
[![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues)
[![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](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"})

View file

@ -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

View file

@ -43,5 +43,5 @@ __all__ = (
'utils',
)
__version__ = '2.12.1'
__api_version__ = '5.1'
__version__ = '2.14.1'
__api_version__ = '5.3'

View file

@ -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

View file

@ -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_)

View file

@ -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]

View file

@ -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:

View file

@ -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,

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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.

View file

@ -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',

View 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)

View file

@ -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):
"""

View file

@ -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()

View file

@ -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 bots 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 bots questions, it will receive the users 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)

View file

@ -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],

View file

@ -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

View file

@ -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.

View file

@ -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 thumbnails 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 thumbnails 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

View file

@ -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):

View file

@ -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 []

View 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()

View file

@ -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

View file

@ -368,7 +368,7 @@ class WrongFileIdentifier(BadRequest):
class GroupDeactivated(BadRequest):
match = 'group is deactivated'
match = 'Group chat was deactivated'
class PhotoAsInputFileRequired(BadRequest):

View file

@ -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()

View file

@ -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```"

View file

@ -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.*

View file

@ -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

View file

@ -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

View 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

View file

@ -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)

View file

@ -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)

View file

@ -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():