diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 82ea7257..c4430ef6 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1 @@
-github: [JRootJunior]
open_collective: aiogram
diff --git a/.gitignore b/.gitignore
index a8b34bd1..d20c39ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,3 +60,6 @@ docs/html
# i18n/l10n
*.mo
+
+# pynev
+.python-version
diff --git a/README.md b/README.md
index 21a19977..dfef918f 100644
--- a/README.md
+++ b/README.md
@@ -6,14 +6,14 @@
[](https://pypi.python.org/pypi/aiogram)
[](https://pypi.python.org/pypi/aiogram)
[](https://pypi.python.org/pypi/aiogram)
-[](https://core.telegram.org/bots/api)
-[](http://aiogram.readthedocs.io/en/latest/?badge=latest)
+[](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)
-**aiogram** is a pretty simple and fully asynchronous library for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
+**aiogram** is a pretty simple and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
-You can [read the docs here](http://aiogram.readthedocs.io/en/latest/).
+You can [read the docs here](http://docs.aiogram.dev/en/latest/).
## Official aiogram resources:
@@ -21,7 +21,7 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/).
- Community: [@aiogram](https://t.me/aiogram)
- Russian community: [@aiogram_ru](https://t.me/aiogram_ru)
- Pip: [aiogram](https://pypi.python.org/pypi/aiogram)
- - Docs: [ReadTheDocs](http://aiogram.readthedocs.io)
+ - Docs: [aiogram site](https://docs.aiogram.dev/)
- Source: [Github repo](https://github.com/aiogram/aiogram)
- Issues/Bug tracker: [Github issues tracker](https://github.com/aiogram/aiogram/issues)
- Test bot: [@aiogram_bot](https://t.me/aiogram_bot)
@@ -45,13 +45,13 @@ Become a financial contributor and help us sustain our community. [[Contribute](
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/aiogram/contribute)]
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/README.rst b/README.rst
index 294c9ee8..1cf2765d 100644
--- a/README.rst
+++ b/README.rst
@@ -21,12 +21,12 @@ AIOGramBot
:target: https://pypi.python.org/pypi/aiogram
:alt: Supported python versions
-.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram
+.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API
.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square
- :target: http://aiogram.readthedocs.io/en/latest/?badge=latest
+ :target: http://docs.aiogram.dev/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square
@@ -38,9 +38,9 @@ AIOGramBot
:alt: MIT License
-**aiogram** is a pretty simple and fully asynchronous library for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler.
+**aiogram** is a pretty simple and fully asynchronous framework for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler.
-You can `read the docs here `_.
+You can `read the docs here `_.
Official aiogram resources
--------------------------
@@ -49,7 +49,7 @@ Official aiogram resources
- Community: `@aiogram `_
- Russian community: `@aiogram_ru `_
- Pip: `aiogram `_
-- Docs: `ReadTheDocs `_
+- Docs: `ReadTheDocs `_
- Source: `Github repo `_
- Issues/Bug tracker: `Github issues tracker `_
- Test bot: `@aiogram_bot `_
diff --git a/aiogram/__init__.py b/aiogram/__init__.py
index b81ceedb..bebafcec 100644
--- a/aiogram/__init__.py
+++ b/aiogram/__init__.py
@@ -1,3 +1,8 @@
+import sys
+if sys.version_info < (3, 7):
+ raise ImportError('Your Python version {0} is not supported by aiogram, please install '
+ 'Python 3.7+'.format('.'.join(map(str, sys.version_info[:3]))))
+
import asyncio
import os
@@ -38,5 +43,5 @@ __all__ = [
'utils'
]
-__version__ = '2.3.dev1'
-__api_version__ = '4.4'
+__version__ = '2.9.2'
+__api_version__ = '4.9'
diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py
index 675626ac..1d0c4f7b 100644
--- a/aiogram/bot/api.py
+++ b/aiogram/bot/api.py
@@ -24,11 +24,17 @@ def check_token(token: str) -> bool:
:param token:
:return:
"""
+ if not isinstance(token, str):
+ message = (f"Token is invalid! "
+ f"It must be 'str' type instead of {type(token)} type.")
+ raise exceptions.ValidationError(message)
+
if any(x.isspace() for x in token):
- raise exceptions.ValidationError('Token is invalid!')
+ message = "Token is invalid! It can't contains spaces."
+ raise exceptions.ValidationError(message)
left, sep, right = token.partition(':')
- if (not sep) or (not left.isdigit()) or (len(left) < 3):
+ if (not sep) or (not left.isdigit()) or (not right):
raise exceptions.ValidationError('Token is invalid!')
return True
@@ -147,7 +153,7 @@ class Methods(Helper):
"""
Helper for Telegram API Methods listed on https://core.telegram.org/bots/api
- List is updated to Bot API 4.4
+ List is updated to Bot API 4.9
"""
mode = HelperMode.lowerCamelCase
@@ -175,6 +181,7 @@ class Methods(Helper):
SEND_VENUE = Item() # sendVenue
SEND_CONTACT = Item() # sendContact
SEND_POLL = Item() # sendPoll
+ SEND_DICE = Item() # sendDice
SEND_CHAT_ACTION = Item() # sendChatAction
GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos
GET_FILE = Item() # getFile
@@ -182,6 +189,7 @@ class Methods(Helper):
UNBAN_CHAT_MEMBER = Item() # unbanChatMember
RESTRICT_CHAT_MEMBER = Item() # restrictChatMember
PROMOTE_CHAT_MEMBER = Item() # promoteChatMember
+ SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE = Item() # setChatAdministratorCustomTitle
SET_CHAT_PERMISSIONS = Item() # setChatPermissions
EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink
SET_CHAT_PHOTO = Item() # setChatPhoto
@@ -198,6 +206,8 @@ class Methods(Helper):
SET_CHAT_STICKER_SET = Item() # setChatStickerSet
DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet
ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery
+ SET_MY_COMMANDS = Item() # setMyCommands
+ GET_MY_COMMANDS = Item() # getMyCommands
# Updating messages
EDIT_MESSAGE_TEXT = Item() # editMessageText
@@ -215,6 +225,7 @@ class Methods(Helper):
ADD_STICKER_TO_SET = Item() # addStickerToSet
SET_STICKER_POSITION_IN_SET = Item() # setStickerPositionInSet
DELETE_STICKER_FROM_SET = Item() # deleteStickerFromSet
+ SET_STICKER_SET_THUMB = Item() # setStickerSetThumb
# Inline mode
ANSWER_INLINE_QUERY = Item() # answerInlineQuery
diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py
index 608abd06..86347e88 100644
--- a/aiogram/bot/base.py
+++ b/aiogram/bot/base.py
@@ -3,8 +3,9 @@ import contextlib
import io
import ssl
import typing
+import warnings
from contextvars import ContextVar
-from typing import Dict, List, Optional, Union
+from typing import Dict, List, Optional, Union, Type
import aiohttp
import certifi
@@ -60,6 +61,7 @@ class BaseBot:
api.check_token(token)
self._token = None
self.__token = token
+ self.id = int(token.split(sep=':')[0])
self.proxy = proxy
self.proxy_auth = proxy_auth
@@ -72,38 +74,48 @@ class BaseBot:
# aiohttp main session
ssl_context = ssl.create_default_context(cafile=certifi.where())
+ self._session: Optional[aiohttp.ClientSession] = None
+ self._connector_class: Type[aiohttp.TCPConnector] = aiohttp.TCPConnector
+ self._connector_init = dict(
+ limit=connections_limit, ssl=ssl_context, loop=self.loop
+ )
+
if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')):
from aiohttp_socks import SocksConnector
- from aiohttp_socks.helpers import parse_socks_url
+ from aiohttp_socks.utils import parse_proxy_url
- socks_ver, host, port, username, password = parse_socks_url(proxy)
+ socks_ver, host, port, username, password = parse_proxy_url(proxy)
if proxy_auth:
if not username:
username = proxy_auth.login
if not password:
password = proxy_auth.password
- connector = SocksConnector(socks_ver=socks_ver, host=host, port=port,
- username=username, password=password,
- limit=connections_limit, ssl_context=ssl_context,
- rdns=True, loop=self.loop)
-
+ self._connector_class = SocksConnector
+ self._connector_init.update(
+ socks_ver=socks_ver, host=host, port=port,
+ username=username, password=password, rdns=True,
+ )
self.proxy = None
self.proxy_auth = None
- else:
- connector = aiohttp.TCPConnector(limit=connections_limit, ssl=ssl_context, loop=self.loop)
+
self._timeout = None
self.timeout = timeout
- self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, json_serialize=json.dumps)
-
self.parse_mode = parse_mode
- def __del__(self):
- if self.loop.is_running():
- self.loop.create_task(self.close())
- else:
- self.loop.run_until_complete(self.close())
+ def get_new_session(self) -> aiohttp.ClientSession:
+ return aiohttp.ClientSession(
+ connector=self._connector_class(**self._connector_init),
+ loop=self.loop,
+ json_serialize=json.dumps
+ )
+
+ @property
+ def session(self) -> Optional[aiohttp.ClientSession]:
+ if self._session is None or self._session.closed:
+ self._session = self.get_new_session()
+ return self._session
@staticmethod
def _prepare_timeout(
@@ -266,6 +278,10 @@ class BaseBot:
if value not in ParseMode.all():
raise ValueError(f"Parse mode must be one of {ParseMode.all()}")
setattr(self, '_parse_mode', value)
+ if value == 'markdown':
+ warnings.warn("Parse mode `Markdown` is legacy since Telegram Bot API 4.5, "
+ "retained for backward compatibility. Use `MarkdownV2` instead.\n"
+ "https://core.telegram.org/bots/api#markdown-style", stacklevel=3)
@parse_mode.deleter
def parse_mode(self):
diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py
index 5933f0db..41f30af1 100644
--- a/aiogram/bot/bot.py
+++ b/aiogram/bot/bot.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import datetime
import typing
import warnings
@@ -209,6 +210,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
+
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals())
if self.parse_mode:
@@ -521,6 +523,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals(), exclude=["animation", "thumb"])
+ if self.parse_mode:
+ payload.setdefault('parse_mode', self.parse_mode)
files = {}
prepare_file(payload, files, 'animation', animation)
@@ -862,6 +866,16 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
async def send_poll(self, chat_id: typing.Union[base.Integer, base.String],
question: base.String,
options: typing.List[base.String],
+ is_anonymous: typing.Optional[base.Boolean] = None,
+ type: typing.Optional[base.String] = None,
+ allows_multiple_answers: typing.Optional[base.Boolean] = None,
+ correct_option_id: typing.Optional[base.Integer] = None,
+ explanation: typing.Optional[base.String] = None,
+ explanation_parse_mode: typing.Optional[base.String] = None,
+ open_period: typing.Union[base.Integer, None] = None,
+ close_date: typing.Union[
+ base.Integer, datetime.datetime, datetime.timedelta, None] = None,
+ is_closed: typing.Optional[base.Boolean] = None,
disable_notification: typing.Optional[base.Boolean] = None,
reply_to_message_id: typing.Optional[base.Integer] = None,
reply_markup: typing.Union[types.InlineKeyboardMarkup,
@@ -879,7 +893,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:param question: Poll question, 1-255 characters
:type question: :obj:`base.String`
:param options: List of answer options, 2-10 strings 1-100 characters each
- :param options: :obj:`typing.List[base.String]`
+ :type options: :obj:`typing.List[base.String]`
+ :param is_anonymous: True, if the poll needs to be anonymous, defaults to True
+ :type is_anonymous: :obj:`typing.Optional[base.Boolean]`
+ :param type: Poll type, “quiz” or “regular”, defaults to “regular”
+ :type type: :obj:`typing.Optional[base.String]`
+ :param allows_multiple_answers: True, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to False
+ :type allows_multiple_answers: :obj:`typing.Optional[base.Boolean]`
+ :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode
+ :type correct_option_id: :obj:`typing.Optional[base.Integer]`
+ :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing
+ :type explanation: :obj:`typing.Optional[base.String]`
+ :param explanation_parse_mode: Mode for parsing entities in the explanation. See formatting options for more details.
+ :type explanation_parse_mode: :obj:`typing.Optional[base.String]`
+ :param open_period: Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with close_date.
+ :type open_period: :obj:`typing.Union[base.Integer, None]`
+ :param close_date: Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with open_period.
+ :type close_date: :obj:`typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None]`
+ :param is_closed: Pass True, if the poll needs to be immediately closed
+ :type is_closed: :obj:`typing.Optional[base.Boolean]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Optional[Boolean]`
:param reply_to_message_id: If the message is a reply, ID of the original message
@@ -892,11 +924,53 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`types.Message`
"""
options = prepare_arg(options)
+ open_period = prepare_arg(open_period)
+ close_date = prepare_arg(close_date)
payload = generate_payload(**locals())
+ if self.parse_mode:
+ payload.setdefault('explanation_parse_mode', self.parse_mode)
result = await self.request(api.Methods.SEND_POLL, payload)
return types.Message(**result)
+ async def send_dice(self, chat_id: typing.Union[base.Integer, base.String],
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ emoji: typing.Union[base.String, None] = None,
+ reply_to_message_id: typing.Union[base.Integer, None] = None,
+ reply_markup: typing.Union[types.InlineKeyboardMarkup,
+ types.ReplyKeyboardMarkup,
+ types.ReplyKeyboardRemove,
+ types.ForceReply, None] = None) -> types.Message:
+ """
+ Use this method to send a dice, which will have a random value from 1 to 6.
+ On success, the sent Message is returned.
+ (Yes, we're aware of the “proper” singular of die.
+ But it's awkward, and we decided to help it change. One dice at a time!)
+
+ Source: https://core.telegram.org/bots/api#senddice
+
+ :param chat_id: Unique identifier for the target chat or username of the target channel
+ :type chat_id: :obj:`typing.Union[base.Integer, base.String]`
+ :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of “🎲” or “🎯”. Defauts to “🎲”
+ :type emoji: :obj:`typing.Union[base.String, None]`
+ :param disable_notification: Sends the message silently. Users will receive a notification with no sound
+ :type disable_notification: :obj:`typing.Union[base.Boolean, None]`
+ :param reply_to_message_id: If the message is a reply, ID of the original message
+ :type reply_to_message_id: :obj:`typing.Union[base.Integer, None]`
+ :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
+ custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
+ :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
+ types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
+ :return: On success, the sent Message is returned
+ :rtype: :obj:`types.Message`
+ """
+
+ reply_markup = prepare_arg(reply_markup)
+ payload = generate_payload(**locals())
+
+ result = await self.request(api.Methods.SEND_DICE, payload)
+ return types.Message(**result)
+
async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String],
action: base.String) -> base.Boolean:
"""
@@ -963,7 +1037,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
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, None] = None) -> base.Boolean:
+ until_date: typing.Union[
+ base.Integer, datetime.datetime, datetime.timedelta, None] = 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 group
@@ -1018,7 +1093,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
user_id: base.Integer,
permissions: typing.Optional[types.ChatPermissions] = None,
# permissions argument need to be required after removing other `can_*` arguments
- until_date: typing.Union[base.Integer, None] = None,
+ until_date: typing.Union[
+ base.Integer, datetime.datetime, datetime.timedelta, None] = None,
can_send_messages: typing.Union[base.Boolean, None] = None,
can_send_media_messages: typing.Union[base.Boolean, None] = None,
can_send_other_messages: typing.Union[base.Boolean, None] = None,
@@ -1116,6 +1192,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload)
return result
+ async def set_chat_administrator_custom_title(self, chat_id: typing.Union[base.Integer, base.String],
+ user_id: base.Integer, custom_title: base.String) -> base.Boolean:
+ """
+ Use this method to set a custom title for an administrator in a supergroup promoted by the bot.
+
+ Returns True on success.
+
+ Source: https://core.telegram.org/bots/api#setchatadministratorcustomtitle
+
+ :param chat_id: Unique identifier for the target chat or username of the target supergroup
+ :param user_id: Unique identifier of the target user
+ :param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed
+ :return: True on success.
+ """
+ payload = generate_payload(**locals())
+
+ result = await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload)
+ return result
+
async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String],
permissions: types.ChatPermissions) -> base.Boolean:
"""
@@ -1447,6 +1542,37 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload)
return result
+ async def set_my_commands(self, commands: typing.List[types.BotCommand]) -> 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.
+ :type commands: :obj: `typing.List[types.BotCommand]`
+ :return: Returns True on success.
+ :rtype: :obj:`base.Boolean`
+ """
+ commands = prepare_arg(commands)
+ payload = generate_payload(**locals())
+
+ result = await self.request(api.Methods.SET_MY_COMMANDS, payload)
+ return result
+
+ async def get_my_commands(self) -> typing.List[types.BotCommand]:
+ """
+ Use this method to get the current list of the bot's commands.
+
+ Source: https://core.telegram.org/bots/api#getmycommands
+ :return: Returns Array of BotCommand on success.
+ :rtype: :obj:`typing.List[types.BotCommand]`
+ """
+ payload = generate_payload(**locals())
+
+ result = await self.request(api.Methods.GET_MY_COMMANDS, payload)
+ return [types.BotCommand(**bot_command_data) for bot_command_data in result]
+
async def edit_message_text(self, text: base.String,
chat_id: typing.Union[base.Integer, base.String, None] = None,
message_id: typing.Union[base.Integer, None] = None,
@@ -1733,24 +1859,40 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.UPLOAD_STICKER_FILE, payload, files)
return types.File(**result)
- async def create_new_sticker_set(self, user_id: base.Integer, name: base.String, title: base.String,
- png_sticker: typing.Union[base.InputFile, base.String], emojis: base.String,
+ async def create_new_sticker_set(self,
+ user_id: base.Integer,
+ name: base.String,
+ title: base.String,
+ emojis: base.String,
+ png_sticker: typing.Union[base.InputFile, base.String] = None,
+ tgs_sticker: base.InputFile = None,
contains_masks: typing.Union[base.Boolean, None] = None,
mask_position: typing.Union[types.MaskPosition, None] = None) -> base.Boolean:
"""
- Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set.
+ Use this method to create a new sticker set owned by a user.
+ The bot will be able to edit the sticker set thus created.
+ You must use exactly one of the fields png_sticker or tgs_sticker.
Source: https://core.telegram.org/bots/api#createnewstickerset
:param user_id: User identifier of created sticker set owner
:type user_id: :obj:`base.Integer`
- :param name: Short name of sticker set, to be used in t.me/addstickers/ URLs (e.g., animals)
+ :param name: Short name of sticker set, to be used in t.me/addstickers/ URLs (e.g., animals).
+ Can contain only english letters, digits and underscores.
+ Must begin with a letter, can't contain consecutive underscores and must end in “_by_”.
+ is case insensitive. 1-64 characters.
:type name: :obj:`base.String`
:param title: Sticker set title, 1-64 characters
:type title: :obj:`base.String`
- :param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size,
+ :param png_sticker: PNG image with the sticker, must be up to 512 kilobytes in size,
dimensions must not exceed 512px, and either width or height must be exactly 512px.
+ Pass a file_id as a String to send a file that already exists on the Telegram servers,
+ 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. More info on https://core.telegram.org/bots/api#sending-files
:type png_sticker: :obj:`typing.Union[base.InputFile, base.String]`
+ :param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data.
+ See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements
+ :type tgs_sticker: :obj:`base.InputFile`
:param emojis: One or more emoji corresponding to the sticker
:type emojis: :obj:`base.String`
:param contains_masks: Pass True, if a set of mask stickers should be created
@@ -1761,19 +1903,28 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`base.Boolean`
"""
mask_position = prepare_arg(mask_position)
- payload = generate_payload(**locals(), exclude=['png_sticker'])
+ payload = generate_payload(**locals(), exclude=['png_sticker', 'tgs_sticker'])
files = {}
prepare_file(payload, files, 'png_sticker', png_sticker)
+ prepare_file(payload, files, 'tgs_sticker', tgs_sticker)
result = await self.request(api.Methods.CREATE_NEW_STICKER_SET, payload, files)
return result
- async def add_sticker_to_set(self, user_id: base.Integer, name: base.String,
- png_sticker: typing.Union[base.InputFile, base.String], emojis: base.String,
+ async def add_sticker_to_set(self,
+ user_id: base.Integer,
+ name: base.String,
+ emojis: base.String,
+ png_sticker: typing.Union[base.InputFile, base.String] = None,
+ tgs_sticker: base.InputFile = None,
mask_position: typing.Union[types.MaskPosition, None] = None) -> base.Boolean:
"""
Use this method to add a new sticker to a set created by the bot.
+ You must use exactly one of the fields png_sticker or tgs_sticker.
+ Animated stickers can be added to animated sticker sets and only to them.
+ Animated sticker sets can have up to 50 stickers.
+ Static sticker sets can have up to 120 stickers.
Source: https://core.telegram.org/bots/api#addstickertoset
@@ -1781,9 +1932,15 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type user_id: :obj:`base.Integer`
:param name: Sticker set name
:type name: :obj:`base.String`
- :param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size,
+ :param png_sticker: PNG image with the sticker, must be up to 512 kilobytes in size,
dimensions must not exceed 512px, and either width or height must be exactly 512px.
+ Pass a file_id as a String to send a file that already exists on the Telegram servers,
+ 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. More info on https://core.telegram.org/bots/api#sending-files
:type png_sticker: :obj:`typing.Union[base.InputFile, base.String]`
+ :param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data.
+ See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements
+ :type tgs_sticker: :obj:`base.InputFile`
:param emojis: One or more emoji corresponding to the sticker
:type emojis: :obj:`base.String`
:param mask_position: A JSON-serialized object for position where the mask should be placed on faces
@@ -1792,10 +1949,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:rtype: :obj:`base.Boolean`
"""
mask_position = prepare_arg(mask_position)
- payload = generate_payload(**locals(), exclude=['png_sticker'])
+ payload = generate_payload(**locals(), exclude=['png_sticker', 'tgs_sticker'])
files = {}
prepare_file(payload, files, 'png_sticker', png_sticker)
+ prepare_file(payload, files, 'tgs_sticker', png_sticker)
result = await self.request(api.Methods.ADD_STICKER_TO_SET, payload, files)
return result
@@ -1822,8 +1980,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
"""
Use this method to delete a sticker from a set created by the bot.
- The following methods and objects allow your bot to work in inline mode.
-
Source: https://core.telegram.org/bots/api#deletestickerfromset
:param sticker: File identifier of the sticker
@@ -1836,6 +1992,39 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
result = await self.request(api.Methods.DELETE_STICKER_FROM_SET, payload)
return result
+ async def set_sticker_set_thumb(self,
+ name: base.String,
+ user_id: base.Integer,
+ thumb: typing.Union[base.InputFile, base.String] = None) -> base.Boolean:
+ """
+ Use this method to set the thumbnail of a sticker set.
+ Animated thumbnails can be set for animated sticker sets only.
+
+ Source: https://core.telegram.org/bots/api#setstickersetthumb
+
+ :param name: Sticker set name
+ :type name: :obj:`base.String`
+ :param user_id: User identifier of the sticker set owner
+ :type user_id: :obj:`base.Integer`
+ :param thumb: A PNG image with the thumbnail, must be up to 128 kilobytes in size and have width and height
+ exactly 100px, or a TGS animation with the thumbnail up to 32 kilobytes in size;
+ see https://core.telegram.org/animated_stickers#technical-requirements for animated sticker technical
+ requirements. Pass a file_id as a String to send a file that already exists on the Telegram servers,
+ 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. More info on https://core.telegram.org/bots/api#sending-files.
+ Animated sticker set thumbnail can't be uploaded via HTTP URL.
+ :type thumb: :obj:`typing.Union[base.InputFile, base.String]`
+ :return: Returns True on success
+ :rtype: :obj:`base.Boolean`
+ """
+ payload = generate_payload(**locals(), exclude=['thumb'])
+
+ files = {}
+ prepare_file(payload, files, 'thumb', thumb)
+
+ result = await self.request(api.Methods.SET_STICKER_SET_THUMB, payload, files)
+ return result
+
async def answer_inline_query(self, inline_query_id: base.String,
results: typing.List[types.InlineQueryResult],
cache_time: typing.Union[base.Integer, None] = None,
@@ -1896,6 +2085,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
need_phone_number: typing.Union[base.Boolean, None] = None,
need_email: typing.Union[base.Boolean, None] = None,
need_shipping_address: typing.Union[base.Boolean, None] = None,
+ send_phone_number_to_provider: typing.Union[base.Boolean, None] = None,
+ send_email_to_provider: typing.Union[base.Boolean, None] = None,
is_flexible: typing.Union[base.Boolean, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,
@@ -1942,6 +2133,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin):
:type need_email: :obj:`typing.Union[base.Boolean, None]`
:param need_shipping_address: Pass True, if you require the user's shipping address to complete the order
:type need_shipping_address: :obj:`typing.Union[base.Boolean, None]`
+ :param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider
+ :type send_phone_number_to_provider: :obj:`typing.Union[base.Boolean, None]`
+ :param send_email_to_provider: Pass True, if user's email address should be sent to provider
+ :type send_email_to_provider: :obj:`typing.Union[base.Boolean, None]`
:param is_flexible: Pass True, if the final price depends on the shipping method
:type is_flexible: :obj:`typing.Union[base.Boolean, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound
diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py
index 106a7b97..bf88eff7 100644
--- a/aiogram/contrib/fsm_storage/redis.py
+++ b/aiogram/contrib/fsm_storage/redis.py
@@ -44,19 +44,19 @@ class RedisStorage(BaseStorage):
self._loop = loop or asyncio.get_event_loop()
self._kwargs = kwargs
- self._redis: aioredis.RedisConnection = None
+ self._redis: typing.Optional[aioredis.RedisConnection] = None
self._connection_lock = asyncio.Lock(loop=self._loop)
async def close(self):
- if self._redis and not self._redis.closed:
- self._redis.close()
- del self._redis
- self._redis = None
+ async with self._connection_lock:
+ if self._redis and not self._redis.closed:
+ self._redis.close()
async def wait_closed(self):
- if self._redis:
- return await self._redis.wait_closed()
- return True
+ async with self._connection_lock:
+ if self._redis:
+ return await self._redis.wait_closed()
+ return True
async def redis(self) -> aioredis.RedisConnection:
"""
@@ -64,7 +64,7 @@ class RedisStorage(BaseStorage):
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
- if self._redis is None:
+ if self._redis is None or self._redis.closed:
self._redis = await aioredis.create_connection((self._host, self._port),
db=self._db, password=self._password, ssl=self._ssl,
loop=self._loop,
@@ -144,7 +144,7 @@ class RedisStorage(BaseStorage):
record_data.update(data, **kwargs)
await self.set_record(chat=chat, user=user, state=record['state'], data=record_data)
- async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
+ async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]:
"""
Get list of all stored chat's and user's
@@ -220,11 +220,11 @@ class RedisStorage2(BaseStorage):
"""
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,
- bucket_ttl: int = 0,
- **kwargs):
+ ssl=None, pool_size=10, loop=None, prefix='fsm',
+ state_ttl: int = 0,
+ data_ttl: int = 0,
+ bucket_ttl: int = 0,
+ **kwargs):
self._host = host
self._port = port
self._db = db
@@ -239,7 +239,7 @@ class RedisStorage2(BaseStorage):
self._data_ttl = data_ttl
self._bucket_ttl = bucket_ttl
- self._redis: aioredis.RedisConnection = None
+ self._redis: typing.Optional[aioredis.RedisConnection] = None
self._connection_lock = asyncio.Lock(loop=self._loop)
async def redis(self) -> aioredis.Redis:
@@ -248,7 +248,7 @@ class RedisStorage2(BaseStorage):
"""
# Use thread-safe asyncio Lock because this method without that is not safe
async with self._connection_lock:
- if self._redis is None:
+ if self._redis is None or self._redis.closed:
self._redis = await aioredis.create_redis_pool((self._host, self._port),
db=self._db, password=self._password, ssl=self._ssl,
minsize=1, maxsize=self._pool_size,
@@ -262,8 +262,6 @@ class RedisStorage2(BaseStorage):
async with self._connection_lock:
if self._redis and not self._redis.closed:
self._redis.close()
- del self._redis
- self._redis = None
async def wait_closed(self):
async with self._connection_lock:
@@ -357,7 +355,7 @@ class RedisStorage2(BaseStorage):
keys = await conn.keys(self.generate_key('*'))
await conn.delete(*keys)
- async def get_states_list(self) -> typing.List[typing.Tuple[int]]:
+ async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]:
"""
Get list of all stored chat's and user's
diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py
index 67ab8cca..63f54510 100644
--- a/aiogram/contrib/middlewares/i18n.py
+++ b/aiogram/contrib/middlewares/i18n.py
@@ -95,7 +95,7 @@ class I18nMiddleware(BaseMiddleware):
locale = self.ctx_locale.get()
if locale not in self.locales:
- if n is 1:
+ if n == 1:
return singular
return plural
diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py
index 9f389b60..308d0e10 100644
--- a/aiogram/contrib/middlewares/logging.py
+++ b/aiogram/contrib/middlewares/logging.py
@@ -146,6 +146,20 @@ class LoggingMiddleware(BaseMiddleware):
if timeout > 0:
self.logger.info(f"Process update [ID:{update.update_id}]: [failed] (in {timeout} ms)")
+ async def on_pre_process_poll(self, poll, data):
+ self.logger.info(f"Received poll [ID:{poll.id}]")
+
+ async def on_post_process_poll(self, poll, results, data):
+ self.logger.debug(f"{HANDLED_STR[bool(len(results))]} poll [ID:{poll.id}]")
+
+ async def on_pre_process_poll_answer(self, poll_answer, data):
+ self.logger.info(f"Received poll answer [ID:{poll_answer.poll_id}] "
+ f"from user [ID:{poll_answer.user.id}]")
+
+ async def on_post_process_poll_answer(self, poll_answer, results, data):
+ self.logger.debug(f"{HANDLED_STR[bool(len(results))]} poll answer [ID:{poll_answer.poll_id}] "
+ f"from user [ID:{poll_answer.user.id}]")
+
class LoggingFilter(logging.Filter):
"""
diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py
index 600e25ba..a236df57 100644
--- a/aiogram/dispatcher/dispatcher.py
+++ b/aiogram/dispatcher/dispatcher.py
@@ -10,7 +10,8 @@ from aiohttp.helpers import sentinel
from aiogram.utils.deprecated import renamed_argument
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
- RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter
+ RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter, ForwardedMessageFilter
+from .filters.builtin import IsSenderContact
from .handler import Handler
from .middlewares import MiddlewareManager
from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \
@@ -69,6 +70,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
self.shipping_query_handlers = Handler(self, middleware_key='shipping_query')
self.pre_checkout_query_handlers = Handler(self, middleware_key='pre_checkout_query')
self.poll_handlers = Handler(self, middleware_key='poll')
+ self.poll_answer_handlers = Handler(self, middleware_key='poll_answer')
self.errors_handlers = Handler(self, once=False, middleware_key='error')
self.middleware = MiddlewareManager(self)
@@ -87,6 +89,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
filters_factory.bind(StateFilter, exclude_event_handlers=[
self.errors_handlers,
self.poll_handlers,
+ self.poll_answer_handlers,
])
filters_factory.bind(ContentTypeFilter, event_handlers=[
self.message_handlers,
@@ -151,6 +154,18 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
self.channel_post_handlers,
self.edited_channel_post_handlers,
])
+ filters_factory.bind(IsSenderContact, event_handlers=[
+ self.message_handlers,
+ self.edited_message_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers,
+ ])
+ filters_factory.bind(ForwardedMessageFilter, event_handlers=[
+ self.message_handlers,
+ self.edited_channel_post_handlers,
+ self.channel_post_handlers,
+ self.edited_channel_post_handlers
+ ])
def __del__(self):
self.stop_polling()
@@ -194,38 +209,52 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
try:
if update.message:
+ types.Message.set_current(update.message)
types.User.set_current(update.message.from_user)
types.Chat.set_current(update.message.chat)
return await self.message_handlers.notify(update.message)
if update.edited_message:
+ types.Message.set_current(update.edited_message)
types.User.set_current(update.edited_message.from_user)
types.Chat.set_current(update.edited_message.chat)
return await self.edited_message_handlers.notify(update.edited_message)
if update.channel_post:
+ types.Message.set_current(update.channel_post)
types.Chat.set_current(update.channel_post.chat)
return await self.channel_post_handlers.notify(update.channel_post)
if update.edited_channel_post:
+ types.Message.set_current(update.edited_channel_post)
types.Chat.set_current(update.edited_channel_post.chat)
return await self.edited_channel_post_handlers.notify(update.edited_channel_post)
if update.inline_query:
+ types.InlineQuery.set_current(update.inline_query)
types.User.set_current(update.inline_query.from_user)
return await self.inline_query_handlers.notify(update.inline_query)
if update.chosen_inline_result:
+ types.ChosenInlineResult.set_current(update.chosen_inline_result)
types.User.set_current(update.chosen_inline_result.from_user)
return await self.chosen_inline_result_handlers.notify(update.chosen_inline_result)
if update.callback_query:
+ types.CallbackQuery.set_current(update.callback_query)
if update.callback_query.message:
types.Chat.set_current(update.callback_query.message.chat)
types.User.set_current(update.callback_query.from_user)
return await self.callback_query_handlers.notify(update.callback_query)
if update.shipping_query:
+ types.ShippingQuery.set_current(update.shipping_query)
types.User.set_current(update.shipping_query.from_user)
return await self.shipping_query_handlers.notify(update.shipping_query)
if update.pre_checkout_query:
+ types.PreCheckoutQuery.set_current(update.pre_checkout_query)
types.User.set_current(update.pre_checkout_query.from_user)
return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query)
if update.poll:
+ types.Poll.set_current(update.poll)
return await self.poll_handlers.notify(update.poll)
+ if update.poll_answer:
+ types.PollAnswer.set_current(update.poll_answer)
+ types.User.set_current(update.poll_answer.user)
+ return await self.poll_answer_handlers.notify(update.poll_answer)
except Exception as e:
err = await self.errors_handlers.notify(update, e)
if err:
@@ -853,18 +882,90 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
return decorator
def register_poll_handler(self, callback, *custom_filters, run_task=None, **kwargs):
+ """
+ Register handler for poll
+
+ Example:
+
+ .. code-block:: python3
+
+ dp.register_poll_handler(some_poll_handler)
+
+ :param callback:
+ :param custom_filters:
+ :param run_task: run callback in task (no wait results)
+ :param kwargs:
+ """
filters_set = self.filters_factory.resolve(self.poll_handlers,
*custom_filters,
**kwargs)
self.poll_handlers.register(self._wrap_async_task(callback, run_task), filters_set)
def poll_handler(self, *custom_filters, run_task=None, **kwargs):
+ """
+ Decorator for poll handler
+
+ Example:
+
+ .. code-block:: python3
+
+ @dp.poll_handler()
+ async def some_poll_handler(poll: types.Poll)
+
+ :param custom_filters:
+ :param run_task: run callback in task (no wait results)
+ :param kwargs:
+ """
+
def decorator(callback):
self.register_poll_handler(callback, *custom_filters, run_task=run_task,
**kwargs)
return callback
return decorator
+
+ def register_poll_answer_handler(self, callback, *custom_filters, run_task=None, **kwargs):
+ """
+ Register handler for poll_answer
+
+ Example:
+
+ .. code-block:: python3
+
+ dp.register_poll_answer_handler(some_poll_answer_handler)
+
+ :param callback:
+ :param custom_filters:
+ :param run_task: run callback in task (no wait results)
+ :param kwargs:
+ """
+ filters_set = self.filters_factory.resolve(self.poll_answer_handlers,
+ *custom_filters,
+ **kwargs)
+ self.poll_answer_handlers.register(self._wrap_async_task(callback, run_task), filters_set)
+
+ def poll_answer_handler(self, *custom_filters, run_task=None, **kwargs):
+ """
+ Decorator for poll_answer handler
+
+ Example:
+
+ .. code-block:: python3
+
+ @dp.poll_answer_handler()
+ async def some_poll_answer_handler(poll_answer: types.PollAnswer)
+
+ :param custom_filters:
+ :param run_task: run callback in task (no wait results)
+ :param kwargs:
+ """
+
+ def decorator(callback):
+ self.register_poll_answer_handler(callback, *custom_filters, run_task=run_task,
+ **kwargs)
+ return callback
+
+ return decorator
def register_errors_handler(self, callback, *custom_filters, exception=None, run_task=None, **kwargs):
"""
diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py
index 67c13872..edd1959a 100644
--- a/aiogram/dispatcher/filters/__init__.py
+++ b/aiogram/dispatcher/filters/__init__.py
@@ -1,6 +1,6 @@
from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \
ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \
- Text, IDFilter, AdminFilter, IsReplyFilter
+ Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact, ForwardedMessageFilter
from .factory import FiltersFactory
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
check_filters, get_filter_spec, get_filters_spec
@@ -26,9 +26,11 @@ __all__ = [
'Text',
'IDFilter',
'IsReplyFilter',
+ 'IsSenderContact',
'AdminFilter',
'get_filter_spec',
'get_filters_spec',
'execute_filter',
'check_filters',
+ 'ForwardedMessageFilter',
]
diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py
index 55ed63e5..c59d9b0d 100644
--- a/aiogram/dispatcher/filters/builtin.py
+++ b/aiogram/dispatcher/filters/builtin.py
@@ -12,6 +12,19 @@ from aiogram.dispatcher.filters.filters import BoundFilter, Filter
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType
+ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int]
+
+
+def extract_chat_ids(chat_id: ChatIDArgumentType) -> typing.Set[int]:
+ # since "str" is also an "Iterable", we have to check for it first
+ if isinstance(chat_id, str):
+ return {int(chat_id), }
+ if isinstance(chat_id, Iterable):
+ return {int(item) for (item) in chat_id}
+ # the last possible type is a single "int"
+ return {chat_id, }
+
+
class Command(Filter):
"""
You can handle commands by using this filter.
@@ -140,7 +153,9 @@ class CommandStart(Command):
This filter based on :obj:`Command` filter but can handle only ``/start`` command.
"""
- def __init__(self, deep_link: typing.Optional[typing.Union[str, re.Pattern]] = None):
+ def __init__(self,
+ deep_link: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None,
+ encoded: bool = False):
"""
Also this filter can handle `deep-linking `_ arguments.
@@ -151,9 +166,11 @@ class CommandStart(Command):
@dp.message_handler(CommandStart(re.compile(r'ref-([\\d]+)')))
:param deep_link: string or compiled regular expression (by ``re.compile(...)``).
+ :param encoded: set True if you're waiting for encoded payload (default - False).
"""
super().__init__(['start'])
self.deep_link = deep_link
+ self.encoded = encoded
async def check(self, message: types.Message):
"""
@@ -162,13 +179,16 @@ class CommandStart(Command):
:param message:
:return:
"""
+ from ...utils.deep_linking import decode_payload
check = await super().check(message)
if check and self.deep_link is not None:
- if not isinstance(self.deep_link, re.Pattern):
- return message.get_args() == self.deep_link
+ payload = decode_payload(message.get_args()) if self.encoded else message.get_args()
- match = self.deep_link.match(message.get_args())
+ if not isinstance(self.deep_link, typing.Pattern):
+ return False if payload != self.deep_link else {'deep_link': payload}
+
+ match = self.deep_link.match(payload)
if match:
return {'deep_link': match}
return False
@@ -244,7 +264,7 @@ class Text(Filter):
raise ValueError(f"No one mode is specified!")
equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy)
- else e,
+ else e,
(equals, contains, endswith, startswith))
self.equals = equals
self.contains = contains
@@ -370,7 +390,7 @@ class Regexp(Filter):
"""
def __init__(self, regexp):
- if not isinstance(regexp, re.Pattern):
+ if not isinstance(regexp, typing.Pattern):
regexp = re.compile(regexp, flags=re.IGNORECASE | re.MULTILINE)
self.regexp = regexp
@@ -437,6 +457,8 @@ class ContentTypeFilter(BoundFilter):
default = types.ContentTypes.TEXT
def __init__(self, content_types):
+ if isinstance(content_types, str):
+ content_types = (content_types,)
self.content_types = content_types
async def check(self, message):
@@ -444,6 +466,28 @@ class ContentTypeFilter(BoundFilter):
message.content_type in self.content_types
+class IsSenderContact(BoundFilter):
+ """
+ Filter check that the contact matches the sender
+
+ `is_sender_contact=True` - contact matches the sender
+ `is_sender_contact=False` - result will be inverted
+ """
+ key = 'is_sender_contact'
+
+ def __init__(self, is_sender_contact: bool):
+ self.is_sender_contact = is_sender_contact
+
+ async def check(self, message: types.Message) -> bool:
+ if not message.contact:
+ return False
+ is_sender_contact = message.contact.user_id == message.from_user.id
+ if self.is_sender_contact:
+ return is_sender_contact
+ else:
+ return not is_sender_contact
+
+
class StateFilter(BoundFilter):
"""
Check user state
@@ -514,10 +558,9 @@ class ExceptionsFilter(BoundFilter):
class IDFilter(Filter):
-
def __init__(self,
- user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
- chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
+ user_id: Optional[ChatIDArgumentType] = None,
+ chat_id: Optional[ChatIDArgumentType] = None,
):
"""
:param user_id:
@@ -526,18 +569,14 @@ class IDFilter(Filter):
if user_id is None and chat_id is None:
raise ValueError("Both user_id and chat_id can't be None")
- self.user_id = None
- self.chat_id = None
+ self.user_id: Optional[typing.Set[int]] = None
+ self.chat_id: Optional[typing.Set[int]] = None
+
if user_id:
- if isinstance(user_id, Iterable):
- self.user_id = list(map(int, user_id))
- else:
- self.user_id = [int(user_id), ]
+ self.user_id = extract_chat_ids(user_id)
+
if chat_id:
- if isinstance(chat_id, Iterable):
- self.chat_id = list(map(int, chat_id))
- else:
- self.chat_id = [int(chat_id), ]
+ self.chat_id = extract_chat_ids(chat_id)
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
@@ -583,22 +622,20 @@ class AdminFilter(Filter):
is_chat_admin is required for InlineQuery.
"""
- def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None):
+ def __init__(self, is_chat_admin: Optional[Union[ChatIDArgumentType, bool]] = None):
self._check_current = False
self._chat_ids = None
if is_chat_admin is False:
raise ValueError("is_chat_admin cannot be False")
- if is_chat_admin:
- if isinstance(is_chat_admin, bool):
- self._check_current = is_chat_admin
- if isinstance(is_chat_admin, Iterable):
- self._chat_ids = list(is_chat_admin)
- else:
- self._chat_ids = [is_chat_admin]
- else:
+ if not is_chat_admin:
self._check_current = True
+ return
+
+ if isinstance(is_chat_admin, bool):
+ self._check_current = is_chat_admin
+ self._chat_ids = extract_chat_ids(is_chat_admin)
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
@@ -644,3 +681,13 @@ class IsReplyFilter(BoundFilter):
return {'reply': msg.reply_to_message}
elif not msg.reply_to_message and not self.is_reply:
return True
+
+
+class ForwardedMessageFilter(BoundFilter):
+ key = 'is_forwarded'
+
+ def __init__(self, is_forwarded: bool):
+ self.is_forwarded = is_forwarded
+
+ async def check(self, message: Message):
+ return bool(getattr(message, "forward_date")) is self.is_forwarded
diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py
index 135fe21e..ed2ebf99 100644
--- a/aiogram/dispatcher/webhook.py
+++ b/aiogram/dispatcher/webhook.py
@@ -182,7 +182,7 @@ class WebhookRequestHandler(web.View):
try:
try:
await waiter
- except asyncio.futures.CancelledError:
+ except asyncio.CancelledError:
fut.remove_done_callback(cb)
fut.cancel()
raise
@@ -1967,7 +1967,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
__slots__ = ('chat_id', 'title', 'description', 'payload', 'provider_token', 'start_parameter',
'currency', 'prices', 'photo_url', 'photo_size', 'photo_width', 'photo_height',
- 'need_name', 'need_phone_number', 'need_email', 'need_shipping_address', 'is_flexible',
+ 'need_name', 'need_phone_number', 'need_email', 'need_shipping_address',
+ 'send_phone_number_to_provider', 'send_email_to_provider', 'is_flexible',
'disable_notification', 'reply_to_message_id', 'reply_markup')
method = api.Methods.SEND_INVOICE
@@ -1988,6 +1989,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
need_phone_number: Optional[Boolean] = None,
need_email: Optional[Boolean] = None,
need_shipping_address: Optional[Boolean] = None,
+ send_phone_number_to_provider: Optional[Boolean] = None,
+ send_email_to_provider: Optional[Boolean] = None,
is_flexible: Optional[Boolean] = None,
disable_notification: Optional[Boolean] = None,
reply_to_message_id: Optional[Integer] = None,
@@ -2016,6 +2019,10 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
:param need_email: Boolean (Optional) - Pass True, if you require the user's email to complete the order
:param need_shipping_address: Boolean (Optional) - Pass True, if you require the user's
shipping address to complete the order
+ :param send_phone_number_to_provider: Boolean (Optional) - Pass True, if user's phone number should be sent
+ to provider
+ :param send_email_to_provider: Boolean (Optional) - Pass True, if user's email address should be sent
+ to provider
:param is_flexible: Boolean (Optional) - Pass True, if the final price depends on the shipping method
:param disable_notification: Boolean (Optional) - Sends the message silently.
Users will receive a notification with no sound.
@@ -2039,6 +2046,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
self.need_phone_number = need_phone_number
self.need_email = need_email
self.need_shipping_address = need_shipping_address
+ self.send_phone_number_to_provider = send_phone_number_to_provider
+ self.send_email_to_provider = send_email_to_provider
self.is_flexible = is_flexible
self.disable_notification = disable_notification
self.reply_to_message_id = reply_to_message_id
@@ -2062,6 +2071,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin):
'need_phone_number': self.need_phone_number,
'need_email': self.need_email,
'need_shipping_address': self.need_shipping_address,
+ 'send_phone_number_to_provider': self.send_phone_number_to_provider,
+ 'send_email_to_provider': self.send_email_to_provider,
'is_flexible': self.is_flexible,
'disable_notification': self.disable_notification,
'reply_to_message_id': self.reply_to_message_id,
diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py
index 37dc4b3e..1221ec72 100644
--- a/aiogram/types/__init__.py
+++ b/aiogram/types/__init__.py
@@ -3,6 +3,7 @@ from . import fields
from .animation import Animation
from .audio import Audio
from .auth_widget_data import AuthWidgetData
+from .bot_command import BotCommand
from .callback_game import CallbackGame
from .callback_query import CallbackQuery
from .chat import Chat, ChatActions, ChatType
@@ -11,6 +12,7 @@ from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
from .chosen_inline_result import ChosenInlineResult
from .contact import Contact
+from .dice import Dice, DiceEmoji
from .document import Document
from .encrypted_credentials import EncryptedCredentials
from .encrypted_passport_element import EncryptedPassportElement
@@ -45,9 +47,9 @@ from .passport_element_error import PassportElementError, PassportElementErrorDa
PassportElementErrorSelfie
from .passport_file import PassportFile
from .photo_size import PhotoSize
-from .poll import PollOption, Poll
+from .poll import PollOption, Poll, PollAnswer, PollType
from .pre_checkout_query import PreCheckoutQuery
-from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
+from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType
from .response_parameters import ResponseParameters
from .shipping_address import ShippingAddress
from .shipping_option import ShippingOption
@@ -69,6 +71,7 @@ __all__ = (
'Animation',
'Audio',
'AuthWidgetData',
+ 'BotCommand',
'CallbackGame',
'CallbackQuery',
'Chat',
@@ -81,6 +84,8 @@ __all__ = (
'Contact',
'ContentType',
'ContentTypes',
+ 'Dice',
+ 'DiceEmoji',
'Document',
'EncryptedCredentials',
'EncryptedPassportElement',
@@ -126,6 +131,7 @@ __all__ = (
'InputVenueMessageContent',
'Invoice',
'KeyboardButton',
+ 'KeyboardButtonPollType',
'LabeledPrice',
'Location',
'LoginUrl',
@@ -147,7 +153,9 @@ __all__ = (
'PassportFile',
'PhotoSize',
'Poll',
+ 'PollAnswer',
'PollOption',
+ 'PollType',
'PreCheckoutQuery',
'ReplyKeyboardMarkup',
'ReplyKeyboardRemove',
diff --git a/aiogram/types/animation.py b/aiogram/types/animation.py
index fd470b38..78f5235a 100644
--- a/aiogram/types/animation.py
+++ b/aiogram/types/animation.py
@@ -14,6 +14,7 @@ class Animation(base.TelegramObject, mixins.Downloadable):
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
file_name: base.String = fields.Field()
mime_type: base.String = fields.Field()
diff --git a/aiogram/types/audio.py b/aiogram/types/audio.py
index 9423d02c..6859668f 100644
--- a/aiogram/types/audio.py
+++ b/aiogram/types/audio.py
@@ -11,6 +11,7 @@ class Audio(base.TelegramObject, mixins.Downloadable):
https://core.telegram.org/bots/api#audio
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
duration: base.Integer = fields.Field()
performer: base.String = fields.Field()
title: base.String = fields.Field()
diff --git a/aiogram/types/base.py b/aiogram/types/base.py
index 97f67b16..e64d3398 100644
--- a/aiogram/types/base.py
+++ b/aiogram/types/base.py
@@ -9,6 +9,8 @@ from babel.support import LazyProxy
from .fields import BaseField
from ..utils import json
from ..utils.mixins import ContextInstanceMixin
+if typing.TYPE_CHECKING:
+ from ..bot.bot import Bot
__all__ = ('MetaTelegramObject', 'TelegramObject', 'InputFile', 'String', 'Integer', 'Float', 'Boolean')
@@ -22,6 +24,7 @@ String = TypeVar('String', bound=str)
Integer = TypeVar('Integer', bound=int)
Float = TypeVar('Float', bound=float)
Boolean = TypeVar('Boolean', bound=bool)
+T = TypeVar('T')
class MetaTelegramObject(type):
@@ -30,7 +33,7 @@ class MetaTelegramObject(type):
"""
_objects = {}
- def __new__(mcs, name, bases, namespace, **kwargs):
+ def __new__(mcs: typing.Type[T], name: str, bases: typing.Tuple[typing.Type], namespace: typing.Dict[str, typing.Any], **kwargs: typing.Any) -> T:
cls = super(MetaTelegramObject, mcs).__new__(mcs, name, bases, namespace)
props = {}
@@ -71,7 +74,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
Abstract class for telegram objects
"""
- def __init__(self, conf=None, **kwargs):
+ def __init__(self, conf: typing.Dict[str, typing.Any]=None, **kwargs: typing.Any) -> None:
"""
Deserialize object
@@ -117,7 +120,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return getattr(self, ALIASES_ATTR_NAME, {})
@property
- def values(self):
+ def values(self) -> typing.Dict[str, typing.Any]:
"""
Get values
@@ -128,11 +131,11 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return getattr(self, VALUES_ATTR_NAME)
@property
- def telegram_types(self):
+ def telegram_types(self) -> typing.List[TelegramObject]:
return type(self).telegram_types
@classmethod
- def to_object(cls, data):
+ def to_object(cls: typing.Type[T], data: typing.Dict[str, typing.Any]) -> T:
"""
Deserialize object
@@ -142,7 +145,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return cls(**data)
@property
- def bot(self):
+ def bot(self) -> Bot:
from ..bot.bot import Bot
bot = Bot.get_current()
@@ -152,15 +155,16 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
"'Bot.set_current(bot_instance)'")
return bot
- def to_python(self) -> typing.Dict:
+ def to_python(self) -> typing.Dict[str, typing.Any]:
"""
Get object as JSON serializable
:return:
"""
- self.clean()
result = {}
for name, value in self.values.items():
+ if value is None:
+ continue
if name in self.props:
value = self.props[name].export(self)
if isinstance(value, TelegramObject):
@@ -170,7 +174,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
result[self.props_aliases.get(name, name)] = value
return result
- def clean(self):
+ def clean(self) -> None:
"""
Remove empty values
"""
@@ -188,7 +192,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return json.dumps(self.to_python())
@classmethod
- def create(cls, *args, **kwargs):
+ def create(cls: typing.Type[T], *args: typing.Any, **kwargs: typing.Any) -> T:
raise NotImplemented
def __str__(self) -> str:
@@ -199,7 +203,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
"""
return self.as_json()
- def __getitem__(self, item):
+ def __getitem__(self, item: typing.Union[str, int]) -> typing.Any:
"""
Item getter (by key)
@@ -210,7 +214,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return self.props[item].get_value(self)
raise KeyError(item)
- def __setitem__(self, key, value):
+ def __setitem__(self, key: str, value: typing.Any) -> None:
"""
Item setter (by key)
@@ -222,17 +226,17 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return self.props[key].set_value(self, value, self.conf.get('parent', None))
raise KeyError(key)
- def __contains__(self, item):
+ def __contains__(self, item: str) -> bool:
"""
Check key contains in that object
:param item:
:return:
"""
- self.clean()
- return item in self.values
+ # self.clean()
+ return bool(self.values.get(item, None))
- def __iter__(self):
+ def __iter__(self) -> typing.Iterator[str]:
"""
Iterate over items
@@ -241,7 +245,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for item in self.to_python().items():
yield item
- def iter_keys(self):
+ def iter_keys(self) -> typing.Generator[typing.Any, None, None]:
"""
Iterate over keys
@@ -250,7 +254,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for key, _ in self:
yield key
- def iter_values(self):
+ def iter_values(self) -> typing.Generator[typing.Any, None, None]:
"""
Iterate over values
@@ -259,9 +263,9 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
for _, value in self:
yield value
- def __hash__(self):
- def _hash(obj):
- buf = 0
+ def __hash__(self) -> int:
+ def _hash(obj) -> int:
+ buf: int = 0
if isinstance(obj, list):
for item in obj:
buf += _hash(item)
@@ -281,5 +285,5 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject):
return result
- def __eq__(self, other):
+ def __eq__(self, other: TelegramObject) -> bool:
return isinstance(other, self.__class__) and hash(other) == hash(self)
diff --git a/aiogram/types/bot_command.py b/aiogram/types/bot_command.py
new file mode 100644
index 00000000..39e38e4f
--- /dev/null
+++ b/aiogram/types/bot_command.py
@@ -0,0 +1,15 @@
+from . import base
+from . import fields
+
+
+class BotCommand(base.TelegramObject):
+ """
+ This object represents a bot command.
+
+ https://core.telegram.org/bots/api#botcommand
+ """
+ command: base.String = fields.Field()
+ description: base.String = fields.Field()
+
+ def __init__(self, command: base.String, description: base.String):
+ super(BotCommand, self).__init__(command=command, description=description)
\ No newline at end of file
diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py
index f5c521a5..4a7287d8 100644
--- a/aiogram/types/chat.py
+++ b/aiogram/types/chat.py
@@ -1,14 +1,15 @@
from __future__ import annotations
import asyncio
+import datetime
import typing
-from . import base
-from . import fields
+from ..utils import helper, markdown
+from . import base, fields
+from .chat_member import ChatMember
from .chat_permissions import ChatPermissions
from .chat_photo import ChatPhoto
-from ..utils import helper
-from ..utils import markdown
+from .input_file import InputFile
class Chat(base.TelegramObject):
@@ -29,6 +30,7 @@ class Chat(base.TelegramObject):
invite_link: base.String = fields.Field()
pinned_message: 'Message' = fields.Field(base='Message')
permissions: ChatPermissions = fields.Field(base=ChatPermissions)
+ slow_mode_delay: base.Integer = fields.Field()
sticker_set_name: base.String = fields.Field()
can_set_sticker_set: base.Boolean = fields.Field()
@@ -36,7 +38,7 @@ class Chat(base.TelegramObject):
return self.id
@property
- def full_name(self):
+ def full_name(self) -> base.String:
if self.type == ChatType.PRIVATE:
full_name = self.first_name
if self.last_name:
@@ -45,7 +47,7 @@ class Chat(base.TelegramObject):
return self.title
@property
- def mention(self):
+ def mention(self) -> typing.Union[base.String, None]:
"""
Get mention if a Chat has a username, or get full name if this is a Private Chat, otherwise None is returned
"""
@@ -56,20 +58,35 @@ class Chat(base.TelegramObject):
return None
@property
- def user_url(self):
+ def user_url(self) -> base.String:
if self.type != ChatType.PRIVATE:
raise TypeError('`user_url` property is only available in private chats!')
return f"tg://user?id={self.id}"
- def get_mention(self, name=None, as_html=False):
+ @property
+ def shifted_id(self) -> int:
+ """
+ Get shifted id of chat, e.g. for private links
+
+ For example: -1001122334455 -> 1122334455
+ """
+ if self.type == ChatType.PRIVATE:
+ raise TypeError('`shifted_id` property is not available for private chats')
+ shift = -1_000_000_000_000
+ return shift - self.id
+
+ def get_mention(self, name=None, as_html=True) -> base.String:
+ if as_html is None and self.bot.parse_mode and self.bot.parse_mode.lower() == 'html':
+ as_html = True
+
if name is None:
name = self.mention
if as_html:
return markdown.hlink(name, self.user_url)
return markdown.link(name, self.user_url)
- async def get_url(self):
+ async def get_url(self) -> base.String:
"""
Use this method to get chat link.
Private chat returns user link.
@@ -100,7 +117,7 @@ class Chat(base.TelegramObject):
for key, value in other:
self[key] = value
- async def set_photo(self, photo):
+ async def set_photo(self, photo: InputFile) -> base.Boolean:
"""
Use this method to set a new profile photo for the chat. Photos can't be changed for private chats.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -117,7 +134,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.set_chat_photo(self.id, photo)
- async def delete_photo(self):
+ async def delete_photo(self) -> base.Boolean:
"""
Use this method to delete a chat photo. Photos can't be changed for private chats.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -132,7 +149,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.delete_chat_photo(self.id)
- async def set_title(self, title):
+ async def set_title(self, title: base.String) -> base.Boolean:
"""
Use this method to change the title of a chat. Titles can't be changed for private chats.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -149,7 +166,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.set_chat_title(self.id, title)
- async def set_description(self, description):
+ async def set_description(self, description: base.String) -> base.Boolean:
"""
Use this method to change the description of a supergroup or a channel.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -161,10 +178,10 @@ class Chat(base.TelegramObject):
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
- return await self.bot.delete_chat_description(self.id, description)
+ return await self.bot.set_chat_description(self.id, description)
async def kick(self, user_id: base.Integer,
- until_date: typing.Union[base.Integer, None] = None):
+ until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = 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 group
@@ -187,7 +204,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.kick_chat_member(self.id, user_id=user_id, until_date=until_date)
- async def unban(self, user_id: base.Integer):
+ async def unban(self, user_id: base.Integer) -> base.Boolean:
"""
Use this method to unban a previously kicked user in a supergroup or channel. `
The user will not return to the group or channel automatically, but will be able to join via link, etc.
@@ -205,7 +222,7 @@ class Chat(base.TelegramObject):
async def restrict(self, user_id: base.Integer,
permissions: typing.Optional[ChatPermissions] = None,
- until_date: typing.Union[base.Integer, None] = None,
+ until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None,
can_send_messages: typing.Union[base.Boolean, None] = None,
can_send_media_messages: typing.Union[base.Boolean, None] = None,
can_send_other_messages: typing.Union[base.Boolean, None] = None,
@@ -295,7 +312,34 @@ class Chat(base.TelegramObject):
can_pin_messages=can_pin_messages,
can_promote_members=can_promote_members)
- async def pin_message(self, message_id: int, disable_notification: bool = False):
+ async def set_permissions(self, permissions: ChatPermissions) -> base.Boolean:
+ """
+ Use this method to set default chat permissions for all members.
+ The bot must be an administrator in the group or a supergroup for this to work and must have the
+ can_restrict_members admin rights.
+
+ Returns True on success.
+
+ :param permissions: New default chat permissions
+ :return: True on success.
+ """
+ return await self.bot.set_chat_permissions(self.id, permissions=permissions)
+
+ async def set_administrator_custom_title(self, user_id: base.Integer, custom_title: base.String) -> base.Boolean:
+ """
+ Use this method to set a custom title for an administrator in a supergroup promoted by the bot.
+
+ Returns True on success.
+
+ Source: https://core.telegram.org/bots/api#setchatadministratorcustomtitle
+
+ :param user_id: Unique identifier of the target user
+ :param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed
+ :return: True on success.
+ """
+ return await self.bot.set_chat_administrator_custom_title(chat_id=self.id, user_id=user_id, custom_title=custom_title)
+
+ async def pin_message(self, message_id: base.Integer, disable_notification: base.Boolean = False) -> base.Boolean:
"""
Use this method to pin a message in a supergroup.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -312,7 +356,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.pin_chat_message(self.id, message_id, disable_notification)
- async def unpin_message(self):
+ async def unpin_message(self) -> base.Boolean:
"""
Use this method to unpin a message in a supergroup chat.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -324,7 +368,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.unpin_chat_message(self.id)
- async def leave(self):
+ async def leave(self) -> base.Boolean:
"""
Use this method for your bot to leave a group, supergroup or channel.
@@ -335,7 +379,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.leave_chat(self.id)
- async def get_administrators(self):
+ async def get_administrators(self) -> typing.List[ChatMember]:
"""
Use this method to get a list of administrators in a chat.
@@ -349,7 +393,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.get_chat_administrators(self.id)
- async def get_members_count(self):
+ async def get_members_count(self) -> base.Integer:
"""
Use this method to get the number of members in a chat.
@@ -360,7 +404,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.get_chat_members_count(self.id)
- async def get_member(self, user_id):
+ async def get_member(self, user_id: base.Integer) -> ChatMember:
"""
Use this method to get information about a member of a chat.
@@ -373,7 +417,39 @@ class Chat(base.TelegramObject):
"""
return await self.bot.get_chat_member(self.id, user_id)
- async def do(self, action):
+ async def set_sticker_set(self, sticker_set_name: base.String) -> base.Boolean:
+ """
+ Use this method to set a new group sticker set for a supergroup.
+ The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+
+ Use the field can_set_sticker_set optionally returned in getChat requests to check
+ if the bot can use this method.
+
+ Source: https://core.telegram.org/bots/api#setchatstickerset
+
+ :param sticker_set_name: Name of the sticker set to be set as the group sticker set
+ :type sticker_set_name: :obj:`base.String`
+ :return: Returns True on success
+ :rtype: :obj:`base.Boolean`
+ """
+ return await self.bot.set_chat_sticker_set(self.id, sticker_set_name=sticker_set_name)
+
+ async def delete_sticker_set(self) -> base.Boolean:
+ """
+ Use this method to delete a group sticker set from a supergroup.
+ The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
+
+ Use the field can_set_sticker_set optionally returned in getChat requests
+ to check if the bot can use this method.
+
+ Source: https://core.telegram.org/bots/api#deletechatstickerset
+
+ :return: Returns True on success
+ :rtype: :obj:`base.Boolean`
+ """
+ return await self.bot.delete_chat_sticker_set(self.id)
+
+ async def do(self, 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
@@ -391,7 +467,7 @@ class Chat(base.TelegramObject):
"""
return await self.bot.send_chat_action(self.id, action)
- async def export_invite_link(self):
+ async def export_invite_link(self) -> base.String:
"""
Use this method to export an invite link to a supergroup or a channel.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py
index 7e05a33f..347b2750 100644
--- a/aiogram/types/chat_member.py
+++ b/aiogram/types/chat_member.py
@@ -16,6 +16,7 @@ class ChatMember(base.TelegramObject):
"""
user: User = fields.Field(base=User)
status: base.String = fields.Field()
+ custom_title: base.String = fields.Field()
until_date: datetime.datetime = fields.DateTimeField()
can_be_edited: base.Boolean = fields.Field()
can_change_info: base.Boolean = fields.Field()
diff --git a/aiogram/types/chat_photo.py b/aiogram/types/chat_photo.py
index 08775d93..d0282a58 100644
--- a/aiogram/types/chat_photo.py
+++ b/aiogram/types/chat_photo.py
@@ -12,7 +12,9 @@ class ChatPhoto(base.TelegramObject):
https://core.telegram.org/bots/api#chatphoto
"""
small_file_id: base.String = fields.Field()
+ small_file_unique_id: base.String = fields.Field()
big_file_id: base.String = fields.Field()
+ big_file_unique_id: base.String = fields.Field()
async def download_small(self, destination=None, timeout=30, chunk_size=65536, seek=True, make_dirs=True):
"""
diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py
new file mode 100644
index 00000000..7b3f1727
--- /dev/null
+++ b/aiogram/types/dice.py
@@ -0,0 +1,19 @@
+from . import base, fields
+
+
+class Dice(base.TelegramObject):
+ """
+ This object represents a dice with random value from 1 to 6.
+ (Yes, we're aware of the “proper” singular of die.
+ But it's awkward, and we decided to help it change. One dice at a time!)
+
+ https://core.telegram.org/bots/api#dice
+ """
+ emoji: base.String = fields.Field()
+ value: base.Integer = fields.Field()
+
+
+class DiceEmoji:
+ DICE = '🎲'
+ DART = '🎯'
+ BASKETBALL = '🏀'
diff --git a/aiogram/types/document.py b/aiogram/types/document.py
index 32d943d8..e15b745d 100644
--- a/aiogram/types/document.py
+++ b/aiogram/types/document.py
@@ -11,6 +11,7 @@ class Document(base.TelegramObject, mixins.Downloadable):
https://core.telegram.org/bots/api#document
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
file_name: base.String = fields.Field()
mime_type: base.String = fields.Field()
diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py
index e0d5b892..022b9b72 100644
--- a/aiogram/types/fields.py
+++ b/aiogram/types/fields.py
@@ -1,5 +1,6 @@
import abc
import datetime
+import weakref
__all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists')
@@ -109,7 +110,9 @@ class Field(BaseField):
and self.base_object is not None \
and not hasattr(value, 'base_object') \
and not hasattr(value, 'to_python'):
- return self.base_object(conf={'parent': parent}, **value)
+ if not isinstance(parent, weakref.ReferenceType):
+ parent = weakref.ref(parent)
+ return self.base_object(conf={'parent':parent}, **value)
return value
diff --git a/aiogram/types/file.py b/aiogram/types/file.py
index f3269f29..ae813ac6 100644
--- a/aiogram/types/file.py
+++ b/aiogram/types/file.py
@@ -17,5 +17,6 @@ class File(base.TelegramObject, mixins.Downloadable):
https://core.telegram.org/bots/api#file
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
file_size: base.Integer = fields.Field()
file_path: base.String = fields.Field()
diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py
index a80352d7..fccaa2a1 100644
--- a/aiogram/types/inline_query_result.py
+++ b/aiogram/types/inline_query_result.py
@@ -92,12 +92,13 @@ class InlineQueryResultPhoto(InlineQueryResult):
title: typing.Optional[base.String] = None,
description: typing.Optional[base.String] = None,
caption: typing.Optional[base.String] = None,
+ parse_mode: typing.Optional[base.String] = None,
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
input_message_content: typing.Optional[InputMessageContent] = None):
super(InlineQueryResultPhoto, self).__init__(id=id, photo_url=photo_url, thumb_url=thumb_url,
photo_width=photo_width, photo_height=photo_height, title=title,
description=description, caption=caption,
- reply_markup=reply_markup,
+ parse_mode=parse_mode, reply_markup=reply_markup,
input_message_content=input_message_content)
@@ -117,6 +118,7 @@ class InlineQueryResultGif(InlineQueryResult):
gif_height: base.Integer = fields.Field()
gif_duration: base.Integer = fields.Field()
thumb_url: base.String = fields.Field()
+ thumb_mime_type: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
@@ -156,6 +158,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
mpeg4_height: base.Integer = fields.Field()
mpeg4_duration: base.Integer = fields.Field()
thumb_url: base.String = fields.Field()
+ thumb_mime_type: base.String = fields.Field()
title: base.String = fields.Field()
caption: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py
index 95ca75ae..d42fac99 100644
--- a/aiogram/types/input_media.py
+++ b/aiogram/types/input_media.py
@@ -137,8 +137,6 @@ class InputMediaAudio(InputMedia):
https://core.telegram.org/bots/api#inputmediaanimation
"""
- width: base.Integer = fields.Field()
- height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
performer: base.String = fields.Field()
title: base.String = fields.Field()
@@ -146,13 +144,12 @@ class InputMediaAudio(InputMedia):
def __init__(self, media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
- width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.String = None, **kwargs):
- super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
- width=width, height=height, duration=duration,
+ super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb,
+ caption=caption, duration=duration,
performer=performer, title=title,
parse_mode=parse_mode, conf=kwargs)
@@ -349,7 +346,7 @@ class MediaGroup(base.TelegramObject):
:return:
"""
- self.clean()
+ # self.clean()
result = []
for obj in self.media:
if isinstance(obj, base.TelegramObject):
diff --git a/aiogram/types/message.py b/aiogram/types/message.py
index 5347027c..c56a143a 100644
--- a/aiogram/types/message.py
+++ b/aiogram/types/message.py
@@ -2,27 +2,29 @@ from __future__ import annotations
import datetime
import functools
-import sys
import typing
-from . import base
-from . import fields
+from ..utils import helper
+from ..utils import markdown as md
+from ..utils.text_decorations import html_decoration, markdown_decoration
+from . import base, fields
from .animation import Animation
from .audio import Audio
from .chat import Chat, ChatType
from .contact import Contact
+from .dice import Dice
from .document import Document
from .force_reply import ForceReply
from .game import Game
from .inline_keyboard import InlineKeyboardMarkup
-from .input_media import MediaGroup, InputMedia
+from .input_media import InputMedia, MediaGroup
from .invoice import Invoice
from .location import Location
from .message_entity import MessageEntity
from .passport_data import PassportData
from .photo_size import PhotoSize
from .poll import Poll
-from .reply_keyboard import ReplyKeyboardRemove, ReplyKeyboardMarkup
+from .reply_keyboard import ReplyKeyboardMarkup, ReplyKeyboardRemove
from .sticker import Sticker
from .successful_payment import SuccessfulPayment
from .user import User
@@ -30,8 +32,6 @@ from .venue import Venue
from .video import Video
from .video_note import VideoNote
from .voice import Voice
-from ..utils import helper
-from ..utils import markdown as md
class Message(base.TelegramObject):
@@ -40,8 +40,9 @@ class Message(base.TelegramObject):
https://core.telegram.org/bots/api#message
"""
+
message_id: base.Integer = fields.Field()
- from_user: User = fields.Field(alias='from', base=User)
+ from_user: User = fields.Field(alias="from", base=User)
date: datetime.datetime = fields.DateTimeField()
chat: Chat = fields.Field(base=Chat)
forward_from: User = fields.Field(base=User)
@@ -49,7 +50,8 @@ class Message(base.TelegramObject):
forward_from_message_id: base.Integer = fields.Field()
forward_signature: base.String = fields.Field()
forward_date: datetime.datetime = fields.DateTimeField()
- reply_to_message: Message = fields.Field(base='Message')
+ reply_to_message: Message = fields.Field(base="Message")
+ via_bot: User = fields.Field(base=User)
edit_date: datetime.datetime = fields.DateTimeField()
media_group_id: base.String = fields.Field()
author_signature: base.String = fields.Field()
@@ -70,6 +72,8 @@ class Message(base.TelegramObject):
contact: Contact = fields.Field(base=Contact)
location: Location = fields.Field(base=Location)
venue: Venue = fields.Field(base=Venue)
+ poll: Poll = fields.Field(base=Poll)
+ dice: Dice = fields.Field(base=Dice)
new_chat_members: typing.List[User] = fields.ListField(base=User)
left_chat_member: User = fields.Field(base=User)
new_chat_title: base.String = fields.Field()
@@ -80,12 +84,11 @@ class Message(base.TelegramObject):
channel_chat_created: base.Boolean = fields.Field()
migrate_to_chat_id: base.Integer = fields.Field()
migrate_from_chat_id: base.Integer = fields.Field()
- pinned_message: Message = fields.Field(base='Message')
+ pinned_message: Message = fields.Field(base="Message")
invoice: Invoice = fields.Field(base=Invoice)
successful_payment: SuccessfulPayment = fields.Field(base=SuccessfulPayment)
connected_website: base.String = fields.Field()
passport_data: PassportData = fields.Field(base=PassportData)
- poll: Poll = fields.Field(base=Poll)
reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup)
@property
@@ -117,6 +120,10 @@ class Message(base.TelegramObject):
return ContentType.VENUE
if self.location:
return ContentType.LOCATION
+ if self.poll:
+ return ContentType.POLL
+ if self.dice:
+ return ContentType.DICE
if self.new_chat_members:
return ContentType.NEW_CHAT_MEMBERS
if self.left_chat_member:
@@ -143,8 +150,6 @@ class Message(base.TelegramObject):
return ContentType.GROUP_CHAT_CREATED
if self.passport_data:
return ContentType.PASSPORT_DATA
- if self.poll:
- return ContentType.POLL
return ContentType.UNKNOWN
@@ -154,7 +159,7 @@ class Message(base.TelegramObject):
:return: bool
"""
- return self.text and self.text.startswith('/')
+ return self.text and self.text.startswith("/")
def get_full_command(self):
"""
@@ -163,7 +168,8 @@ class Message(base.TelegramObject):
:return: tuple of (command, args)
"""
if self.is_command():
- command, _, args = self.text.partition(' ')
+ command, *args = self.text.split(maxsplit=1)
+ args = args[-1] if args else ""
return command, args
def get_command(self, pure=False):
@@ -176,7 +182,7 @@ class Message(base.TelegramObject):
if command:
command = command[0]
if pure:
- command, _, _ = command[1:].partition('@')
+ command, _, _ = command[1:].partition("@")
return command
def get_args(self):
@@ -187,7 +193,7 @@ class Message(base.TelegramObject):
"""
command = self.get_full_command()
if command:
- return command[1].strip()
+ return command[1]
def parse_entities(self, as_html=True):
"""
@@ -200,38 +206,10 @@ class Message(base.TelegramObject):
if text is None:
raise TypeError("This message doesn't have any text.")
- quote_fn = md.quote_html if as_html else md.escape_md
-
entities = self.entities or self.caption_entities
- if not entities:
- return quote_fn(text)
+ text_decorator = html_decoration if as_html else markdown_decoration
- if not sys.maxunicode == 0xffff:
- text = text.encode('utf-16-le')
-
- result = ''
- offset = 0
-
- for entity in sorted(entities, key=lambda item: item.offset):
- entity_text = entity.parse(text, as_html=as_html)
-
- if sys.maxunicode == 0xffff:
- part = text[offset:entity.offset]
- result += quote_fn(part) + entity_text
- else:
- part = text[offset * 2:entity.offset * 2]
- result += quote_fn(part.decode('utf-16-le')) + entity_text
-
- offset = entity.offset + entity.length
-
- if sys.maxunicode == 0xffff:
- part = text[offset:]
- result += quote_fn(part)
- else:
- part = text[offset * 2:]
- result += quote_fn(part.decode('utf-16-le'))
-
- return result
+ return text_decorator.unparse(text, entities)
@property
def md_text(self) -> str:
@@ -258,12 +236,19 @@ class Message(base.TelegramObject):
:return: str
"""
- if self.chat.type not in [ChatType.SUPER_GROUP, ChatType.CHANNEL]:
- raise TypeError('Invalid chat type!')
- elif not self.chat.username:
- raise TypeError('This chat does not have @username')
+ if ChatType.is_private(self.chat):
+ raise TypeError("Invalid chat type!")
- return f"https://t.me/{self.chat.username}/{self.message_id}"
+ url = "https://t.me/"
+ if self.chat.username:
+ # Generates public link
+ url += f"{self.chat.username}/"
+ else:
+ # Generates private link available for chat members
+ url += f"c/{self.chat.shifted_id}/"
+ url += f"{self.message_id}"
+
+ return url
def link(self, text, as_html=True) -> str:
"""
@@ -284,15 +269,21 @@ class Message(base.TelegramObject):
return md.hlink(text, url)
return md.link(text, url)
- async def answer(self, text: base.String,
- parse_mode: typing.Union[base.String, None] = None,
- disable_web_page_preview: typing.Union[base.Boolean, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer(
+ self,
+ text: base.String,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_web_page_preview: typing.Union[base.Boolean, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Answer to this message
@@ -314,23 +305,31 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_message(chat_id=self.chat.id,
- text=text,
- parse_mode=parse_mode,
- disable_web_page_preview=disable_web_page_preview,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_message(
+ chat_id=self.chat.id,
+ text=text,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_photo(self, photo: typing.Union[base.InputFile, base.String],
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_photo(
+ self,
+ photo: typing.Union[base.InputFile, base.String],
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send photos.
@@ -354,26 +353,34 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_photo(chat_id=self.chat.id,
- photo=photo,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_photo(
+ chat_id=self.chat.id,
+ photo=photo,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_audio(self, audio: typing.Union[base.InputFile, base.String],
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- duration: typing.Union[base.Integer, None] = None,
- performer: typing.Union[base.String, None] = None,
- title: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_audio(
+ self,
+ audio: typing.Union[base.InputFile, base.String],
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ duration: typing.Union[base.Integer, None] = None,
+ performer: typing.Union[base.String, None] = None,
+ title: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send audio files, if you want Telegram clients to display them in the music player.
Your audio must be in the .mp3 format.
@@ -405,30 +412,38 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_audio(chat_id=self.chat.id,
- audio=audio,
- caption=caption,
- parse_mode=parse_mode,
- duration=duration,
- performer=performer,
- title=title,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_audio(
+ chat_id=self.chat.id,
+ audio=audio,
+ caption=caption,
+ parse_mode=parse_mode,
+ duration=duration,
+ performer=performer,
+ title=title,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_animation(self, animation: typing.Union[base.InputFile, base.String],
- duration: typing.Union[base.Integer, None] = None,
- width: typing.Union[base.Integer, None] = None,
- height: typing.Union[base.Integer, None] = None,
- thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_animation(
+ self,
+ animation: typing.Union[base.InputFile, base.String],
+ duration: typing.Union[base.Integer, None] = None,
+ width: typing.Union[base.Integer, None] = None,
+ height: typing.Union[base.Integer, None] = None,
+ thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
@@ -465,27 +480,35 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_animation(self.chat.id,
- animation=animation,
- duration=duration,
- width=width,
- height=height,
- thumb=thumb,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_animation(
+ self.chat.id,
+ animation=animation,
+ duration=duration,
+ width=width,
+ height=height,
+ thumb=thumb,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_document(self, document: typing.Union[base.InputFile, base.String],
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_document(
+ self,
+ document: typing.Union[base.InputFile, base.String],
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send general files.
@@ -510,26 +533,34 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_document(chat_id=self.chat.id,
- document=document,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_document(
+ chat_id=self.chat.id,
+ document=document,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_video(self, video: typing.Union[base.InputFile, base.String],
- duration: typing.Union[base.Integer, None] = None,
- width: typing.Union[base.Integer, None] = None,
- height: typing.Union[base.Integer, None] = None,
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_video(
+ self,
+ video: typing.Union[base.InputFile, base.String],
+ duration: typing.Union[base.Integer, None] = None,
+ width: typing.Union[base.Integer, None] = None,
+ height: typing.Union[base.Integer, None] = None,
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send video files, Telegram clients support mp4 videos
(other formats may be sent as Document).
@@ -559,27 +590,35 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_video(chat_id=self.chat.id,
- video=video,
- duration=duration,
- width=width,
- height=height,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_video(
+ chat_id=self.chat.id,
+ video=video,
+ duration=duration,
+ width=width,
+ height=height,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_voice(self, voice: typing.Union[base.InputFile, base.String],
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- duration: typing.Union[base.Integer, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_voice(
+ self,
+ voice: typing.Union[base.InputFile, base.String],
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ duration: typing.Union[base.Integer, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send audio files, if you want Telegram clients to display the file
as a playable voice message.
@@ -608,24 +647,32 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_voice(chat_id=self.chat.id,
- voice=voice,
- caption=caption,
- parse_mode=parse_mode,
- duration=duration,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_voice(
+ chat_id=self.chat.id,
+ voice=voice,
+ caption=caption,
+ parse_mode=parse_mode,
+ duration=duration,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_video_note(self, video_note: typing.Union[base.InputFile, base.String],
- duration: typing.Union[base.Integer, None] = None,
- length: typing.Union[base.Integer, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_video_note(
+ self,
+ video_note: typing.Union[base.InputFile, base.String],
+ duration: typing.Union[base.Integer, None] = None,
+ length: typing.Union[base.Integer, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long.
Use this method to send video messages.
@@ -648,17 +695,22 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_video_note(chat_id=self.chat.id,
- video_note=video_note,
- duration=duration,
- length=length,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_video_note(
+ chat_id=self.chat.id,
+ video_note=video_note,
+ duration=duration,
+ length=length,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_media_group(self, media: typing.Union[MediaGroup, typing.List],
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply: base.Boolean = False) -> typing.List[Message]:
+ async def answer_media_group(
+ self,
+ media: typing.Union[MediaGroup, typing.List],
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply: base.Boolean = False,
+ ) -> typing.List[Message]:
"""
Use this method to send a group of photos or videos as an album.
@@ -672,20 +724,28 @@ class Message(base.TelegramObject):
:return: On success, an array of the sent Messages is returned.
:rtype: typing.List[types.Message]
"""
- return await self.bot.send_media_group(self.chat.id,
- media=media,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None)
+ return await self.bot.send_media_group(
+ self.chat.id,
+ media=media,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ )
- async def answer_location(self,
- latitude: base.Float, longitude: base.Float,
- live_period: typing.Union[base.Integer, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_location(
+ self,
+ latitude: base.Float,
+ longitude: base.Float,
+ live_period: typing.Union[base.Integer, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send point on the map.
@@ -707,24 +767,33 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_location(chat_id=self.chat.id,
- latitude=latitude,
- longitude=longitude,
- live_period=live_period,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_location(
+ chat_id=self.chat.id,
+ latitude=latitude,
+ longitude=longitude,
+ live_period=live_period,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_venue(self,
- latitude: base.Float, longitude: base.Float,
- title: base.String, address: base.String,
- foursquare_id: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_venue(
+ self,
+ latitude: base.Float,
+ longitude: base.Float,
+ title: base.String,
+ address: base.String,
+ foursquare_id: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send information about a venue.
@@ -750,24 +819,33 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_venue(chat_id=self.chat.id,
- latitude=latitude,
- longitude=longitude,
- title=title,
- address=address,
- foursquare_id=foursquare_id,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_venue(
+ chat_id=self.chat.id,
+ latitude=latitude,
+ longitude=longitude,
+ title=title,
+ address=address,
+ foursquare_id=foursquare_id,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_contact(self, phone_number: base.String,
- first_name: base.String, last_name: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_contact(
+ self,
+ phone_number: base.String,
+ first_name: base.String,
+ last_name: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send phone contacts.
@@ -789,20 +867,29 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_contact(chat_id=self.chat.id,
- phone_number=phone_number,
- first_name=first_name, last_name=last_name,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_contact(
+ chat_id=self.chat.id,
+ phone_number=phone_number,
+ first_name=first_name,
+ last_name=last_name,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def answer_sticker(self, sticker: typing.Union[base.InputFile, base.String],
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = False) -> Message:
+ async def answer_sticker(
+ self,
+ sticker: typing.Union[base.InputFile, base.String],
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
"""
Use this method to send .webp stickers.
@@ -820,21 +907,70 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_sticker(chat_id=self.chat.id,
- sticker=sticker,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_sticker(
+ chat_id=self.chat.id,
+ sticker=sticker,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply(self, text: base.String,
- parse_mode: typing.Union[base.String, None] = None,
- disable_web_page_preview: typing.Union[base.Boolean, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def answer_dice(
+ self,
+ emoji: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = False,
+ ) -> Message:
+ """
+ Use this method to send a dice, which will have a random value from 1 to 6.
+ On success, the sent Message is returned.
+ (Yes, we're aware of the “proper” singular of die.
+ But it's awkward, and we decided to help it change. One dice at a time!)
+
+ Source: https://core.telegram.org/bots/api#senddice
+
+ :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of “🎲” or “🎯”. Defauts to “🎲”
+ :type emoji: :obj:`typing.Union[base.String, None]`
+ :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
+ :type disable_notification: :obj:`typing.Union[base.Boolean, None]`
+ :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
+ custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
+ :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
+ types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
+ :param reply: fill 'reply_to_message_id'
+ :return: On success, the sent Message is returned.
+ :rtype: :obj:`types.Message`
+ """
+ return await self.bot.send_dice(
+ chat_id=self.chat.id,
+ disable_notification=disable_notification,
+ emoji=emoji,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
+
+ async def reply(
+ self,
+ text: base.String,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_web_page_preview: typing.Union[base.Boolean, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Reply to this message
@@ -856,23 +992,31 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_message(chat_id=self.chat.id,
- text=text,
- parse_mode=parse_mode,
- disable_web_page_preview=disable_web_page_preview,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_message(
+ chat_id=self.chat.id,
+ text=text,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_photo(self, photo: typing.Union[base.InputFile, base.String],
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_photo(
+ self,
+ photo: typing.Union[base.InputFile, base.String],
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send photos.
@@ -896,26 +1040,34 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_photo(chat_id=self.chat.id,
- photo=photo,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_photo(
+ chat_id=self.chat.id,
+ photo=photo,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_audio(self, audio: typing.Union[base.InputFile, base.String],
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- duration: typing.Union[base.Integer, None] = None,
- performer: typing.Union[base.String, None] = None,
- title: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_audio(
+ self,
+ audio: typing.Union[base.InputFile, base.String],
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ duration: typing.Union[base.Integer, None] = None,
+ performer: typing.Union[base.String, None] = None,
+ title: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send audio files, if you want Telegram clients to display them in the music player.
Your audio must be in the .mp3 format.
@@ -947,30 +1099,38 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_audio(chat_id=self.chat.id,
- audio=audio,
- caption=caption,
- parse_mode=parse_mode,
- duration=duration,
- performer=performer,
- title=title,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_audio(
+ chat_id=self.chat.id,
+ audio=audio,
+ caption=caption,
+ parse_mode=parse_mode,
+ duration=duration,
+ performer=performer,
+ title=title,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_animation(self, animation: typing.Union[base.InputFile, base.String],
- duration: typing.Union[base.Integer, None] = None,
- width: typing.Union[base.Integer, None] = None,
- height: typing.Union[base.Integer, None] = None,
- thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_animation(
+ self,
+ animation: typing.Union[base.InputFile, base.String],
+ duration: typing.Union[base.Integer, None] = None,
+ width: typing.Union[base.Integer, None] = None,
+ height: typing.Union[base.Integer, None] = None,
+ thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
@@ -1007,27 +1167,35 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_animation(self.chat.id,
- animation=animation,
- duration=duration,
- width=width,
- height=height,
- thumb=thumb,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_animation(
+ self.chat.id,
+ animation=animation,
+ duration=duration,
+ width=width,
+ height=height,
+ thumb=thumb,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_document(self, document: typing.Union[base.InputFile, base.String],
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_document(
+ self,
+ document: typing.Union[base.InputFile, base.String],
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send general files.
@@ -1052,26 +1220,34 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_document(chat_id=self.chat.id,
- document=document,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_document(
+ chat_id=self.chat.id,
+ document=document,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_video(self, video: typing.Union[base.InputFile, base.String],
- duration: typing.Union[base.Integer, None] = None,
- width: typing.Union[base.Integer, None] = None,
- height: typing.Union[base.Integer, None] = None,
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_video(
+ self,
+ video: typing.Union[base.InputFile, base.String],
+ duration: typing.Union[base.Integer, None] = None,
+ width: typing.Union[base.Integer, None] = None,
+ height: typing.Union[base.Integer, None] = None,
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send video files, Telegram clients support mp4 videos
(other formats may be sent as Document).
@@ -1101,27 +1277,35 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_video(chat_id=self.chat.id,
- video=video,
- duration=duration,
- width=width,
- height=height,
- caption=caption,
- parse_mode=parse_mode,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_video(
+ chat_id=self.chat.id,
+ video=video,
+ duration=duration,
+ width=width,
+ height=height,
+ caption=caption,
+ parse_mode=parse_mode,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_voice(self, voice: typing.Union[base.InputFile, base.String],
- caption: typing.Union[base.String, None] = None,
- parse_mode: typing.Union[base.String, None] = None,
- duration: typing.Union[base.Integer, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_voice(
+ self,
+ voice: typing.Union[base.InputFile, base.String],
+ caption: typing.Union[base.String, None] = None,
+ parse_mode: typing.Union[base.String, None] = None,
+ duration: typing.Union[base.Integer, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send audio files, if you want Telegram clients to display the file
as a playable voice message.
@@ -1150,24 +1334,32 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_voice(chat_id=self.chat.id,
- voice=voice,
- caption=caption,
- parse_mode=parse_mode,
- duration=duration,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_voice(
+ chat_id=self.chat.id,
+ voice=voice,
+ caption=caption,
+ parse_mode=parse_mode,
+ duration=duration,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_video_note(self, video_note: typing.Union[base.InputFile, base.String],
- duration: typing.Union[base.Integer, None] = None,
- length: typing.Union[base.Integer, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_video_note(
+ self,
+ video_note: typing.Union[base.InputFile, base.String],
+ duration: typing.Union[base.Integer, None] = None,
+ length: typing.Union[base.Integer, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long.
Use this method to send video messages.
@@ -1190,17 +1382,22 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_video_note(chat_id=self.chat.id,
- video_note=video_note,
- duration=duration,
- length=length,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_video_note(
+ chat_id=self.chat.id,
+ video_note=video_note,
+ duration=duration,
+ length=length,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_media_group(self, media: typing.Union[MediaGroup, typing.List],
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply: base.Boolean = True) -> typing.List[Message]:
+ async def reply_media_group(
+ self,
+ media: typing.Union[MediaGroup, typing.List],
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply: base.Boolean = True,
+ ) -> typing.List[Message]:
"""
Use this method to send a group of photos or videos as an album.
@@ -1214,20 +1411,28 @@ class Message(base.TelegramObject):
:return: On success, an array of the sent Messages is returned.
:rtype: typing.List[types.Message]
"""
- return await self.bot.send_media_group(self.chat.id,
- media=media,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None)
+ return await self.bot.send_media_group(
+ self.chat.id,
+ media=media,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ )
- async def reply_location(self,
- latitude: base.Float, longitude: base.Float,
- live_period: typing.Union[base.Integer, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_location(
+ self,
+ latitude: base.Float,
+ longitude: base.Float,
+ live_period: typing.Union[base.Integer, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send point on the map.
@@ -1249,24 +1454,33 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_location(chat_id=self.chat.id,
- latitude=latitude,
- longitude=longitude,
- live_period=live_period,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_location(
+ chat_id=self.chat.id,
+ latitude=latitude,
+ longitude=longitude,
+ live_period=live_period,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_venue(self,
- latitude: base.Float, longitude: base.Float,
- title: base.String, address: base.String,
- foursquare_id: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_venue(
+ self,
+ latitude: base.Float,
+ longitude: base.Float,
+ title: base.String,
+ address: base.String,
+ foursquare_id: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send information about a venue.
@@ -1292,24 +1506,33 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_venue(chat_id=self.chat.id,
- latitude=latitude,
- longitude=longitude,
- title=title,
- address=address,
- foursquare_id=foursquare_id,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_venue(
+ chat_id=self.chat.id,
+ latitude=latitude,
+ longitude=longitude,
+ title=title,
+ address=address,
+ foursquare_id=foursquare_id,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_contact(self, phone_number: base.String,
- first_name: base.String, last_name: typing.Union[base.String, None] = None,
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_contact(
+ self,
+ phone_number: base.String,
+ first_name: base.String,
+ last_name: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send phone contacts.
@@ -1331,20 +1554,29 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_contact(chat_id=self.chat.id,
- phone_number=phone_number,
- first_name=first_name, last_name=last_name,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_contact(
+ chat_id=self.chat.id,
+ phone_number=phone_number,
+ first_name=first_name,
+ last_name=last_name,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def reply_sticker(self, sticker: typing.Union[base.InputFile, base.String],
- disable_notification: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- ReplyKeyboardMarkup,
- ReplyKeyboardRemove,
- ForceReply, None] = None,
- reply: base.Boolean = True) -> Message:
+ async def reply_sticker(
+ self,
+ sticker: typing.Union[base.InputFile, base.String],
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
"""
Use this method to send .webp stickers.
@@ -1362,14 +1594,59 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
- return await self.bot.send_sticker(chat_id=self.chat.id,
- sticker=sticker,
- disable_notification=disable_notification,
- reply_to_message_id=self.message_id if reply else None,
- reply_markup=reply_markup)
+ return await self.bot.send_sticker(
+ chat_id=self.chat.id,
+ sticker=sticker,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
- async def forward(self, chat_id: typing.Union[base.Integer, base.String],
- disable_notification: typing.Union[base.Boolean, None] = None) -> Message:
+ async def reply_dice(
+ self,
+ emoji: typing.Union[base.String, None] = None,
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup,
+ ReplyKeyboardMarkup,
+ ReplyKeyboardRemove,
+ ForceReply,
+ None,
+ ] = None,
+ reply: base.Boolean = True,
+ ) -> Message:
+ """
+ Use this method to send a dice, which will have a random value from 1 to 6.
+ On success, the sent Message is returned.
+ (Yes, we're aware of the “proper” singular of die.
+ But it's awkward, and we decided to help it change. One dice at a time!)
+
+ Source: https://core.telegram.org/bots/api#senddice
+
+ :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of “🎲” or “🎯”. Defauts to “🎲”
+ :type emoji: :obj:`typing.Union[base.String, None]`
+ :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
+ :type disable_notification: :obj:`typing.Union[base.Boolean, None]`
+ :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard,
+ custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user
+ :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup,
+ types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]`
+ :param reply: fill 'reply_to_message_id'
+ :return: On success, the sent Message is returned.
+ :rtype: :obj:`types.Message`
+ """
+ return await self.bot.send_dice(
+ chat_id=self.chat.id,
+ disable_notification=disable_notification,
+ reply_to_message_id=self.message_id if reply else None,
+ reply_markup=reply_markup,
+ )
+
+ async def forward(
+ self,
+ chat_id: typing.Union[base.Integer, base.String],
+ disable_notification: typing.Union[base.Boolean, None] = None,
+ ) -> Message:
"""
Forward this message
@@ -1382,13 +1659,17 @@ class Message(base.TelegramObject):
:return: On success, the sent Message is returned
:rtype: :obj:`types.Message`
"""
- return await self.bot.forward_message(chat_id, self.chat.id, self.message_id, disable_notification)
+ return await self.bot.forward_message(
+ chat_id, self.chat.id, self.message_id, disable_notification
+ )
- async def edit_text(self, text: base.String,
- parse_mode: typing.Union[base.String, None] = None,
- disable_web_page_preview: typing.Union[base.Boolean, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- None] = None) -> typing.Union[Message, base.Boolean]:
+ async def edit_text(
+ self,
+ text: base.String,
+ parse_mode: typing.Union[base.String, None] = None,
+ disable_web_page_preview: typing.Union[base.Boolean, None] = None,
+ reply_markup: typing.Union[InlineKeyboardMarkup, None] = None,
+ ) -> typing.Union[Message, base.Boolean]:
"""
Use this method to edit text and game messages sent by the bot or via the bot (for inline bots).
@@ -1407,16 +1688,21 @@ class Message(base.TelegramObject):
the edited Message is returned, otherwise True is returned.
:rtype: :obj:`typing.Union[types.Message, base.Boolean]`
"""
- return await self.bot.edit_message_text(text=text,
- chat_id=self.chat.id, message_id=self.message_id,
- parse_mode=parse_mode,
- disable_web_page_preview=disable_web_page_preview,
- reply_markup=reply_markup)
+ return await self.bot.edit_message_text(
+ text=text,
+ chat_id=self.chat.id,
+ message_id=self.message_id,
+ parse_mode=parse_mode,
+ disable_web_page_preview=disable_web_page_preview,
+ reply_markup=reply_markup,
+ )
- async def edit_caption(self, caption: base.String,
- parse_mode: typing.Union[base.String, None] = None,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- None] = None) -> typing.Union[Message, base.Boolean]:
+ async def edit_caption(
+ self,
+ caption: base.String,
+ parse_mode: typing.Union[base.String, None] = None,
+ reply_markup: typing.Union[InlineKeyboardMarkup, None] = None,
+ ) -> typing.Union[Message, base.Boolean]:
"""
Use this method to edit captions of messages sent by the bot or via the bot (for inline bots).
@@ -1433,12 +1719,19 @@ class Message(base.TelegramObject):
otherwise True is returned.
:rtype: :obj:`typing.Union[types.Message, base.Boolean]`
"""
- return await self.bot.edit_message_caption(chat_id=self.chat.id, message_id=self.message_id, caption=caption,
- parse_mode=parse_mode, reply_markup=reply_markup)
+ return await self.bot.edit_message_caption(
+ chat_id=self.chat.id,
+ message_id=self.message_id,
+ caption=caption,
+ parse_mode=parse_mode,
+ reply_markup=reply_markup,
+ )
- async def edit_media(self, media: InputMedia,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- None] = None) -> typing.Union[Message, base.Boolean]:
+ async def edit_media(
+ self,
+ media: InputMedia,
+ reply_markup: typing.Union[InlineKeyboardMarkup, None] = None,
+ ) -> typing.Union[Message, base.Boolean]:
"""
Use this method to edit audio, document, photo, or video messages.
If a message is a part of a message album, then it can be edited only to a photo or a video.
@@ -1459,12 +1752,16 @@ class Message(base.TelegramObject):
otherwise True is returned
:rtype: :obj:`typing.Union[types.Message, base.Boolean]`
"""
- return await self.bot.edit_message_media(media=media, chat_id=self.chat.id, message_id=self.message_id,
- reply_markup=reply_markup)
+ return await self.bot.edit_message_media(
+ media=media,
+ chat_id=self.chat.id,
+ message_id=self.message_id,
+ reply_markup=reply_markup,
+ )
- async def edit_reply_markup(self,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- None] = None) -> typing.Union[Message, base.Boolean]:
+ async def edit_reply_markup(
+ self, reply_markup: typing.Union[InlineKeyboardMarkup, None] = None
+ ) -> typing.Union[Message, base.Boolean]:
"""
Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots).
@@ -1476,8 +1773,9 @@ class Message(base.TelegramObject):
otherwise True is returned.
:rtype: :obj:`typing.Union[types.Message, base.Boolean]`
"""
- return await self.bot.edit_message_reply_markup(chat_id=self.chat.id, message_id=self.message_id,
- reply_markup=reply_markup)
+ return await self.bot.edit_message_reply_markup(
+ chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup
+ )
async def delete_reply_markup(self) -> typing.Union[Message, base.Boolean]:
"""
@@ -1487,12 +1785,16 @@ class Message(base.TelegramObject):
otherwise True is returned.
:rtype: :obj:`typing.Union[types.Message, base.Boolean]`
"""
- return await self.bot.edit_message_reply_markup(chat_id=self.chat.id, message_id=self.message_id)
+ return await self.bot.edit_message_reply_markup(
+ chat_id=self.chat.id, message_id=self.message_id
+ )
- async def edit_live_location(self, latitude: base.Float,
- longitude: base.Float,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- None] = None) -> typing.Union[Message, base.Boolean]:
+ async def edit_live_location(
+ self,
+ latitude: base.Float,
+ longitude: base.Float,
+ reply_markup: typing.Union[InlineKeyboardMarkup, None] = None,
+ ) -> typing.Union[Message, base.Boolean]:
"""
Use this method to edit live location messages sent by the bot or via the bot (for inline bots).
A location can be edited until its live_period expires or editing is explicitly disabled by a call
@@ -1510,13 +1812,17 @@ class Message(base.TelegramObject):
otherwise True is returned.
:rtype: :obj:`typing.Union[types.Message, base.Boolean]`
"""
- return await self.bot.edit_message_live_location(latitude=latitude, longitude=longitude,
- chat_id=self.chat.id, message_id=self.message_id,
- reply_markup=reply_markup)
+ return await self.bot.edit_message_live_location(
+ latitude=latitude,
+ longitude=longitude,
+ chat_id=self.chat.id,
+ message_id=self.message_id,
+ reply_markup=reply_markup,
+ )
- async def stop_live_location(self,
- reply_markup: typing.Union[InlineKeyboardMarkup,
- None] = None) -> typing.Union[Message, base.Boolean]:
+ async def stop_live_location(
+ self, reply_markup: typing.Union[InlineKeyboardMarkup, None] = None
+ ) -> typing.Union[Message, base.Boolean]:
"""
Use this method to stop updating a live location message sent by the bot or via the bot
(for inline bots) before live_period expires.
@@ -1529,8 +1835,9 @@ class Message(base.TelegramObject):
otherwise True is returned.
:rtype: :obj:`typing.Union[types.Message, base.Boolean]`
"""
- return await self.bot.stop_message_live_location(chat_id=self.chat.id, message_id=self.message_id,
- reply_markup=reply_markup)
+ return await self.bot.stop_message_live_location(
+ chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup
+ )
async def delete(self) -> base.Boolean:
"""
@@ -1549,7 +1856,9 @@ class Message(base.TelegramObject):
"""
return await self.bot.delete_message(self.chat.id, self.message_id)
- async def pin(self, disable_notification: typing.Union[base.Boolean, None] = None) -> base.Boolean:
+ async def pin(
+ self, disable_notification: typing.Union[base.Boolean, None] = None
+ ) -> base.Boolean:
"""
Use this method to pin a message in a supergroup.
The bot must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -1565,30 +1874,30 @@ class Message(base.TelegramObject):
return await self.chat.pin_message(self.message_id, disable_notification)
async def send_copy(
- self: Message,
- chat_id: typing.Union[str, int],
- with_markup: bool = False,
- disable_notification: typing.Optional[bool] = None,
- reply_to_message_id: typing.Optional[int] = None,
+ self: Message,
+ chat_id: typing.Union[str, int],
+ disable_notification: typing.Optional[bool] = None,
+ reply_to_message_id: typing.Optional[int] = None,
+ reply_markup: typing.Union[
+ InlineKeyboardMarkup, ReplyKeyboardMarkup, None
+ ] = None,
) -> Message:
"""
Send copy of current message
:param chat_id:
- :param with_markup:
:param disable_notification:
:param reply_to_message_id:
+ :param reply_markup:
:return:
"""
- kwargs = {"chat_id": chat_id, "parse_mode": ParseMode.HTML}
-
- if disable_notification is not None:
- kwargs["disable_notification"] = disable_notification
- if reply_to_message_id is not None:
- kwargs["reply_to_message_id"] = reply_to_message_id
- if with_markup and self.reply_markup:
- kwargs["reply_markup"] = self.reply_markup
-
+ kwargs = {
+ "chat_id": chat_id,
+ "reply_markup": reply_markup or self.reply_markup,
+ "parse_mode": ParseMode.HTML,
+ "disable_notification": disable_notification,
+ "reply_to_message_id": reply_to_message_id,
+ }
text = self.html_text if (self.text or self.caption) else None
if self.text:
@@ -1600,7 +1909,7 @@ class Message(base.TelegramObject):
title=self.audio.title,
performer=self.audio.performer,
duration=self.audio.duration,
- **kwargs
+ **kwargs,
)
elif self.animation:
return await self.bot.send_animation(
@@ -1635,7 +1944,7 @@ class Message(base.TelegramObject):
first_name=self.contact.first_name,
last_name=self.contact.last_name,
vcard=self.contact.vcard,
- **kwargs
+ **kwargs,
)
elif self.venue:
kwargs.pop("parse_mode")
@@ -1646,17 +1955,21 @@ class Message(base.TelegramObject):
address=self.venue.address,
foursquare_id=self.venue.foursquare_id,
foursquare_type=self.venue.foursquare_type,
- **kwargs
+ **kwargs,
)
elif self.location:
kwargs.pop("parse_mode")
return await self.bot.send_location(
- latitude=self.location.latitude, longitude=self.location.longitude, **kwargs
+ latitude=self.location.latitude,
+ longitude=self.location.longitude,
+ **kwargs,
)
elif self.poll:
kwargs.pop("parse_mode")
return await self.bot.send_poll(
- question=self.poll.question, options=self.poll.options, **kwargs
+ question=self.poll.question,
+ options=[option.text for option in self.poll.options],
+ **kwargs,
)
else:
raise TypeError("This type of message can't be copied.")
@@ -1693,6 +2006,7 @@ class ContentType(helper.Helper):
:key: UNKNOWN
:key: ANY
"""
+
mode = helper.HelperMode.snake_case
TEXT = helper.Item() # text
@@ -1708,6 +2022,8 @@ class ContentType(helper.Helper):
CONTACT = helper.Item() # contact
LOCATION = helper.Item() # location
VENUE = helper.Item() # venue
+ POLL = helper.Item() # poll
+ DICE = helper.Item() # dice
NEW_CHAT_MEMBERS = helper.Item() # new_chat_member
LEFT_CHAT_MEMBER = helper.Item() # left_chat_member
INVOICE = helper.Item() # invoice
@@ -1721,7 +2037,6 @@ class ContentType(helper.Helper):
DELETE_CHAT_PHOTO = helper.Item() # delete_chat_photo
GROUP_CHAT_CREATED = helper.Item() # group_chat_created
PASSPORT_DATA = helper.Item() # passport_data
- POLL = helper.Item()
UNKNOWN = helper.Item() # unknown
ANY = helper.Item() # any
@@ -1755,6 +2070,7 @@ class ContentTypes(helper.Helper):
:key: UNKNOWN
:key: ANY
"""
+
mode = helper.HelperMode.snake_case
TEXT = helper.ListItem() # text
@@ -1800,4 +2116,5 @@ class ParseMode(helper.Helper):
mode = helper.HelperMode.lowercase
MARKDOWN = helper.Item()
+ MARKDOWN_V2 = helper.Item()
HTML = helper.Item()
diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py
index f0ad75d6..77b23c5c 100644
--- a/aiogram/types/message_entity.py
+++ b/aiogram/types/message_entity.py
@@ -4,6 +4,7 @@ from . import base
from . import fields
from .user import User
from ..utils import helper, markdown
+from ..utils.deprecated import deprecated
class MessageEntity(base.TelegramObject):
@@ -17,6 +18,7 @@ class MessageEntity(base.TelegramObject):
length: base.Integer = fields.Field()
url: base.String = fields.Field()
user: User = fields.Field(base=User)
+ language: base.String = fields.Field()
def get_text(self, text):
"""
@@ -36,6 +38,7 @@ class MessageEntity(base.TelegramObject):
entity_text = entity_text[self.offset * 2:(self.offset + self.length) * 2]
return entity_text.decode('utf-16-le')
+ @deprecated("This method doesn't work with nested entities and will be removed in aiogram 3.0")
def parse(self, text, as_html=True):
"""
Get entity value with markup
@@ -87,6 +90,8 @@ class MessageEntityType(helper.Helper):
:key: ITALIC
:key: CODE
:key: PRE
+ :key: UNDERLINE
+ :key: STRIKETHROUGH
:key: TEXT_LINK
:key: TEXT_MENTION
"""
@@ -101,7 +106,9 @@ class MessageEntityType(helper.Helper):
PHONE_NUMBER = helper.Item() # phone_number
BOLD = helper.Item() # bold - bold text
ITALIC = helper.Item() # italic - italic text
- CODE = helper.Item() # code - monowidth string
- PRE = helper.Item() # pre - monowidth block
+ CODE = helper.Item() # code - monowidth string
+ PRE = helper.Item() # pre - monowidth block
+ UNDERLINE = helper.Item() # underline
+ STRIKETHROUGH = helper.Item() # strikethrough
TEXT_LINK = helper.Item() # text_link - for clickable text URLs
TEXT_MENTION = helper.Item() # text_mention - for users without usernames
diff --git a/aiogram/types/passport_file.py b/aiogram/types/passport_file.py
index f00e80c7..de59e66b 100644
--- a/aiogram/types/passport_file.py
+++ b/aiogram/types/passport_file.py
@@ -9,7 +9,7 @@ class PassportFile(base.TelegramObject):
https://core.telegram.org/bots/api#passportfile
"""
-
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
file_size: base.Integer = fields.Field()
file_date: base.Integer = fields.Field()
diff --git a/aiogram/types/photo_size.py b/aiogram/types/photo_size.py
index c7ba59b6..cca95304 100644
--- a/aiogram/types/photo_size.py
+++ b/aiogram/types/photo_size.py
@@ -10,6 +10,7 @@ class PhotoSize(base.TelegramObject, mixins.Downloadable):
https://core.telegram.org/bots/api#photosize
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
file_size: base.Integer = fields.Field()
diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py
index 316bca2d..a709edcd 100644
--- a/aiogram/types/poll.py
+++ b/aiogram/types/poll.py
@@ -1,16 +1,83 @@
+import datetime
import typing
-from . import base
-from . import fields
+from . import base, fields
+from .message_entity import MessageEntity
+from .user import User
+from ..utils import helper
+from ..utils.text_decorations import html_decoration, markdown_decoration
class PollOption(base.TelegramObject):
+ """
+ This object contains information about one answer option in a poll.
+
+ https://core.telegram.org/bots/api#polloption
+ """
+
text: base.String = fields.Field()
voter_count: base.Integer = fields.Field()
+class PollAnswer(base.TelegramObject):
+ """
+ This object represents an answer of a user in a non-anonymous poll.
+
+ https://core.telegram.org/bots/api#pollanswer
+ """
+
+ poll_id: base.String = fields.Field()
+ user: User = fields.Field(base=User)
+ option_ids: typing.List[base.Integer] = fields.ListField()
+
+
class Poll(base.TelegramObject):
+ """
+ This object contains information about a poll.
+
+ https://core.telegram.org/bots/api#poll
+ """
+
id: base.String = fields.Field()
question: base.String = fields.Field()
options: typing.List[PollOption] = fields.ListField(base=PollOption)
+ total_voter_count: base.Integer = fields.Field()
is_closed: base.Boolean = fields.Field()
+ is_anonymous: base.Boolean = fields.Field()
+ type: base.String = fields.Field()
+ allows_multiple_answers: base.Boolean = fields.Field()
+ correct_option_id: base.Integer = fields.Field()
+ explanation: base.String = fields.Field()
+ explanation_entities: base.String = fields.ListField(base=MessageEntity)
+ open_period: base.Integer = fields.Field()
+ close_date: datetime.datetime = fields.DateTimeField()
+
+ def parse_entities(self, as_html=True):
+ text_decorator = html_decoration if as_html else markdown_decoration
+
+ return text_decorator.unparse(self.explanation or '', self.explanation_entities or [])
+
+ @property
+ def md_explanation(self) -> str:
+ """
+ Explanation formatted as markdown.
+
+ :return: str
+ """
+ return self.parse_entities(False)
+
+ @property
+ def html_explanation(self) -> str:
+ """
+ Explanation formatted as HTML
+
+ :return: str
+ """
+ return self.parse_entities()
+
+
+class PollType(helper.Helper):
+ mode = helper.HelperMode.snake_case
+
+ REGULAR = helper.Item()
+ QUIZ = helper.Item()
diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py
index 8eda21f9..ffe07ae1 100644
--- a/aiogram/types/reply_keyboard.py
+++ b/aiogram/types/reply_keyboard.py
@@ -4,6 +4,18 @@ from . import base
from . import fields
+class KeyboardButtonPollType(base.TelegramObject):
+ """
+ This object represents type of a poll, which is allowed to be created and sent when the corresponding button is pressed.
+
+ https://core.telegram.org/bots/api#keyboardbuttonpolltype
+ """
+ type: base.String = fields.Field()
+
+ def __init__(self, type: typing.Optional[base.String] = None):
+ super(KeyboardButtonPollType, self).__init__(type=type)
+
+
class ReplyKeyboardMarkup(base.TelegramObject):
"""
This object represents a custom keyboard with reply options (see Introduction to bots for details and examples).
@@ -81,21 +93,31 @@ class ReplyKeyboardMarkup(base.TelegramObject):
class KeyboardButton(base.TelegramObject):
"""
- This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. Optional fields are mutually exclusive.
- Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them.
+ This object represents one button of the reply keyboard.
+ For simple text buttons String can be used instead of this object to specify text of the button.
+ Optional fields request_contact, request_location, and request_poll are mutually exclusive.
+ Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016.
+ Older clients will ignore them.
+ Note: request_poll option will only work in Telegram versions released after 23 January, 2020.
+ Older clients will receive unsupported message.
https://core.telegram.org/bots/api#keyboardbutton
"""
text: base.String = fields.Field()
request_contact: base.Boolean = fields.Field()
request_location: base.Boolean = fields.Field()
+ request_poll: KeyboardButtonPollType = fields.Field()
def __init__(self, text: base.String,
request_contact: base.Boolean = None,
- request_location: base.Boolean = None):
+ request_location: base.Boolean = None,
+ request_poll: KeyboardButtonPollType = None,
+ **kwargs):
super(KeyboardButton, self).__init__(text=text,
request_contact=request_contact,
- request_location=request_location)
+ request_location=request_location,
+ request_poll=request_poll,
+ **kwargs)
class ReplyKeyboardRemove(base.TelegramObject):
diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py
index 8da1e9eb..ea222831 100644
--- a/aiogram/types/sticker.py
+++ b/aiogram/types/sticker.py
@@ -12,6 +12,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
https://core.telegram.org/bots/api#sticker
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
is_animated: base.Boolean = fields.Field()
@@ -20,3 +21,29 @@ class Sticker(base.TelegramObject, mixins.Downloadable):
set_name: base.String = fields.Field()
mask_position: MaskPosition = fields.Field(base=MaskPosition)
file_size: base.Integer = fields.Field()
+
+ async def set_position_in_set(self, position: base.Integer) -> base.Boolean:
+ """
+ Use this method to move a sticker in a set created by the bot to a specific position.
+
+ Source: https://core.telegram.org/bots/api#setstickerpositioninset
+
+ :param position: New sticker position in the set, zero-based
+ :type position: :obj:`base.Integer`
+ :return: Returns True on success
+ :rtype: :obj:`base.Boolean`
+ """
+ return await self.bot.set_sticker_position_in_set(self.file_id, position=position)
+
+ async def delete_from_set(self) -> base.Boolean:
+ """
+ Use this method to delete a sticker from a set created by the bot.
+
+ Source: https://core.telegram.org/bots/api#deletestickerfromset
+
+ :param sticker: File identifier of the sticker
+ :type sticker: :obj:`base.String`
+ :return: Returns True on success
+ :rtype: :obj:`base.Boolean`
+ """
+ return await self.bot.delete_sticker_from_set(self.file_id)
diff --git a/aiogram/types/sticker_set.py b/aiogram/types/sticker_set.py
index cb30abe6..3b5290c3 100644
--- a/aiogram/types/sticker_set.py
+++ b/aiogram/types/sticker_set.py
@@ -2,6 +2,7 @@ import typing
from . import base
from . import fields
+from .photo_size import PhotoSize
from .sticker import Sticker
@@ -16,3 +17,4 @@ class StickerSet(base.TelegramObject):
is_animated: base.Boolean = fields.Field()
contains_masks: base.Boolean = fields.Field()
stickers: typing.List[Sticker] = fields.ListField(base=Sticker)
+ thumb: PhotoSize = fields.Field(base=PhotoSize)
diff --git a/aiogram/types/update.py b/aiogram/types/update.py
index 9f8ce0fb..2146cb9d 100644
--- a/aiogram/types/update.py
+++ b/aiogram/types/update.py
@@ -6,7 +6,7 @@ from .callback_query import CallbackQuery
from .chosen_inline_result import ChosenInlineResult
from .inline_query import InlineQuery
from .message import Message
-from .poll import Poll
+from .poll import Poll, PollAnswer
from .pre_checkout_query import PreCheckoutQuery
from .shipping_query import ShippingQuery
from ..utils import helper
@@ -30,6 +30,7 @@ class Update(base.TelegramObject):
shipping_query: ShippingQuery = fields.Field(base=ShippingQuery)
pre_checkout_query: PreCheckoutQuery = fields.Field(base=PreCheckoutQuery)
poll: Poll = fields.Field(base=Poll)
+ poll_answer: PollAnswer = fields.Field(base=PollAnswer)
def __hash__(self):
return self.update_id
@@ -58,3 +59,5 @@ class AllowedUpdates(helper.Helper):
CALLBACK_QUERY = helper.ListItem() # callback_query
SHIPPING_QUERY = helper.ListItem() # shipping_query
PRE_CHECKOUT_QUERY = helper.ListItem() # pre_checkout_query
+ POLL = helper.ListItem() # poll
+ POLL_ANSWER = helper.ListItem() # poll_answer
diff --git a/aiogram/types/user.py b/aiogram/types/user.py
index 27ee27e0..8263cfc2 100644
--- a/aiogram/types/user.py
+++ b/aiogram/types/user.py
@@ -7,6 +7,7 @@ import babel
from . import base
from . import fields
from ..utils import markdown
+from ..utils.deprecated import deprecated
class User(base.TelegramObject):
@@ -21,6 +22,9 @@ class User(base.TelegramObject):
last_name: base.String = fields.Field()
username: base.String = fields.Field()
language_code: base.String = fields.Field()
+ can_join_groups: base.Boolean = fields.Field()
+ can_read_all_group_messages: base.Boolean = fields.Field()
+ supports_inline_queries: base.Boolean = fields.Field()
@property
def full_name(self):
@@ -73,9 +77,16 @@ class User(base.TelegramObject):
return markdown.hlink(name, self.url)
return markdown.link(name, self.url)
+ @deprecated(
+ '`get_user_profile_photos` is outdated, please use `get_profile_photos`',
+ stacklevel=3
+ )
async def get_user_profile_photos(self, offset=None, limit=None):
return await self.bot.get_user_profile_photos(self.id, offset, limit)
+ async def get_profile_photos(self, offset=None, limit=None):
+ return await self.bot.get_user_profile_photos(self.id, offset, limit)
+
def __hash__(self):
return self.id
diff --git a/aiogram/types/video.py b/aiogram/types/video.py
index bf5187cd..97dbb90f 100644
--- a/aiogram/types/video.py
+++ b/aiogram/types/video.py
@@ -11,6 +11,7 @@ class Video(base.TelegramObject, mixins.Downloadable):
https://core.telegram.org/bots/api#video
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
diff --git a/aiogram/types/video_note.py b/aiogram/types/video_note.py
index 9665b6bc..8702faae 100644
--- a/aiogram/types/video_note.py
+++ b/aiogram/types/video_note.py
@@ -11,6 +11,7 @@ class VideoNote(base.TelegramObject, mixins.Downloadable):
https://core.telegram.org/bots/api#videonote
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
length: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)
diff --git a/aiogram/types/voice.py b/aiogram/types/voice.py
index 621f2247..fd88e402 100644
--- a/aiogram/types/voice.py
+++ b/aiogram/types/voice.py
@@ -10,6 +10,7 @@ class Voice(base.TelegramObject, mixins.Downloadable):
https://core.telegram.org/bots/api#voice
"""
file_id: base.String = fields.Field()
+ file_unique_id: base.String = fields.Field()
duration: base.Integer = fields.Field()
mime_type: base.String = fields.Field()
file_size: base.Integer = fields.Field()
diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py
index b0162a7e..e24ad7b1 100644
--- a/aiogram/utils/callback_data.py
+++ b/aiogram/utils/callback_data.py
@@ -75,7 +75,7 @@ class CallbackData:
raise TypeError('Too many arguments were passed!')
callback_data = self.sep.join(data)
- if len(callback_data) > 64:
+ if len(callback_data.encode()) > 64:
raise ValueError('Resulted callback data is too long!')
return callback_data
diff --git a/aiogram/utils/deep_linking.py b/aiogram/utils/deep_linking.py
new file mode 100644
index 00000000..acb105da
--- /dev/null
+++ b/aiogram/utils/deep_linking.py
@@ -0,0 +1,101 @@
+"""
+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.
+
+You can read detailed description in the source:
+https://core.telegram.org/bots#deep-linking
+
+We have add some utils to get deep links more handy.
+
+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'
+
+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'
+
+"""
+
+
+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
+
+ :param payload: args passed with /start
+ :param encode: encode payload with base64url
+ :return: link
+ """
+ return await _create_link('start', payload, 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
+
+ :param payload: args passed with /start
+ :param encode: encode payload with base64url
+ :return: link
+ """
+ return await _create_link('startgroup', payload, encode)
+
+
+async def _create_link(link_type, payload: str, encode=False):
+ 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.')
+ raise ValueError(message)
+
+ return payload
+
+
+async def _get_bot_user():
+ """ Get current user of bot. """
+ from ..bot import Bot
+ bot = Bot.get_current()
+ return await bot.me
diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py
index cb22c506..83a9034c 100644
--- a/aiogram/utils/deprecated.py
+++ b/aiogram/utils/deprecated.py
@@ -99,35 +99,31 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve
"""
def decorator(func):
- if asyncio.iscoroutinefunction(func):
+ is_coroutine = asyncio.iscoroutinefunction(func)
+
+ def _handling(kwargs):
+ """
+ Returns updated version of kwargs.
+ """
+ routine_type = 'coroutine' if is_coroutine else 'function'
+ if old_name in kwargs:
+ warn_deprecated(f"In {routine_type} '{func.__name__}' argument '{old_name}' "
+ f"is renamed to '{new_name}' "
+ f"and will be removed in aiogram {until_version}",
+ stacklevel=stacklevel)
+ kwargs = kwargs.copy()
+ kwargs.update({new_name: kwargs.pop(old_name)})
+ return kwargs
+
+ if is_coroutine:
@functools.wraps(func)
async def wrapped(*args, **kwargs):
- if old_name in kwargs:
- warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' "
- f"is renamed to '{new_name}' "
- f"and will be removed in aiogram {until_version}",
- stacklevel=stacklevel)
- kwargs.update(
- {
- new_name: kwargs[old_name],
- }
- )
- kwargs.pop(old_name)
+ kwargs = _handling(kwargs)
return await func(*args, **kwargs)
else:
@functools.wraps(func)
def wrapped(*args, **kwargs):
- if old_name in kwargs:
- warn_deprecated(f"In function `{func.__name__}` argument `{old_name}` "
- f"is renamed to `{new_name}` "
- f"and will be removed in aiogram {until_version}",
- stacklevel=stacklevel)
- kwargs.update(
- {
- new_name: kwargs[old_name],
- }
- )
- kwargs.pop(old_name)
+ kwargs = _handling(kwargs)
return func(*args, **kwargs)
return wrapped
diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py
index f77fe257..cee2820a 100644
--- a/aiogram/utils/exceptions.py
+++ b/aiogram/utils/exceptions.py
@@ -65,6 +65,7 @@
- UnsupportedUrlProtocol
- CantParseEntities
- ResultIdDuplicate
+ - MethodIsNotAvailable
- ConflictError
- TerminatedByOtherGetUpdates
- CantGetUpdates
@@ -271,6 +272,10 @@ class PollQuestionLengthTooLong(PollSizeError):
match = "poll question length must not exceed 255"
+class PollCanBeRequestedInPrivateChatsOnly(PollError):
+ match = "Poll can be requested in private chats only"
+
+
class MessageWithPollNotFound(PollError, MessageError):
"""
Will be raised when you try to stop poll with message without poll
@@ -461,6 +466,10 @@ class BotDomainInvalid(BadRequest):
text = 'Invalid bot domain'
+class MethodIsNotAvailable(BadRequest):
+ match = "Method is available only for supergroups"
+
+
class NotFound(TelegramAPIError, _MatchErrorMixin):
__group = True
diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py
index 33f80684..fe3483f6 100644
--- a/aiogram/utils/executor.py
+++ b/aiogram/utils/executor.py
@@ -361,11 +361,11 @@ class Executor:
await callback(self.dispatcher)
async def _shutdown_polling(self, wait_closed=False):
- await self._shutdown()
-
for callback in self._on_shutdown_polling:
await callback(self.dispatcher)
+ await self._shutdown()
+
if wait_closed:
await self.dispatcher.wait_closed()
diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py
index 443a2ffe..735afe5d 100644
--- a/aiogram/utils/helper.py
+++ b/aiogram/utils/helper.py
@@ -13,6 +13,9 @@ Example:
>>> print(MyHelper.all())
<<< ['barItem', 'bazItem', 'fooItem', 'lorem']
"""
+from typing import List
+
+PROPS_KEYS_ATTR_NAME = '_props_keys'
class Helper:
@@ -191,3 +194,36 @@ class ItemsList(list):
return self
__iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add
+
+
+class OrderedHelperMeta(type):
+
+ def __new__(mcs, name, bases, namespace, **kwargs):
+ cls = super().__new__(mcs, name, bases, namespace)
+
+ props_keys = []
+
+ for prop_name in (name for name, prop in namespace.items() if isinstance(prop, (Item, ListItem))):
+ props_keys.append(prop_name)
+
+ setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys)
+
+ return cls
+
+
+class OrderedHelper(metaclass=OrderedHelperMeta):
+ mode = ''
+
+ @classmethod
+ def all(cls) -> List[str]:
+ """
+ Get all Items values
+ """
+ result = []
+ for name in getattr(cls, PROPS_KEYS_ATTR_NAME, []):
+ value = getattr(cls, name)
+ if isinstance(value, ItemsList):
+ result.append(value[0])
+ else:
+ result.append(value)
+ return result
diff --git a/aiogram/utils/json.py b/aiogram/utils/json.py
index b2305b88..56f122e4 100644
--- a/aiogram/utils/json.py
+++ b/aiogram/utils/json.py
@@ -21,13 +21,11 @@ for json_lib in (RAPIDJSON, UJSON):
if mode == RAPIDJSON:
def dumps(data):
- return json.dumps(data, ensure_ascii=False, number_mode=json.NM_NATIVE,
- datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC)
+ return json.dumps(data, ensure_ascii=False)
def loads(data):
- return json.loads(data, number_mode=json.NM_NATIVE,
- datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC)
+ return json.loads(data, number_mode=json.NM_NATIVE)
elif mode == UJSON:
def loads(data):
diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py
index 89a23d94..da27bc39 100644
--- a/aiogram/utils/markdown.py
+++ b/aiogram/utils/markdown.py
@@ -1,42 +1,24 @@
-LIST_MD_SYMBOLS = '*_`['
+from .text_decorations import html_decoration, markdown_decoration
+
+LIST_MD_SYMBOLS = "*_`["
MD_SYMBOLS = (
(LIST_MD_SYMBOLS[0], LIST_MD_SYMBOLS[0]),
(LIST_MD_SYMBOLS[1], LIST_MD_SYMBOLS[1]),
(LIST_MD_SYMBOLS[2], LIST_MD_SYMBOLS[2]),
- (LIST_MD_SYMBOLS[2] * 3 + '\n', '\n' + LIST_MD_SYMBOLS[2] * 3),
- ('', ''),
- ('', ''),
- ('', ''),
- ('', '
'),
+ (LIST_MD_SYMBOLS[2] * 3 + "\n", "\n" + LIST_MD_SYMBOLS[2] * 3),
+ ("", ""),
+ ("", ""),
+ ("", ""),
+ ("", "
"),
)
-HTML_QUOTES_MAP = {
- '<': '<',
- '>': '>',
- '&': '&',
- '"': '"'
-}
+HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """}
_HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS
-def _join(*content, sep=' '):
- return sep.join(map(str, content))
-
-
-def _escape(s, symbols=LIST_MD_SYMBOLS):
- for symbol in symbols:
- s = s.replace(symbol, '\\' + symbol)
- return s
-
-
-def _md(string, symbols=('', '')):
- start, end = symbols
- return start + string + end
-
-
-def quote_html(content):
+def quote_html(*content, sep=" ") -> str:
"""
Quote HTML symbols
@@ -44,16 +26,31 @@ def quote_html(content):
an HTML entity must be replaced with the corresponding HTML entities
(< with < > with > & with & and " with ").
- :param content: str
- :return: str
+ :param content:
+ :param sep:
+ :return:
"""
- new_content = ''
- for symbol in content:
- new_content += HTML_QUOTES_MAP[symbol] if symbol in _HQS else symbol
- return new_content
+ return html_decoration.quote(_join(*content, sep=sep))
-def text(*content, sep=' '):
+def escape_md(*content, sep=" ") -> str:
+ """
+ Escape markdown text
+
+ E.g. for usernames
+
+ :param content:
+ :param sep:
+ :return:
+ """
+ return markdown_decoration.quote(_join(*content, sep=sep))
+
+
+def _join(*content, sep=" "):
+ return sep.join(map(str, content))
+
+
+def text(*content, sep=" "):
"""
Join all elements with a separator
@@ -64,7 +61,7 @@ def text(*content, sep=' '):
return _join(*content, sep=sep)
-def bold(*content, sep=' '):
+def bold(*content, sep=" ") -> str:
"""
Make bold text (Markdown)
@@ -72,10 +69,12 @@ def bold(*content, sep=' '):
:param sep:
:return:
"""
- return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[0])
+ return markdown_decoration.bold(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
+ )
-def hbold(*content, sep=' '):
+def hbold(*content, sep=" ") -> str:
"""
Make bold text (HTML)
@@ -83,10 +82,12 @@ def hbold(*content, sep=' '):
:param sep:
:return:
"""
- return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[4])
+ return html_decoration.bold(
+ value=html_decoration.quote(_join(*content, sep=sep))
+ )
-def italic(*content, sep=' '):
+def italic(*content, sep=" ") -> str:
"""
Make italic text (Markdown)
@@ -94,10 +95,12 @@ def italic(*content, sep=' '):
:param sep:
:return:
"""
- return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[1])
+ return markdown_decoration.italic(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
+ )
-def hitalic(*content, sep=' '):
+def hitalic(*content, sep=" ") -> str:
"""
Make italic text (HTML)
@@ -105,10 +108,12 @@ def hitalic(*content, sep=' '):
:param sep:
:return:
"""
- return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[5])
+ return html_decoration.italic(
+ value=html_decoration.quote(_join(*content, sep=sep))
+ )
-def code(*content, sep=' '):
+def code(*content, sep=" ") -> str:
"""
Make mono-width text (Markdown)
@@ -116,10 +121,12 @@ def code(*content, sep=' '):
:param sep:
:return:
"""
- return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[2])
+ return markdown_decoration.code(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
+ )
-def hcode(*content, sep=' '):
+def hcode(*content, sep=" ") -> str:
"""
Make mono-width text (HTML)
@@ -127,10 +134,12 @@ def hcode(*content, sep=' '):
:param sep:
:return:
"""
- return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[6])
+ return html_decoration.code(
+ value=html_decoration.quote(_join(*content, sep=sep))
+ )
-def pre(*content, sep='\n'):
+def pre(*content, sep="\n") -> str:
"""
Make mono-width text block (Markdown)
@@ -138,10 +147,12 @@ def pre(*content, sep='\n'):
:param sep:
:return:
"""
- return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[3])
+ return markdown_decoration.pre(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
+ )
-def hpre(*content, sep='\n'):
+def hpre(*content, sep="\n") -> str:
"""
Make mono-width text block (HTML)
@@ -149,10 +160,64 @@ def hpre(*content, sep='\n'):
:param sep:
:return:
"""
- return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[7])
+ return html_decoration.pre(
+ value=html_decoration.quote(_join(*content, sep=sep))
+ )
-def link(title, url):
+def underline(*content, sep=" ") -> str:
+ """
+ Make underlined text (Markdown)
+
+ :param content:
+ :param sep:
+ :return:
+ """
+ return markdown_decoration.underline(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
+ )
+
+
+def hunderline(*content, sep=" ") -> str:
+ """
+ Make underlined text (HTML)
+
+ :param content:
+ :param sep:
+ :return:
+ """
+ return html_decoration.underline(
+ value=html_decoration.quote(_join(*content, sep=sep))
+ )
+
+
+def strikethrough(*content, sep=" ") -> str:
+ """
+ Make strikethrough text (Markdown)
+
+ :param content:
+ :param sep:
+ :return:
+ """
+ return markdown_decoration.strikethrough(
+ value=markdown_decoration.quote(_join(*content, sep=sep))
+ )
+
+
+def hstrikethrough(*content, sep=" ") -> str:
+ """
+ Make strikethrough text (HTML)
+
+ :param content:
+ :param sep:
+ :return:
+ """
+ return html_decoration.strikethrough(
+ value=html_decoration.quote(_join(*content, sep=sep))
+ )
+
+
+def link(title: str, url: str) -> str:
"""
Format URL (Markdown)
@@ -160,10 +225,10 @@ def link(title, url):
:param url:
:return:
"""
- return "[{0}]({1})".format(title, url)
+ return markdown_decoration.link(value=markdown_decoration.quote(title), link=url)
-def hlink(title, url):
+def hlink(title: str, url: str) -> str:
"""
Format URL (HTML)
@@ -171,23 +236,10 @@ def hlink(title, url):
:param url:
:return:
"""
- return '{1}'.format(url, quote_html(title))
+ return html_decoration.link(value=html_decoration.quote(title), link=url)
-def escape_md(*content, sep=' '):
- """
- Escape markdown text
-
- E.g. for usernames
-
- :param content:
- :param sep:
- :return:
- """
- return _escape(_join(*content, sep=sep))
-
-
-def hide_link(url):
+def hide_link(url: str) -> str:
"""
Hide URL (HTML only)
Can be used for adding an image to a text message
diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py
index e6857263..90ef4edb 100644
--- a/aiogram/utils/mixins.py
+++ b/aiogram/utils/mixins.py
@@ -22,6 +22,9 @@ class DataMixin:
def __delitem__(self, key):
del self.data[key]
+ def __contains__(self, key):
+ return key in self.data
+
def get(self, key, default=None):
return self.data.get(key, default)
diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py
new file mode 100644
index 00000000..3d22f637
--- /dev/null
+++ b/aiogram/utils/text_decorations.py
@@ -0,0 +1,197 @@
+from __future__ import annotations
+
+import html
+import re
+from abc import ABC, abstractmethod
+from typing import TYPE_CHECKING, Generator, List, Optional, Pattern, cast
+
+if TYPE_CHECKING: # pragma: no cover
+ from aiogram.types import MessageEntity
+
+__all__ = (
+ "TextDecoration",
+ "HtmlDecoration",
+ "MarkdownDecoration",
+ "html_decoration",
+ "markdown_decoration",
+)
+
+
+class TextDecoration(ABC):
+ def apply_entity(self, entity: MessageEntity, text: str) -> str:
+ """
+ Apply single entity to text
+
+ :param entity:
+ :param text:
+ :return:
+ """
+ if entity.type in {"bot_command", "url", "mention", "phone_number"}:
+ # This entities should not be changed
+ return text
+ if entity.type in {"bold", "italic", "code", "underline", "strikethrough"}:
+ return cast(str, getattr(self, entity.type)(value=text))
+ if entity.type == "pre":
+ return (
+ self.pre_language(value=text, language=entity.language)
+ if entity.language
+ else self.pre(value=text)
+ )
+ if entity.type == "text_mention":
+ from aiogram.types import User
+
+ user = cast(User, entity.user)
+ return self.link(value=text, link=f"tg://user?id={user.id}")
+ if entity.type == "text_link":
+ return self.link(value=text, link=cast(str, entity.url))
+
+ return self.quote(text)
+
+ def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str:
+ """
+ Unparse message entities
+
+ :param text: raw text
+ :param entities: Array of MessageEntities
+ :return:
+ """
+ result = "".join(
+ self._unparse_entities(
+ text, sorted(entities, key=lambda item: item.offset) if entities else []
+ )
+ )
+ return result
+
+ def _unparse_entities(
+ self,
+ text: str,
+ entities: List[MessageEntity],
+ offset: Optional[int] = None,
+ length: Optional[int] = None,
+ ) -> Generator[str, None, None]:
+ if offset is None:
+ offset = 0
+ length = length or len(text)
+
+ for index, entity in enumerate(entities):
+ if entity.offset < offset:
+ continue
+ if entity.offset > offset:
+ yield self.quote(text[offset : entity.offset])
+ start = entity.offset
+ offset = entity.offset + entity.length
+
+ sub_entities = list(
+ filter(lambda e: e.offset < (offset or 0), entities[index + 1 :])
+ )
+ yield self.apply_entity(
+ entity,
+ "".join(
+ self._unparse_entities(
+ text, sub_entities, offset=start, length=offset
+ )
+ ),
+ )
+
+ if offset < length:
+ yield self.quote(text[offset:length])
+
+ @abstractmethod
+ def link(self, value: str, link: str) -> str: # pragma: no cover
+ pass
+
+ @abstractmethod
+ def bold(self, value: str) -> str: # pragma: no cover
+ pass
+
+ @abstractmethod
+ def italic(self, value: str) -> str: # pragma: no cover
+ pass
+
+ @abstractmethod
+ def code(self, value: str) -> str: # pragma: no cover
+ pass
+
+ @abstractmethod
+ def pre(self, value: str) -> str: # pragma: no cover
+ pass
+
+ @abstractmethod
+ def pre_language(self, value: str, language: str) -> str: # pragma: no cover
+ pass
+
+ @abstractmethod
+ def underline(self, value: str) -> str: # pragma: no cover
+ pass
+
+ @abstractmethod
+ def strikethrough(self, value: str) -> str: # pragma: no cover
+ pass
+
+ @abstractmethod
+ def quote(self, value: str) -> str: # pragma: no cover
+ pass
+
+
+class HtmlDecoration(TextDecoration):
+ def link(self, value: str, link: str) -> str:
+ return f'{value}'
+
+ def bold(self, value: str) -> str:
+ return f"{value}"
+
+ def italic(self, value: str) -> str:
+ return f"{value}"
+
+ def code(self, value: str) -> str:
+ return f"{value}"
+
+ def pre(self, value: str) -> str:
+ return f"{value}"
+
+ def pre_language(self, value: str, language: str) -> str:
+ return f'{value}
'
+
+ def underline(self, value: str) -> str:
+ return f"{value}"
+
+ def strikethrough(self, value: str) -> str:
+ return f"{value}"
+
+ def quote(self, value: str) -> str:
+ return html.escape(value)
+
+
+class MarkdownDecoration(TextDecoration):
+ MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
+
+ def link(self, value: str, link: str) -> str:
+ return f"[{value}]({link})"
+
+ def bold(self, value: str) -> str:
+ return f"*{value}*"
+
+ def italic(self, value: str) -> str:
+ return f"_{value}_\r"
+
+ def code(self, value: str) -> str:
+ return f"`{value}`"
+
+ def pre(self, value: str) -> str:
+ return f"```{value}```"
+
+ def pre_language(self, value: str, language: str) -> str:
+ return f"```{language}\n{value}\n```"
+
+ def underline(self, value: str) -> str:
+ return f"__{value}__"
+
+ def strikethrough(self, value: str) -> str:
+ return f"~{value}~"
+
+ def quote(self, value: str) -> str:
+ return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value)
+
+
+html_decoration = HtmlDecoration()
+markdown_decoration = MarkdownDecoration()
diff --git a/dev_requirements.txt b/dev_requirements.txt
index 06bc3e9c..c0c2a39d 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -3,7 +3,7 @@
ujson>=1.35
python-rapidjson>=0.7.0
emoji>=0.5.2
-pytest>=4.4.1,<4.6
+pytest>=5.4
pytest-asyncio>=0.10.0
tox>=3.9.0
aresponses>=1.1.1
@@ -13,6 +13,6 @@ wheel>=0.31.1
sphinx>=2.0.1
sphinx-rtd-theme>=0.4.3
sphinxcontrib-programoutput>=0.14
-aiohttp-socks>=0.2.2
+aiohttp-socks>=0.3.4
rethinkdb>=2.4.1
coverage==4.5.3
diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst
index b174f1ef..3681dfcb 100644
--- a/docs/source/dispatcher/filters.rst
+++ b/docs/source/dispatcher/filters.rst
@@ -94,6 +94,12 @@ ContentTypeFilter
:members:
:show-inheritance:
+IsSenderContact
+---------------
+
+.. autoclass:: aiogram.dispatcher.filters.builtin.IsSenderContact
+ :members:
+ :show-inheritance:
StateFilter
-----------
@@ -135,6 +141,14 @@ IsReplyFilter
:show-inheritance:
+ForwardedMessageFilter
+-------------
+
+.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter
+ :members:
+ :show-inheritance:
+
+
Making own filters (Custom filters)
===================================
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 4fdf7a20..0ac6eccd 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -22,12 +22,12 @@ 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-4.4-blue.svg?style=flat-square&logo=telegram
+ .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram
:target: https://core.telegram.org/bots/api
:alt: Telegram Bot API
.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square
- :target: http://aiogram.readthedocs.io/en/latest/?badge=latest
+ :target: http://docs.aiogram.dev/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square
@@ -39,7 +39,7 @@ Welcome to aiogram's documentation!
:alt: MIT License
-**aiogram** is a pretty simple and fully asynchronous library for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler.
+**aiogram** is a pretty simple and fully asynchronous framework for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler.
Official aiogram resources
@@ -48,7 +48,7 @@ Official aiogram resources
- Community: `@aiogram `_
- Russian community: `@aiogram_ru `_
- Pip: `aiogram `_
-- Docs: `ReadTheDocs `_
+- Docs: `ReadTheDocs `_
- Source: `Github repo `_
- Issues/Bug tracker: `Github issues tracker `_
- Test bot: `@aiogram_bot `_
diff --git a/docs/source/install.rst b/docs/source/install.rst
index cd89dc54..e4717a42 100644
--- a/docs/source/install.rst
+++ b/docs/source/install.rst
@@ -7,25 +7,34 @@ Using PIP
$ pip install -U aiogram
+Using Pipenv
+------------
+ .. code-block:: bash
+
+ $ pipenv install aiogram
+
Using AUR
---------
-*aiogram* is also available in Arch User Repository, so you can install this library on any Arch-based distribution like ArchLinux, Antergos, Manjaro, etc. To do this, use your favorite AUR-helper and install `python-aiogram `_ package.
+*aiogram* is also available in Arch User Repository, so you can install this framework on any Arch-based distribution like ArchLinux, Antergos, Manjaro, etc. To do this, use your favorite AUR-helper and install the `python-aiogram `_ package.
From sources
------------
+
+ Development versions:
+
.. code-block:: bash
$ git clone https://github.com/aiogram/aiogram.git
$ cd aiogram
$ python setup.py install
- or if you want to install development version (maybe unstable):
+ Or if you want to install stable version (The same with version form PyPi):
.. code-block:: bash
$ git clone https://github.com/aiogram/aiogram.git
$ cd aiogram
- $ git checkout dev-2.x
+ $ git checkout master
$ python setup.py install
@@ -43,7 +52,7 @@ You can speedup your bots by following next instructions:
$ pip install uvloop
-- Use `ujson `_ instead of default json module.
+- Use `ujson `_ instead of the default json module.
*UltraJSON* is an ultra fast JSON encoder and decoder written in pure C with bindings for Python 2.5+ and 3.
@@ -53,4 +62,36 @@ You can speedup your bots by following next instructions:
$ pip install ujson
-In addition, you don't need do nothing, *aiogram* is automatically starts using that if is found in your environment.
+- Use aiohttp speedups
+
+ - Use `cchardet `_ instead of the chardet module.
+
+ *cChardet* is a high speed universal character encoding detector.
+
+ **Installation:**
+
+ .. code-block:: bash
+
+ $ pip install cchardet
+
+ - Use `aiodns `_ for speeding up DNS resolving.
+
+ *aiodns* provides a simple way for doing asynchronous DNS resolutions.
+
+ **Installation:**
+
+ .. code-block:: bash
+
+ $ pip install aiodns
+
+ - Installing speedups altogether.
+
+ The following will get you ``aiohttp`` along with ``cchardet``, ``aiodns`` and ``brotlipy`` in one bundle.
+
+ **Installation:**
+
+ .. code-block:: bash
+
+ $ pip install aiohttp[speedups]
+
+In addition, you don't need do anything, *aiogram* automatically starts using that if it is found in your environment.
diff --git a/docs/source/migration_1_to_2.rst b/docs/source/migration_1_to_2.rst
index 67684831..7016063a 100644
--- a/docs/source/migration_1_to_2.rst
+++ b/docs/source/migration_1_to_2.rst
@@ -73,7 +73,7 @@ Also you can bind your own filters for using as keyword arguments:
async def check(self, message: types.Message):
member = await bot.get_chat_member(message.chat.id, message.from_user.id)
- return member.is_admin()
+ return member.is_chat_admin()
dp.filters_factory.bind(MyFilter)
@@ -195,7 +195,7 @@ Example:
.. code-block:: python
- URL = 'https://aiogram.readthedocs.io/en/dev-2.x/_static/logo.png'
+ URL = 'https://docs.aiogram.dev/en/dev-2.x/_static/logo.png'
@dp.message_handler(commands=['image, img'])
diff --git a/docs/source/utils/deep_linking.rst b/docs/source/utils/deep_linking.rst
new file mode 100644
index 00000000..e00e0d20
--- /dev/null
+++ b/docs/source/utils/deep_linking.rst
@@ -0,0 +1,6 @@
+============
+Deep linking
+============
+
+.. automodule:: aiogram.utils.deep_linking
+ :members:
diff --git a/examples/echo_bot.py b/examples/echo_bot.py
index 00046f3a..0055d155 100644
--- a/examples/echo_bot.py
+++ b/examples/echo_bot.py
@@ -46,7 +46,7 @@ async def echo(message: types.Message):
# old style:
# await bot.send_message(message.chat.id, message.text)
- await message.reply(message.text, reply=False)
+ await message.answer(message.text)
if __name__ == '__main__':
diff --git a/examples/i18n_example.py b/examples/i18n_example.py
index 3bb624bd..b626d048 100644
--- a/examples/i18n_example.py
+++ b/examples/i18n_example.py
@@ -62,8 +62,7 @@ async def cmd_start(message: types.Message):
@dp.message_handler(commands='lang')
async def cmd_lang(message: types.Message, locale):
- # For setting custom lang you have to modify i18n middleware, like this:
- # https://github.com/aiogram/EventsTrackerBot/blob/master/modules/base/middlewares.py
+ # For setting custom lang you have to modify i18n middleware
await message.reply(_('Your current language: {language}').format(language=locale))
# If you care about pluralization, here's small handler
@@ -84,7 +83,6 @@ def get_likes() -> int:
def increase_likes() -> int:
LIKES_STORAGE['count'] += 1
return get_likes()
-#
@dp.message_handler(commands='like')
diff --git a/examples/id_filter_example.py b/examples/id_filter_example.py
index 343253e3..bb9bba6a 100644
--- a/examples/id_filter_example.py
+++ b/examples/id_filter_example.py
@@ -24,12 +24,12 @@ async def handler2(msg: types.Message):
@dp.message_handler(user_id=user_id_required, chat_id=chat_id_required)
async def handler3(msg: types.Message):
- await msg.reply("Hello from user= & chat_id=", reply=False)
+ await msg.answer("Hello from user= & chat_id=")
@dp.message_handler(user_id=[user_id_required, 42]) # TODO: You can add any number of ids here
async def handler4(msg: types.Message):
- await msg.reply("Checked user_id with list!", reply=False)
+ await msg.answer("Checked user_id with list!")
if __name__ == '__main__':
diff --git a/requirements.txt b/requirements.txt
index 37d328b3..7f7dc1ac 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-aiohttp>=3.5.4
+aiohttp>=3.5.4,<4.0.0
Babel>=2.6.0
certifi>=2019.3.9
diff --git a/setup.py b/setup.py
index b5c9e61c..b21b4e57 100755
--- a/setup.py
+++ b/setup.py
@@ -5,11 +5,6 @@ import sys
from setuptools import find_packages, setup
-try:
- from pip.req import parse_requirements
-except ImportError: # pip >= 10.0.0
- from pip._internal.req import parse_requirements
-
WORK_DIR = pathlib.Path(__file__).parent
# Check python version
@@ -42,22 +37,6 @@ def get_description():
return f.read()
-def get_requirements(filename=None):
- """
- Read requirements from 'requirements txt'
-
- :return: requirements
- :rtype: list
- """
- if filename is None:
- filename = 'requirements.txt'
-
- file = WORK_DIR / filename
-
- install_reqs = parse_requirements(str(file), session='hack')
- return [str(ir.req) for ir in install_reqs]
-
-
setup(
name='aiogram',
version=get_version(),
@@ -67,7 +46,7 @@ setup(
author='Alex Root Junior',
requires_python='>=3.7',
author_email='jroot.junior@gmail.com',
- description='Is a pretty simple and fully asynchronous library for Telegram Bot API',
+ description='Is a pretty simple and fully asynchronous framework for Telegram Bot API',
long_description=get_description(),
classifiers=[
'Development Status :: 5 - Production/Stable',
@@ -77,9 +56,22 @@ setup(
'Intended Audience :: System Administrators',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
'Topic :: Software Development :: Libraries :: Application Frameworks',
],
- install_requires=get_requirements(),
- package_data={'': ['requirements.txt']},
+ install_requires=[
+ 'aiohttp>=3.5.4,<4.0.0',
+ 'Babel>=2.6.0',
+ 'certifi>=2019.3.9',
+ ],
+ extras_require={
+ 'proxy': [
+ 'aiohttp-socks>=0.3.4,<0.4.0',
+ ],
+ 'fast': [
+ 'uvloop>=0.14.0,<0.15.0',
+ 'ujson>=1.35',
+ ],
+ },
include_package_data=False,
)
diff --git a/tests/__init__.py b/tests/__init__.py
index 262c9395..920d5663 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -2,13 +2,14 @@ import aresponses
from aiogram import Bot
-TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
+BOT_ID = 123456789
+TOKEN = f'{BOT_ID}:AABBCCDDEEFFaabbccddeeff-1234567890'
class FakeTelegram(aresponses.ResponsesMockServer):
- def __init__(self, message_dict, bot=None, **kwargs):
+ def __init__(self, message_data, bot=None, **kwargs):
super().__init__(**kwargs)
- self._body, self._headers = self.parse_data(message_dict)
+ self._body, self._headers = self.parse_data(message_data)
if isinstance(bot, Bot):
Bot.set_current(bot)
@@ -24,10 +25,11 @@ class FakeTelegram(aresponses.ResponsesMockServer):
await super().__aexit__(exc_type, exc_val, exc_tb)
@staticmethod
- def parse_data(message_dict):
+ def parse_data(message_data):
import json
+ from aiogram.utils.payload import _normalize
- _body = '{"ok":true,"result":' + json.dumps(message_dict) + '}'
+ _body = '{"ok":true,"result":' + json.dumps(_normalize(message_data)) + '}'
_headers = {'Server': 'nginx/1.12.2',
'Date': 'Tue, 03 Apr 2018 16:59:54 GMT',
'Content-Type': 'application/json',
diff --git a/tests/conftest.py b/tests/conftest.py
index fe936e18..03c8dbe4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1 +1,35 @@
-# pytest_plugins = "pytest_asyncio.plugin"
+import pytest
+from _pytest.config import UsageError
+import aioredis.util
+
+
+def pytest_addoption(parser):
+ parser.addoption("--redis", default=None,
+ help="run tests which require redis connection")
+
+
+def pytest_configure(config):
+ config.addinivalue_line("markers", "redis: marked tests require redis connection to run")
+
+
+def pytest_collection_modifyitems(config, items):
+ redis_uri = config.getoption("--redis")
+ if redis_uri is None:
+ skip_redis = pytest.mark.skip(reason="need --redis option with redis URI to run")
+ for item in items:
+ if "redis" in item.keywords:
+ item.add_marker(skip_redis)
+ return
+ try:
+ address, options = aioredis.util.parse_url(redis_uri)
+ assert isinstance(address, tuple), "Only redis and rediss schemas are supported, eg redis://foo."
+ except AssertionError as e:
+ raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}")
+
+
+@pytest.fixture(scope='session')
+def redis_options(request):
+ redis_uri = request.config.getoption("--redis")
+ (host, port), options = aioredis.util.parse_url(redis_uri)
+ options.update({'host': host, 'port': port})
+ return options
diff --git a/tests/contrib/fsm_storage/test_redis.py b/tests/contrib/fsm_storage/test_redis.py
new file mode 100644
index 00000000..527c905e
--- /dev/null
+++ b/tests/contrib/fsm_storage/test_redis.py
@@ -0,0 +1,33 @@
+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
diff --git a/tests/test_bot.py b/tests/test_bot.py
index 3e48ea57..cf1c3c3b 100644
--- a/tests/test_bot.py
+++ b/tests/test_bot.py
@@ -1,127 +1,92 @@
-import aresponses
import pytest
from aiogram import Bot, types
+from . import FakeTelegram, TOKEN, BOT_ID
-TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
+pytestmark = pytest.mark.asyncio
-class FakeTelegram(aresponses.ResponsesMockServer):
- def __init__(self, message_dict, **kwargs):
- super().__init__(**kwargs)
- self._body, self._headers = self.parse_data(message_dict)
-
- async def __aenter__(self):
- await super().__aenter__()
- _response = self.Response(text=self._body, headers=self._headers, status=200, reason='OK')
- self.add(self.ANY, response=_response)
-
- @staticmethod
- def parse_data(message_dict):
- import json
-
- _body = '{"ok":true,"result":' + json.dumps(message_dict) + '}'
- _headers = {'Server': 'nginx/1.12.2',
- 'Date': 'Tue, 03 Apr 2018 16:59:54 GMT',
- 'Content-Type': 'application/json',
- 'Content-Length': str(len(_body)),
- 'Connection': 'keep-alive',
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
- 'Access-Control-Expose-Headers': 'Content-Length,Content-Type,Date,Server,Connection',
- 'Strict-Transport-Security': 'max-age=31536000; includeSubdomains'}
- return _body, _headers
-
-
-@pytest.yield_fixture()
-@pytest.mark.asyncio
-async def bot(event_loop):
+@pytest.yield_fixture(name='bot')
+async def bot_fixture(event_loop):
""" Bot fixture """
_bot = Bot(TOKEN, loop=event_loop, parse_mode=types.ParseMode.MARKDOWN)
yield _bot
await _bot.close()
-@pytest.mark.asyncio
async def test_get_me(bot: Bot, event_loop):
""" getMe method test """
from .types.dataset import USER
user = types.User(**USER)
- async with FakeTelegram(message_dict=USER, loop=event_loop):
+ async with FakeTelegram(message_data=USER, loop=event_loop):
result = await bot.me
assert result == user
-@pytest.mark.asyncio
async def test_send_message(bot: Bot, event_loop):
""" sendMessage method test """
from .types.dataset import MESSAGE
msg = types.Message(**MESSAGE)
- async with FakeTelegram(message_dict=MESSAGE, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE, loop=event_loop):
result = await bot.send_message(chat_id=msg.chat.id, text=msg.text)
assert result == msg
-@pytest.mark.asyncio
async def test_forward_message(bot: Bot, event_loop):
""" forwardMessage method test """
from .types.dataset import FORWARDED_MESSAGE
msg = types.Message(**FORWARDED_MESSAGE)
- async with FakeTelegram(message_dict=FORWARDED_MESSAGE, loop=event_loop):
+ async with FakeTelegram(message_data=FORWARDED_MESSAGE, loop=event_loop):
result = await bot.forward_message(chat_id=msg.chat.id, from_chat_id=msg.forward_from_chat.id,
message_id=msg.forward_from_message_id)
assert result == msg
-@pytest.mark.asyncio
async def test_send_photo(bot: Bot, event_loop):
""" sendPhoto method test with file_id """
from .types.dataset import MESSAGE_WITH_PHOTO, PHOTO
msg = types.Message(**MESSAGE_WITH_PHOTO)
photo = types.PhotoSize(**PHOTO)
- async with FakeTelegram(message_dict=MESSAGE_WITH_PHOTO, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_PHOTO, loop=event_loop):
result = await bot.send_photo(msg.chat.id, photo=photo.file_id, caption=msg.caption,
parse_mode=types.ParseMode.HTML, disable_notification=False)
assert result == msg
-@pytest.mark.asyncio
async def test_send_audio(bot: Bot, event_loop):
""" sendAudio method test with file_id """
from .types.dataset import MESSAGE_WITH_AUDIO
msg = types.Message(**MESSAGE_WITH_AUDIO)
- async with FakeTelegram(message_dict=MESSAGE_WITH_AUDIO, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_AUDIO, loop=event_loop):
result = await bot.send_audio(chat_id=msg.chat.id, audio=msg.audio.file_id, caption=msg.caption,
parse_mode=types.ParseMode.HTML, duration=msg.audio.duration,
performer=msg.audio.performer, title=msg.audio.title, disable_notification=False)
assert result == msg
-@pytest.mark.asyncio
async def test_send_document(bot: Bot, event_loop):
""" sendDocument method test with file_id """
from .types.dataset import MESSAGE_WITH_DOCUMENT
msg = types.Message(**MESSAGE_WITH_DOCUMENT)
- async with FakeTelegram(message_dict=MESSAGE_WITH_DOCUMENT, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_DOCUMENT, loop=event_loop):
result = await bot.send_document(chat_id=msg.chat.id, document=msg.document.file_id, caption=msg.caption,
parse_mode=types.ParseMode.HTML, disable_notification=False)
assert result == msg
-@pytest.mark.asyncio
async def test_send_video(bot: Bot, event_loop):
""" sendVideo method test with file_id """
from .types.dataset import MESSAGE_WITH_VIDEO, VIDEO
msg = types.Message(**MESSAGE_WITH_VIDEO)
video = types.Video(**VIDEO)
- async with FakeTelegram(message_dict=MESSAGE_WITH_VIDEO, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_VIDEO, loop=event_loop):
result = await bot.send_video(chat_id=msg.chat.id, video=video.file_id, duration=video.duration,
width=video.width, height=video.height, caption=msg.caption,
parse_mode=types.ParseMode.HTML, supports_streaming=True,
@@ -129,35 +94,32 @@ async def test_send_video(bot: Bot, event_loop):
assert result == msg
-@pytest.mark.asyncio
async def test_send_voice(bot: Bot, event_loop):
""" sendVoice method test with file_id """
from .types.dataset import MESSAGE_WITH_VOICE, VOICE
msg = types.Message(**MESSAGE_WITH_VOICE)
voice = types.Voice(**VOICE)
- async with FakeTelegram(message_dict=MESSAGE_WITH_VOICE, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_VOICE, loop=event_loop):
result = await bot.send_voice(chat_id=msg.chat.id, voice=voice.file_id, caption=msg.caption,
parse_mode=types.ParseMode.HTML, duration=voice.duration,
disable_notification=False)
assert result == msg
-@pytest.mark.asyncio
async def test_send_video_note(bot: Bot, event_loop):
""" sendVideoNote method test with file_id """
from .types.dataset import MESSAGE_WITH_VIDEO_NOTE, VIDEO_NOTE
msg = types.Message(**MESSAGE_WITH_VIDEO_NOTE)
video_note = types.VideoNote(**VIDEO_NOTE)
- async with FakeTelegram(message_dict=MESSAGE_WITH_VIDEO_NOTE, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_VIDEO_NOTE, loop=event_loop):
result = await bot.send_video_note(chat_id=msg.chat.id, video_note=video_note.file_id,
duration=video_note.duration, length=video_note.length,
disable_notification=False)
assert result == msg
-@pytest.mark.asyncio
async def test_send_media_group(bot: Bot, event_loop):
""" sendMediaGroup method test with file_id """
from .types.dataset import MESSAGE_WITH_MEDIA_GROUP, PHOTO
@@ -165,26 +127,24 @@ async def test_send_media_group(bot: Bot, event_loop):
photo = types.PhotoSize(**PHOTO)
media = [types.InputMediaPhoto(media=photo.file_id), types.InputMediaPhoto(media=photo.file_id)]
- async with FakeTelegram(message_dict=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP], loop=event_loop):
+ async with FakeTelegram(message_data=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP], loop=event_loop):
result = await bot.send_media_group(msg.chat.id, media=media, disable_notification=False)
assert len(result) == len(media)
assert result.pop().media_group_id
-@pytest.mark.asyncio
async def test_send_location(bot: Bot, event_loop):
""" sendLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION
msg = types.Message(**MESSAGE_WITH_LOCATION)
location = types.Location(**LOCATION)
- async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop):
result = await bot.send_location(msg.chat.id, latitude=location.latitude, longitude=location.longitude,
live_period=10, disable_notification=False)
assert result == msg
-@pytest.mark.asyncio
async def test_edit_message_live_location_by_bot(bot: Bot, event_loop):
""" editMessageLiveLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION
@@ -192,13 +152,12 @@ async def test_edit_message_live_location_by_bot(bot: Bot, event_loop):
location = types.Location(**LOCATION)
# editing bot message
- async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop):
result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id,
latitude=location.latitude, longitude=location.longitude)
assert result == msg
-@pytest.mark.asyncio
async def test_edit_message_live_location_by_user(bot: Bot, event_loop):
""" editMessageLiveLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION
@@ -206,38 +165,35 @@ async def test_edit_message_live_location_by_user(bot: Bot, event_loop):
location = types.Location(**LOCATION)
# editing user's message
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id,
latitude=location.latitude, longitude=location.longitude)
assert isinstance(result, bool) and result is True
-@pytest.mark.asyncio
async def test_stop_message_live_location_by_bot(bot: Bot, event_loop):
""" stopMessageLiveLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION
msg = types.Message(**MESSAGE_WITH_LOCATION)
# stopping bot message
- async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop):
result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id)
assert result == msg
-@pytest.mark.asyncio
async def test_stop_message_live_location_by_user(bot: Bot, event_loop):
""" stopMessageLiveLocation method test """
from .types.dataset import MESSAGE_WITH_LOCATION
msg = types.Message(**MESSAGE_WITH_LOCATION)
# stopping user's message
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_send_venue(bot: Bot, event_loop):
""" sendVenue method test """
from .types.dataset import MESSAGE_WITH_VENUE, VENUE, LOCATION
@@ -245,94 +201,97 @@ async def test_send_venue(bot: Bot, event_loop):
location = types.Location(**LOCATION)
venue = types.Venue(**VENUE)
- async with FakeTelegram(message_dict=MESSAGE_WITH_VENUE, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_VENUE, loop=event_loop):
result = await bot.send_venue(msg.chat.id, latitude=location.latitude, longitude=location.longitude,
title=venue.title, address=venue.address, foursquare_id=venue.foursquare_id,
disable_notification=False)
assert result == msg
-@pytest.mark.asyncio
async def test_send_contact(bot: Bot, event_loop):
""" sendContact method test """
from .types.dataset import MESSAGE_WITH_CONTACT, CONTACT
msg = types.Message(**MESSAGE_WITH_CONTACT)
contact = types.Contact(**CONTACT)
- async with FakeTelegram(message_dict=MESSAGE_WITH_CONTACT, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE_WITH_CONTACT, loop=event_loop):
result = await bot.send_contact(msg.chat.id, phone_number=contact.phone_number, first_name=contact.first_name,
last_name=contact.last_name, disable_notification=False)
assert result == msg
-@pytest.mark.asyncio
+async def test_send_dice(bot: Bot, event_loop):
+ """ sendDice method test """
+ from .types.dataset import MESSAGE_WITH_DICE
+ msg = types.Message(**MESSAGE_WITH_DICE)
+
+ async with FakeTelegram(message_data=MESSAGE_WITH_DICE, loop=event_loop):
+ result = await bot.send_dice(msg.chat.id, disable_notification=False)
+ assert result == msg
+
+
async def test_send_chat_action(bot: Bot, event_loop):
""" sendChatAction method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.send_chat_action(chat_id=chat.id, action=types.ChatActions.TYPING)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_get_user_profile_photo(bot: Bot, event_loop):
""" getUserProfilePhotos method test """
from .types.dataset import USER_PROFILE_PHOTOS, USER
user = types.User(**USER)
- async with FakeTelegram(message_dict=USER_PROFILE_PHOTOS, loop=event_loop):
+ async with FakeTelegram(message_data=USER_PROFILE_PHOTOS, loop=event_loop):
result = await bot.get_user_profile_photos(user_id=user.id, offset=1, limit=1)
assert isinstance(result, types.UserProfilePhotos)
-@pytest.mark.asyncio
async def test_get_file(bot: Bot, event_loop):
""" getFile method test """
from .types.dataset import FILE
file = types.File(**FILE)
- async with FakeTelegram(message_dict=FILE, loop=event_loop):
+ async with FakeTelegram(message_data=FILE, loop=event_loop):
result = await bot.get_file(file_id=file.file_id)
assert isinstance(result, types.File)
-@pytest.mark.asyncio
async def test_kick_chat_member(bot: Bot, event_loop):
""" kickChatMember method test """
from .types.dataset import USER, CHAT
user = types.User(**USER)
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.kick_chat_member(chat_id=chat.id, user_id=user.id, until_date=123)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_unban_chat_member(bot: Bot, event_loop):
""" unbanChatMember method test """
from .types.dataset import USER, CHAT
user = types.User(**USER)
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.unban_chat_member(chat_id=chat.id, user_id=user.id)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_restrict_chat_member(bot: Bot, event_loop):
""" restrictChatMember method test """
from .types.dataset import USER, CHAT
user = types.User(**USER)
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.restrict_chat_member(
chat_id=chat.id,
user_id=user.id,
@@ -346,14 +305,13 @@ async def test_restrict_chat_member(bot: Bot, event_loop):
assert result is True
-@pytest.mark.asyncio
async def test_promote_chat_member(bot: Bot, event_loop):
""" promoteChatMember method test """
from .types.dataset import USER, CHAT
user = types.User(**USER)
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.promote_chat_member(chat_id=chat.id, user_id=user.id, can_change_info=True,
can_delete_messages=True, can_edit_messages=True,
can_invite_users=True, can_pin_messages=True, can_post_messages=True,
@@ -362,193 +320,214 @@ async def test_promote_chat_member(bot: Bot, event_loop):
assert result is True
-@pytest.mark.asyncio
async def test_export_chat_invite_link(bot: Bot, event_loop):
""" exportChatInviteLink method test """
from .types.dataset import CHAT, INVITE_LINK
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=INVITE_LINK, loop=event_loop):
+ async with FakeTelegram(message_data=INVITE_LINK, loop=event_loop):
result = await bot.export_chat_invite_link(chat_id=chat.id)
assert result == INVITE_LINK
-@pytest.mark.asyncio
async def test_delete_chat_photo(bot: Bot, event_loop):
""" deleteChatPhoto method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.delete_chat_photo(chat_id=chat.id)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_set_chat_title(bot: Bot, event_loop):
""" setChatTitle method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.set_chat_title(chat_id=chat.id, title='Test title')
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_set_chat_description(bot: Bot, event_loop):
""" setChatDescription method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.set_chat_description(chat_id=chat.id, description='Test description')
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_pin_chat_message(bot: Bot, event_loop):
""" pinChatMessage method test """
from .types.dataset import MESSAGE
message = types.Message(**MESSAGE)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.pin_chat_message(chat_id=message.chat.id, message_id=message.message_id,
disable_notification=False)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_unpin_chat_message(bot: Bot, event_loop):
""" unpinChatMessage method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.unpin_chat_message(chat_id=chat.id)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_leave_chat(bot: Bot, event_loop):
""" leaveChat method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.leave_chat(chat_id=chat.id)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_get_chat(bot: Bot, event_loop):
""" getChat method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=CHAT, loop=event_loop):
+ async with FakeTelegram(message_data=CHAT, loop=event_loop):
result = await bot.get_chat(chat_id=chat.id)
assert result == chat
-@pytest.mark.asyncio
async def test_get_chat_administrators(bot: Bot, event_loop):
""" getChatAdministrators method test """
from .types.dataset import CHAT, CHAT_MEMBER
chat = types.Chat(**CHAT)
member = types.ChatMember(**CHAT_MEMBER)
- async with FakeTelegram(message_dict=[CHAT_MEMBER, CHAT_MEMBER], loop=event_loop):
+ async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER], loop=event_loop):
result = await bot.get_chat_administrators(chat_id=chat.id)
assert result[0] == member
assert len(result) == 2
-@pytest.mark.asyncio
async def test_get_chat_members_count(bot: Bot, event_loop):
""" getChatMembersCount method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
count = 5
- async with FakeTelegram(message_dict=count, loop=event_loop):
+ async with FakeTelegram(message_data=count, loop=event_loop):
result = await bot.get_chat_members_count(chat_id=chat.id)
assert result == count
-@pytest.mark.asyncio
async def test_get_chat_member(bot: Bot, event_loop):
""" getChatMember method test """
from .types.dataset import CHAT, CHAT_MEMBER
chat = types.Chat(**CHAT)
member = types.ChatMember(**CHAT_MEMBER)
- async with FakeTelegram(message_dict=CHAT_MEMBER, loop=event_loop):
+ async with FakeTelegram(message_data=CHAT_MEMBER, loop=event_loop):
result = await bot.get_chat_member(chat_id=chat.id, user_id=member.user.id)
assert isinstance(result, types.ChatMember)
assert result == member
-@pytest.mark.asyncio
async def test_set_chat_sticker_set(bot: Bot, event_loop):
""" setChatStickerSet method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.set_chat_sticker_set(chat_id=chat.id, sticker_set_name='aiogram_stickers')
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_delete_chat_sticker_set(bot: Bot, event_loop):
""" setChatStickerSet method test """
from .types.dataset import CHAT
chat = types.Chat(**CHAT)
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.delete_chat_sticker_set(chat_id=chat.id)
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
async def test_answer_callback_query(bot: Bot, event_loop):
""" answerCallbackQuery method test """
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.answer_callback_query(callback_query_id='QuERyId', text='Test Answer')
assert isinstance(result, bool)
assert result is True
-@pytest.mark.asyncio
+async def test_set_my_commands(bot: Bot, event_loop):
+ """ setMyCommands method test """
+ from .types.dataset import BOT_COMMAND
+
+ async with FakeTelegram(message_data=True, loop=event_loop):
+ commands = [types.BotCommand(**BOT_COMMAND), types.BotCommand(**BOT_COMMAND)]
+ result = await bot.set_my_commands(commands)
+ assert isinstance(result, bool)
+ assert result is True
+
+
+async def test_get_my_commands(bot: Bot, event_loop):
+ """ getMyCommands method test """
+ from .types.dataset import BOT_COMMAND
+ command = types.BotCommand(**BOT_COMMAND)
+ commands = [command, command]
+ async with FakeTelegram(message_data=commands, loop=event_loop):
+ result = await bot.get_my_commands()
+ assert isinstance(result, list)
+ assert all([isinstance(command, types.BotCommand) for command in result])
+
+
async def test_edit_message_text_by_bot(bot: Bot, event_loop):
""" editMessageText method test """
from .types.dataset import EDITED_MESSAGE
msg = types.Message(**EDITED_MESSAGE)
# message by bot
- async with FakeTelegram(message_dict=EDITED_MESSAGE, loop=event_loop):
+ async with FakeTelegram(message_data=EDITED_MESSAGE, loop=event_loop):
result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id)
assert result == msg
-@pytest.mark.asyncio
async def test_edit_message_text_by_user(bot: Bot, event_loop):
""" editMessageText method test """
from .types.dataset import EDITED_MESSAGE
msg = types.Message(**EDITED_MESSAGE)
# message by user
- async with FakeTelegram(message_dict=True, loop=event_loop):
+ async with FakeTelegram(message_data=True, loop=event_loop):
result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id)
assert isinstance(result, bool)
assert result is True
+
+
+async def test_set_sticker_set_thumb(bot: Bot, event_loop):
+ """ setStickerSetThumb method test """
+
+ async with FakeTelegram(message_data=True, loop=event_loop):
+ result = await bot.set_sticker_set_thumb(name='test', user_id=123456789, thumb='file_id')
+ assert isinstance(result, bool)
+ assert result is True
+
+
+async def test_bot_id(bot: Bot):
+ """ Check getting id from token. """
+ bot = Bot(TOKEN)
+ assert bot.id == BOT_ID # BOT_ID is a correct id from TOKEN
diff --git a/tests/test_bot/test_api.py b/tests/test_bot/test_api.py
index c5193bcc..29418169 100644
--- a/tests/test_bot/test_api.py
+++ b/tests/test_bot/test_api.py
@@ -1,18 +1,32 @@
import pytest
-from aiogram.bot.api import check_token
+from aiogram.bot.api import check_token
from aiogram.utils.exceptions import ValidationError
-
VALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
-INVALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff 123456789' # Space in token and wrong length
+INVALID_TOKENS = [
+ '123456789:AABBCCDDEEFFaabbccddeeff 123456789', # space is exists
+ 'ABC:AABBCCDDEEFFaabbccddeeff123456789', # left part is not digit
+ ':AABBCCDDEEFFaabbccddeeff123456789', # there is no left part
+ '123456789:', # there is no right part
+ 'ABC AABBCCDDEEFFaabbccddeeff123456789', # there is no ':' separator
+ None, # is None
+ 12345678, # is digit
+ {}, # is dict
+ [], # is dict
+]
-class Test_check_token:
+@pytest.fixture(params=INVALID_TOKENS, name='invalid_token')
+def invalid_token_fixture(request):
+ return request.param
+
+
+class TestCheckToken:
def test_valid(self):
assert check_token(VALID_TOKEN) is True
- def test_invalid_token(self):
+ def test_invalid_token(self, invalid_token):
with pytest.raises(ValidationError):
- check_token(INVALID_TOKEN)
+ check_token(invalid_token)
diff --git a/tests/test_bot/test_session.py b/tests/test_bot/test_session.py
new file mode 100644
index 00000000..dec6379c
--- /dev/null
+++ b/tests/test_bot/test_session.py
@@ -0,0 +1,61 @@
+import aiohttp
+import aiohttp_socks
+import pytest
+
+from aiogram.bot.base import BaseBot
+
+try:
+ from asynctest import CoroutineMock, patch
+except ImportError:
+ from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore
+
+
+class TestAiohttpSession:
+ @pytest.mark.asyncio
+ async def test_create_bot(self):
+ bot = BaseBot(token="42:correct")
+
+ assert bot._session is None
+ assert isinstance(bot._connector_init, dict)
+ assert all(key in {"limit", "ssl", "loop"} for key in bot._connector_init)
+ assert isinstance(bot._connector_class, type)
+ assert issubclass(bot._connector_class, aiohttp.TCPConnector)
+
+ assert bot._session is None
+
+ assert isinstance(bot.session, aiohttp.ClientSession)
+ assert bot.session == bot._session
+
+ @pytest.mark.asyncio
+ async def test_create_proxy_bot(self):
+ socks_ver, host, port, username, password = (
+ "socks5", "124.90.90.90", 9999, "login", "password"
+ )
+
+ bot = BaseBot(
+ token="42:correct",
+ proxy=f"{socks_ver}://{host}:{port}/",
+ proxy_auth=aiohttp.BasicAuth(username, password, "encoding"),
+ )
+
+ assert bot._connector_class == aiohttp_socks.SocksConnector
+
+ assert isinstance(bot._connector_init, dict)
+
+ init_kwargs = bot._connector_init
+ assert init_kwargs["username"] == username
+ assert init_kwargs["password"] == password
+ assert init_kwargs["host"] == host
+ assert init_kwargs["port"] == port
+
+ @pytest.mark.asyncio
+ async def test_close_session(self):
+ bot = BaseBot(token="42:correct",)
+ aiohttp_client_0 = bot.session
+
+ with patch("aiohttp.ClientSession.close", new=CoroutineMock()) as mocked_close:
+ await aiohttp_client_0.close()
+ mocked_close.assert_called_once()
+
+ await aiohttp_client_0.close()
+ assert aiohttp_client_0 != bot.session # will create new session
diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py
index 86344cec..4cfce465 100644
--- a/tests/test_dispatcher/test_filters/test_builtin.py
+++ b/tests/test_dispatcher/test_filters/test_builtin.py
@@ -1,6 +1,15 @@
+from typing import Set
+from datetime import datetime
+
import pytest
-from aiogram.dispatcher.filters.builtin import Text
+from aiogram.dispatcher.filters.builtin import (
+ Text,
+ extract_chat_ids,
+ ChatIDArgumentType, ForwardedMessageFilter,
+)
+from aiogram.types import Message
+from tests.types.dataset import MESSAGE
class TestText:
@@ -16,3 +25,72 @@ class TestText:
config = {param: value}
res = Text.validate(config)
assert res == {key: value}
+
+
+@pytest.mark.parametrize(
+ ('chat_id', 'expected'),
+ (
+ pytest.param('-64856280', {-64856280,}, id='single negative int as string'),
+ pytest.param('64856280', {64856280,}, id='single positive int as string'),
+ pytest.param(-64856280, {-64856280,}, id='single negative int'),
+ pytest.param(64856280, {64856280,}, id='single positive negative int'),
+ pytest.param(
+ ['-64856280'], {-64856280,}, id='list of single negative int as string'
+ ),
+ pytest.param([-64856280], {-64856280,}, id='list of single negative int'),
+ pytest.param(
+ ['-64856280', '-64856280'],
+ {-64856280,},
+ id='list of two duplicated negative ints as strings',
+ ),
+ pytest.param(
+ ['-64856280', -64856280],
+ {-64856280,},
+ id='list of one negative int as string and one negative int',
+ ),
+ pytest.param(
+ [-64856280, -64856280],
+ {-64856280,},
+ id='list of two duplicated negative ints',
+ ),
+ pytest.param(
+ iter(['-64856280']),
+ {-64856280,},
+ id='iterator from a list of single negative int as string',
+ ),
+ pytest.param(
+ [10000000, 20000000, 30000000],
+ {10000000, 20000000, 30000000},
+ id='list of several positive ints',
+ ),
+ pytest.param(
+ [10000000, '20000000', -30000000],
+ {10000000, 20000000, -30000000},
+ id='list of positive int, positive int as string, negative int',
+ ),
+ ),
+)
+def test_extract_chat_ids(chat_id: ChatIDArgumentType, expected: Set[int]):
+ assert extract_chat_ids(chat_id) == expected
+
+
+class TestForwardedMessageFilter:
+ async def test_filter_forwarded_messages(self):
+ filter = ForwardedMessageFilter(is_forwarded=True)
+
+ forwarded_message = Message(forward_date=round(datetime(2020, 5, 21, 5, 1).timestamp()), **MESSAGE)
+
+ not_forwarded_message = Message(**MESSAGE)
+
+ assert await filter.check(forwarded_message)
+ assert not await filter.check(not_forwarded_message)
+
+ async def test_filter_not_forwarded_messages(self):
+ filter = ForwardedMessageFilter(is_forwarded=False)
+
+ forwarded_message = Message(forward_date=round(datetime(2020, 5, 21, 5, 1).timestamp()), **MESSAGE)
+
+ not_forwarded_message = Message(**MESSAGE)
+
+ assert await filter.check(not_forwarded_message)
+ assert not await filter.check(forwarded_message)
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 609db736..f29e1982 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -1,8 +1,14 @@
+import re
+from typing import Match
+
import pytest
-from aiogram.dispatcher.filters import Text
+from aiogram.dispatcher.filters import Text, CommandStart
from aiogram.types import Message, CallbackQuery, InlineQuery, Poll
+# enable asyncio mode
+pytestmark = pytest.mark.asyncio
+
def data_sample_1():
return [
@@ -22,15 +28,16 @@ def data_sample_1():
('EXample_string', 'not_example_string'),
]
+
class TestTextFilter:
- async def _run_check(self, check, test_text):
+ @staticmethod
+ async def _run_check(check, test_text):
assert await check(Message(text=test_text))
assert await check(CallbackQuery(data=test_text))
assert await check(InlineQuery(query=test_text))
assert await check(Poll(question=test_text))
- @pytest.mark.asyncio
@pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_prefix, test_text", data_sample_1())
async def test_startswith(self, test_prefix, test_text, ignore_case):
@@ -49,7 +56,6 @@ class TestTextFilter:
await self._run_check(check, test_text)
- @pytest.mark.asyncio
@pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_prefix_list, test_text", [
(['not_example', ''], ''),
@@ -83,7 +89,6 @@ class TestTextFilter:
await self._run_check(check, test_text)
- @pytest.mark.asyncio
@pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_postfix, test_text", data_sample_1())
async def test_endswith(self, test_postfix, test_text, ignore_case):
@@ -102,7 +107,6 @@ class TestTextFilter:
await self._run_check(check, test_text)
- @pytest.mark.asyncio
@pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_postfix_list, test_text", [
(['', 'not_example'], ''),
@@ -133,9 +137,9 @@ class TestTextFilter:
_test_text = test_text
return result is any(map(_test_text.endswith, _test_postfix_list))
+
await self._run_check(check, test_text)
- @pytest.mark.asyncio
@pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_string, test_text", [
('', ''),
@@ -169,7 +173,6 @@ class TestTextFilter:
await self._run_check(check, test_text)
- @pytest.mark.asyncio
@pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_filter_list, test_text", [
(['a', 'ab', 'abc'], 'A'),
@@ -193,7 +196,6 @@ class TestTextFilter:
await self._run_check(check, test_text)
- @pytest.mark.asyncio
@pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_filter_text, test_text", [
('', ''),
@@ -222,7 +224,6 @@ class TestTextFilter:
await self._run_check(check, test_text)
- @pytest.mark.asyncio
@pytest.mark.parametrize('ignore_case', (True, False))
@pytest.mark.parametrize("test_filter_list, test_text", [
(['new_string', ''], ''),
@@ -261,3 +262,50 @@ class TestTextFilter:
await check(CallbackQuery(data=test_text))
await check(InlineQuery(query=test_text))
await check(Poll(question=test_text))
+
+
+class TestCommandStart:
+ START = '/start'
+ GOOD = 'foo'
+ BAD = 'bar'
+ GOOD_PATTERN = re.compile(r'^f..$')
+ BAD_PATTERN = re.compile(r'^b..$')
+ ENCODED = 'Zm9v'
+
+ async def test_start_command_without_payload(self):
+ test_filter = CommandStart() # empty filter
+ message = Message(text=self.START)
+ result = await test_filter.check(message)
+ assert result
+
+ async def test_start_command_payload_is_matched(self):
+ test_filter = CommandStart(deep_link=self.GOOD)
+ message = Message(text=f'{self.START} {self.GOOD}')
+ result = await test_filter.check(message)
+ assert result == {'deep_link': self.GOOD}
+
+ async def test_start_command_payload_is_not_matched(self):
+ test_filter = CommandStart(deep_link=self.GOOD)
+ message = Message(text=f'{self.START} {self.BAD}')
+ result = await test_filter.check(message)
+ assert result is False
+
+ async def test_start_command_payload_pattern_is_matched(self):
+ test_filter = CommandStart(deep_link=self.GOOD_PATTERN)
+ message = Message(text=f'{self.START} {self.GOOD}')
+ result = await test_filter.check(message)
+ assert isinstance(result, dict)
+ match = result.get('deep_link')
+ assert isinstance(match, Match)
+
+ async def test_start_command_payload_pattern_is_not_matched(self):
+ test_filter = CommandStart(deep_link=self.BAD_PATTERN)
+ message = Message(text=f'{self.START} {self.GOOD}')
+ result = await test_filter.check(message)
+ assert result is False
+
+ async def test_start_command_payload_is_encoded(self):
+ test_filter = CommandStart(deep_link=self.GOOD, encoded=True)
+ message = Message(text=f'{self.START} {self.ENCODED}')
+ result = await test_filter.check(message)
+ assert result == {'deep_link': self.GOOD}
diff --git a/tests/test_message.py b/tests/test_message.py
index 996529f3..32168d57 100644
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -28,7 +28,7 @@ async def message(bot, event_loop):
from .types.dataset import MESSAGE
msg = types.Message(**MESSAGE)
- async with FakeTelegram(message_dict=MESSAGE, loop=event_loop):
+ async with FakeTelegram(message_data=MESSAGE, loop=event_loop):
_message = await bot.send_message(chat_id=msg.chat.id, text=msg.text)
yield _message
diff --git a/tests/test_utils/test_deep_linking.py b/tests/test_utils/test_deep_linking.py
new file mode 100644
index 00000000..a1d01e4e
--- /dev/null
+++ b/tests/test_utils/test_deep_linking.py
@@ -0,0 +1,74 @@
+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 tests.types import dataset
+
+# enable asyncio mode
+pytestmark = pytest.mark.asyncio
+
+PAYLOADS = [
+ 'foo',
+ 'AAbbCCddEEff1122334455',
+ 'aaBBccDDeeFF5544332211',
+ -12345678901234567890,
+ 12345678901234567890,
+]
+
+WRONG_PAYLOADS = [
+ '@BotFather',
+ 'spaces spaces spaces',
+ 1234567890123456789.0,
+]
+
+
+@pytest.fixture(params=PAYLOADS, name='payload')
+def payload_fixture(request):
+ return request.param
+
+
+@pytest.fixture(params=WRONG_PAYLOADS, name='wrong_payload')
+def wrong_payload_fixture(request):
+ return request.param
+
+
+@pytest.fixture(autouse=True)
+def get_bot_user_fixture(monkeypatch):
+ """ Monkey patching of bot.me calling. """
+ from aiogram.utils import deep_linking
+
+ async def get_bot_user_mock():
+ from aiogram.types import User
+ return User(**dataset.USER)
+
+ monkeypatch.setattr(deep_linking, '_get_bot_user', get_bot_user_mock)
+
+
+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}'
+
+ async def test_wrong_symbols(self, wrong_payload):
+ with pytest.raises(ValueError):
+ await get_start_link(wrong_payload)
+
+ 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}'
+
+ async def test_filter_encode_and_decode(self, payload):
+ _payload = filter_payload(payload)
+ encoded = encode_payload(_payload)
+ decoded = decode_payload(encoded)
+ assert decoded == str(payload)
+
+ async def test_get_start_link_with_encoding(self, payload):
+ # define link
+ link = await get_start_link(payload, encode=True)
+
+ # define reference link
+ payload = filter_payload(payload)
+ encoded_payload = encode_payload(payload)
+
+ assert link == f'https://t.me/{dataset.USER["username"]}?start={encoded_payload}'
diff --git a/tests/test_utils/test_helper.py b/tests/test_utils/test_helper.py
new file mode 100644
index 00000000..d202d289
--- /dev/null
+++ b/tests/test_utils/test_helper.py
@@ -0,0 +1,22 @@
+from aiogram.utils.helper import OrderedHelper, Item, ListItem
+
+
+class TestOrderedHelper:
+
+ def test_items_are_ordered(self):
+ class Helper(OrderedHelper):
+ A = Item()
+ D = Item()
+ C = Item()
+ B = Item()
+
+ assert Helper.all() == ['A', 'D', 'C', 'B']
+
+ def test_list_items_are_ordered(self):
+ class Helper(OrderedHelper):
+ A = ListItem()
+ D = ListItem()
+ C = ListItem()
+ B = ListItem()
+
+ assert Helper.all() == ['A', 'D', 'C', 'B']
diff --git a/tests/test_utils/test_markdown.py b/tests/test_utils/test_markdown.py
new file mode 100644
index 00000000..02faea2a
--- /dev/null
+++ b/tests/test_utils/test_markdown.py
@@ -0,0 +1,11 @@
+import pytest
+
+from aiogram.utils import markdown
+
+
+class TestMarkdownEscape:
+ def test_equality_sign_is_escaped(self):
+ assert markdown.escape_md(r"e = mc2") == r"e \= mc2"
+
+ def test_pre_escaped(self):
+ assert markdown.escape_md(r"hello\.") == r"hello\\\."
diff --git a/tests/types/dataset.py b/tests/types/dataset.py
index 18bcbdad..739e8e2c 100644
--- a/tests/types/dataset.py
+++ b/tests/types/dataset.py
@@ -35,6 +35,11 @@ AUDIO = {
"file_size": 9507774,
}
+BOT_COMMAND = {
+ "command": "start",
+ "description": "Start bot",
+}
+
CHAT_MEMBER = {
"user": USER,
"status": "administrator",
@@ -53,6 +58,10 @@ CONTACT = {
"last_name": "Smith",
}
+DICE = {
+ "value": 6
+}
+
DOCUMENT = {
"file_name": "test.docx",
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -255,6 +264,14 @@ MESSAGE_WITH_CONTACT = {
MESSAGE_WITH_DELETE_CHAT_PHOTO = {}
+MESSAGE_WITH_DICE = {
+ "message_id": 12345,
+ "from": USER,
+ "chat": CHAT,
+ "date": 1508768012,
+ "dice": DICE
+}
+
MESSAGE_WITH_DOCUMENT = {
"message_id": 12345,
"from": USER,
@@ -440,3 +457,8 @@ WEBHOOK_INFO = {
"has_custom_certificate": False,
"pending_update_count": 0,
}
+
+REPLY_KEYBOARD_MARKUP = {
+ "keyboard": [[{"text": "something here"}]],
+ "resize_keyboard": True,
+}
diff --git a/tests/types/test_input_media.py b/tests/types/test_input_media.py
new file mode 100644
index 00000000..953197c9
--- /dev/null
+++ b/tests/types/test_input_media.py
@@ -0,0 +1,42 @@
+from aiogram import types
+from .dataset import AUDIO, ANIMATION, \
+ DOCUMENT, PHOTO, VIDEO
+
+
+WIDTH = 'width'
+HEIGHT = 'height'
+
+input_media_audio = types.InputMediaAudio(
+ types.Audio(**AUDIO))
+input_media_animation = types.InputMediaAnimation(
+ types.Animation(**ANIMATION))
+input_media_document = types.InputMediaDocument(
+ types.Document(**DOCUMENT))
+input_media_video = types.InputMediaVideo(
+ types.Video(**VIDEO))
+input_media_photo = types.InputMediaPhoto(
+ types.PhotoSize(**PHOTO))
+
+
+def test_field_width():
+ """
+ https://core.telegram.org/bots/api#inputmedia
+ """
+ assert not hasattr(input_media_audio, WIDTH)
+ assert not hasattr(input_media_document, WIDTH)
+ assert not hasattr(input_media_photo, WIDTH)
+
+ assert hasattr(input_media_animation, WIDTH)
+ assert hasattr(input_media_video, WIDTH)
+
+
+def test_field_height():
+ """
+ https://core.telegram.org/bots/api#inputmedia
+ """
+ assert not hasattr(input_media_audio, HEIGHT)
+ assert not hasattr(input_media_document, HEIGHT)
+ assert not hasattr(input_media_photo, HEIGHT)
+
+ assert hasattr(input_media_animation, HEIGHT)
+ assert hasattr(input_media_video, HEIGHT)
diff --git a/tests/types/test_reply_keyboard.py b/tests/types/test_reply_keyboard.py
new file mode 100644
index 00000000..ae0b6d9e
--- /dev/null
+++ b/tests/types/test_reply_keyboard.py
@@ -0,0 +1,12 @@
+from aiogram import types
+from .dataset import REPLY_KEYBOARD_MARKUP
+
+reply_keyboard = types.ReplyKeyboardMarkup(**REPLY_KEYBOARD_MARKUP)
+
+
+def test_serialize():
+ assert reply_keyboard.to_python() == REPLY_KEYBOARD_MARKUP
+
+
+def test_deserialize():
+ assert reply_keyboard.to_object(reply_keyboard.to_python()) == reply_keyboard