diff --git a/README.md b/README.md index 74de8a8d..dfef918f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index 78dc071c..1cf2765d 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-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 diff --git a/aiogram/__init__.py b/aiogram/__init__.py index cb339a25..bebafcec 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = [ 'utils' ] -__version__ = '2.8' -__api_version__ = '4.8' +__version__ = '2.9.2' +__api_version__ = '4.9' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 3b341ec9..1d0c4f7b 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -153,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.8 + List is updated to Bot API 4.9 """ mode = HelperMode.lowerCamelCase diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 41f30af1..f427fa8c 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -398,6 +398,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): files = {} prepare_file(payload, files, 'document', document) + prepare_attachment(payload, files, 'thumb', thumb) result = await self.request(api.Methods.SEND_DOCUMENT, payload, files) return types.Message(**result) @@ -503,7 +504,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param height: Animation height :type height: :obj:`typing.Union[base.Integer, None]` :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail‘s width and height should not exceed 90. + A thumbnail‘s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters :type caption: :obj:`typing.Union[base.String, None]` @@ -886,6 +887,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): Use this method to send a native poll. A native poll can't be sent to a private chat. On success, the sent Message is returned. + Source: https://core.telegram.org/bots/api#sendpoll + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername). A native poll can't be sent to a private chat. @@ -1953,7 +1956,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): files = {} prepare_file(payload, files, 'png_sticker', png_sticker) - prepare_file(payload, files, 'tgs_sticker', png_sticker) + prepare_file(payload, files, 'tgs_sticker', tgs_sticker) result = await self.request(api.Methods.ADD_STICKER_TO_SET, payload, files) return result diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index 9ec18090..a7601cc4 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -1,12 +1,19 @@ """ This module has mongo storage for finite-state machine - based on `aiomongo `_ driver """ from typing import Union, Dict, Optional, List, Tuple, AnyStr -import aiomongo -from aiomongo import AioMongoClient, Database +import pymongo + +try: + import motor + from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase +except ModuleNotFoundError as e: + import warnings + warnings.warn("Install motor with `pip install motor`") + raise e from ...dispatcher.storage import BaseStorage @@ -35,22 +42,34 @@ class MongoStorage(BaseStorage): """ - def __init__(self, host='localhost', port=27017, db_name='aiogram_fsm', + def __init__(self, host='localhost', port=27017, db_name='aiogram_fsm', uri=None, username=None, password=None, index=True, **kwargs): self._host = host self._port = port self._db_name: str = db_name + self._uri = uri self._username = username self._password = password self._kwargs = kwargs - self._mongo: Union[AioMongoClient, None] = None - self._db: Union[Database, None] = None + self._mongo: Optional[AsyncIOMotorClient] = None + self._db: Optional[AsyncIOMotorDatabase] = None self._index = index - async def get_client(self) -> AioMongoClient: - if isinstance(self._mongo, AioMongoClient): + async def get_client(self) -> AsyncIOMotorClient: + if isinstance(self._mongo, AsyncIOMotorClient): + return self._mongo + + if self._uri: + try: + self._mongo = AsyncIOMotorClient(self._uri) + except pymongo.errors.ConfigurationError as e: + if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]: + import logging + logger = logging.getLogger("aiogram") + logger.warning("Run `pip install dnspython==1.16.0` in order to fix ConfigurationError. More information: https://github.com/mongodb/mongo-python-driver/pull/423#issuecomment-528998245") + raise e return self._mongo uri = 'mongodb://' @@ -63,16 +82,16 @@ class MongoStorage(BaseStorage): uri += f'{self._host}:{self._port}' if self._host else f'localhost:{self._port}' # define and return client - self._mongo = await aiomongo.create_client(uri) + self._mongo = AsyncIOMotorClient(uri) return self._mongo - async def get_db(self) -> Database: + async def get_db(self) -> AsyncIOMotorDatabase: """ Get Mongo db This property is awaitable. """ - if isinstance(self._db, Database): + if isinstance(self._db, AsyncIOMotorDatabase): return self._db mongo = await self.get_client() @@ -93,8 +112,6 @@ class MongoStorage(BaseStorage): self._mongo.close() async def wait_closed(self): - if self._mongo: - return await self._mongo.wait_closed() return True async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index bf88eff7..74dd736c 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -208,7 +208,7 @@ class RedisStorage2(BaseStorage): .. code-block:: python3 - storage = RedisStorage('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key') + storage = RedisStorage2('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key') dp = Dispatcher(bot, storage=storage) And need to close Redis connection when shutdown diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 63f54510..bb6d8003 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -59,13 +59,13 @@ class I18nMiddleware(BaseMiddleware): with open(mo_path, 'rb') as fp: translations[name] = gettext.GNUTranslations(fp) elif os.path.exists(mo_path[:-2] + 'po'): - raise RuntimeError(f"Found locale '{name} but this language is not compiled!") + raise RuntimeError(f"Found locale '{name}' but this language is not compiled!") return translations def reload(self): """ - Hot reload locles + Hot reload locales """ self.locales = self.find_locales() diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index b485fa49..7a3aa5b3 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -10,8 +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 -from .filters.builtin import IsSenderContact + RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter, ForwardedMessageFilter, \ + IsSenderContact, ChatTypeFilter, AbstractFilter from .handler import Handler from .middlewares import MiddlewareManager from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \ @@ -160,6 +160,19 @@ class Dispatcher(DataMixin, ContextInstanceMixin): 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 + ]) + filters_factory.bind(ChatTypeFilter, event_handlers=[ + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + self.callback_query_handlers, + ]) def __del__(self): self.stop_polling() @@ -203,39 +216,50 @@ 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: @@ -421,7 +445,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): .. code-block:: python3 - @dp.message_handler(rexexp='^[a-z]+-[0-9]+') + @dp.message_handler(regexp='^[a-z]+-[0-9]+') async def msg_handler(message: types.Message): Filter messages by command regular expression: @@ -1215,3 +1239,35 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return wrapped return decorator + + def bind_filter(self, callback: typing.Union[typing.Callable, AbstractFilter], + validator: typing.Optional[typing.Callable] = None, + event_handlers: typing.Optional[typing.List[Handler]] = None, + exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None): + """ + Register filter + + :param callback: callable or subclass of :obj:`AbstractFilter` + :param validator: custom validator. + :param event_handlers: list of instances of :obj:`Handler` + :param exclude_event_handlers: list of excluded event handlers (:obj:`Handler`) + """ + self.filters_factory.bind(callback=callback, validator=validator, event_handlers=event_handlers, + exclude_event_handlers=exclude_event_handlers) + + def unbind_filter(self, callback: typing.Union[typing.Callable, AbstractFilter]): + """ + Unregister filter + + :param callback: callable of subclass of :obj:`AbstractFilter` + """ + self.filters_factory.unbind(callback=callback) + + def setup_middleware(self, middleware): + """ + Setup middleware + + :param middleware: + :return: + """ + self.middleware.setup(middleware) diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 6de3cc7a..5f839662 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,6 +1,7 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \ - Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact + Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact, ForwardedMessageFilter, \ + ChatTypeFilter from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -32,4 +33,6 @@ __all__ = [ 'get_filters_spec', 'execute_filter', 'check_filters', + 'ForwardedMessageFilter', + 'ChatTypeFilter', ] diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 62527e2b..20317f57 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -1,6 +1,7 @@ import inspect import re import typing +import warnings from contextvars import ContextVar from dataclasses import dataclass, field from typing import Any, Dict, Iterable, Optional, Union @@ -9,8 +10,7 @@ from babel.support import LazyProxy from aiogram import types from aiogram.dispatcher.filters.filters import BoundFilter, Filter -from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType - +from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] @@ -37,7 +37,8 @@ class Command(Filter): def __init__(self, commands: Union[Iterable, str], prefixes: Union[Iterable, str] = '/', ignore_case: bool = True, - ignore_mention: bool = False): + ignore_mention: bool = False, + ignore_caption: bool = True): """ Filter can be initialized from filters factory or by simply creating instance of this class. @@ -55,6 +56,15 @@ class Command(Filter): :param ignore_case: Ignore case of the command :param ignore_mention: Ignore mention in command (By default this filter pass only the commands addressed to current bot) + :param ignore_caption: Ignore caption from message (in message types like photo, video, audio, etc) + By default is True. If you want check commands in captions, you also should set required content_types. + + Examples: + + .. code-block:: python + + @dp.message_handler(commands=['myCommand'], commands_ignore_caption=False, content_types=ContentType.ANY) + @dp.message_handler(Command(['myCommand'], ignore_caption=False), content_types=[ContentType.TEXT, ContentType.DOCUMENT]) """ if isinstance(commands, str): commands = (commands,) @@ -63,6 +73,7 @@ class Command(Filter): self.prefixes = prefixes self.ignore_case = ignore_case self.ignore_mention = ignore_mention + self.ignore_caption = ignore_caption @classmethod def validate(cls, full_config: Dict[str, Any]) -> Optional[Dict[str, Any]]: @@ -73,7 +84,8 @@ class Command(Filter): - ``command`` - ``commands_prefix`` (will be passed as ``prefixes``) - - ``commands_ignore_mention`` (will be passed as ``ignore_mention`` + - ``commands_ignore_mention`` (will be passed as ``ignore_mention``) + - ``commands_ignore_caption`` (will be passed as ``ignore_caption``) :param full_config: :return: config or empty dict @@ -85,17 +97,20 @@ class Command(Filter): config['prefixes'] = full_config.pop('commands_prefix') if config and 'commands_ignore_mention' in full_config: config['ignore_mention'] = full_config.pop('commands_ignore_mention') + if config and 'commands_ignore_caption' in full_config: + config['ignore_caption'] = full_config.pop('commands_ignore_caption') return config async def check(self, message: types.Message): - return await self.check_command(message, self.commands, self.prefixes, self.ignore_case, self.ignore_mention) + return await self.check_command(message, self.commands, self.prefixes, self.ignore_case, self.ignore_mention, self.ignore_caption) - @staticmethod - async def check_command(message: types.Message, commands, prefixes, ignore_case=True, ignore_mention=False): - if not message.text: # Prevent to use with non-text content types + @classmethod + async def check_command(cls, message: types.Message, commands, prefixes, ignore_case=True, ignore_mention=False, ignore_caption=True): + text = message.text or (message.caption if not ignore_caption else None) + if not text: return False - full_command = message.text.split()[0] + full_command = text.split()[0] prefix, (command, _, mention) = full_command[0], full_command[1:].partition('@') if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower(): @@ -105,7 +120,7 @@ class Command(Filter): if (command.lower() if ignore_case else command) not in commands: return False - return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)} + return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention)} @dataclass class CommandObj: @@ -591,7 +606,9 @@ class IDFilter(Filter): async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]): if isinstance(obj, Message): - user_id = obj.from_user.id + user_id = None + if obj.from_user is not None: + user_id = obj.from_user.id chat_id = obj.chat.id elif isinstance(obj, CallbackQuery): user_id = obj.from_user.id @@ -681,3 +698,34 @@ 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 + + +class ChatTypeFilter(BoundFilter): + key = 'chat_type' + + def __init__(self, chat_type: typing.Container[ChatType]): + if isinstance(chat_type, str): + chat_type = {chat_type} + + self.chat_type: typing.Set[str] = set(chat_type) + + async def check(self, obj: Union[Message, CallbackQuery]): + if isinstance(obj, Message): + obj = obj.chat + elif isinstance(obj, CallbackQuery): + obj = obj.message.chat + else: + warnings.warn("ChatTypeFilter doesn't support %s as input", type(obj)) + return False + + return obj.type in self.chat_type diff --git a/aiogram/dispatcher/filters/factory.py b/aiogram/dispatcher/filters/factory.py index 13b188ff..564e7f89 100644 --- a/aiogram/dispatcher/filters/factory.py +++ b/aiogram/dispatcher/filters/factory.py @@ -30,7 +30,7 @@ class FiltersFactory: def unbind(self, callback: typing.Union[typing.Callable, AbstractFilter]): """ - Unregister callback + Unregister filter :param callback: callable of subclass of :obj:`AbstractFilter` """ diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index cd5e9b50..38219012 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -33,7 +33,7 @@ def _check_spec(spec: inspect.FullArgSpec, kwargs: dict): if spec.varkw: return kwargs - return {k: v for k, v in kwargs.items() if k in spec.args} + return {k: v for k, v in kwargs.items() if k in set(spec.args + spec.kwonlyargs)} class Handler: diff --git a/aiogram/types/animation.py b/aiogram/types/animation.py index 78f5235a..b08089c1 100644 --- a/aiogram/types/animation.py +++ b/aiogram/types/animation.py @@ -15,6 +15,9 @@ class Animation(base.TelegramObject, mixins.Downloadable): 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() thumb: PhotoSize = fields.Field(base=PhotoSize) file_name: base.String = fields.Field() mime_type: base.String = fields.Field() diff --git a/aiogram/types/callback_query.py b/aiogram/types/callback_query.py index 51ba1f17..e847bff8 100644 --- a/aiogram/types/callback_query.py +++ b/aiogram/types/callback_query.py @@ -54,8 +54,11 @@ class CallbackQuery(base.TelegramObject): :type cache_time: :obj:`typing.Union[base.Integer, None]` :return: On success, True is returned. :rtype: :obj:`base.Boolean`""" - await self.bot.answer_callback_query(callback_query_id=self.id, text=text, - show_alert=show_alert, url=url, cache_time=cache_time) + return await self.bot.answer_callback_query(callback_query_id=self.id, + text=text, + show_alert=show_alert, + url=url, + cache_time=cache_time) def __hash__(self): return hash(self.id) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 4a7287d8..28cc5ed0 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -10,6 +10,7 @@ from .chat_member import ChatMember from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .input_file import InputFile +from ..utils.deprecated import deprecated class Chat(base.TelegramObject): @@ -512,6 +513,7 @@ class ChatType(helper.Helper): return obj.type in chat_types @classmethod + @deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0") def is_private(cls, obj) -> bool: """ Check chat is private @@ -522,6 +524,7 @@ class ChatType(helper.Helper): return cls._check(obj, [cls.PRIVATE]) @classmethod + @deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0") def is_group(cls, obj) -> bool: """ Check chat is group @@ -532,6 +535,7 @@ class ChatType(helper.Helper): return cls._check(obj, [cls.GROUP]) @classmethod + @deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0") def is_super_group(cls, obj) -> bool: """ Check chat is super-group @@ -542,6 +546,7 @@ class ChatType(helper.Helper): return cls._check(obj, [cls.SUPER_GROUP]) @classmethod + @deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0") def is_group_or_super_group(cls, obj) -> bool: """ Check chat is group or super-group @@ -552,6 +557,7 @@ class ChatType(helper.Helper): return cls._check(obj, [cls.GROUP, cls.SUPER_GROUP]) @classmethod + @deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0") def is_channel(cls, obj) -> bool: """ Check chat is channel diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py index 6dfb190f..7b3f1727 100644 --- a/aiogram/types/dice.py +++ b/aiogram/types/dice.py @@ -16,3 +16,4 @@ class Dice(base.TelegramObject): class DiceEmoji: DICE = '🎲' DART = '🎯' + BASKETBALL = '🏀' 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/inline_query_result.py b/aiogram/types/inline_query_result.py index cf26f8a4..fccaa2a1 100644 --- a/aiogram/types/inline_query_result.py +++ b/aiogram/types/inline_query_result.py @@ -118,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) @@ -157,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 952e7a55..9a77658f 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -132,13 +132,11 @@ class InputMediaDocument(InputMedia): class InputMediaAudio(InputMedia): """ - Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. + Represents an audio file to be treated as music to be sent. - https://core.telegram.org/bots/api#inputmediaanimation + https://core.telegram.org/bots/api#inputmediaaudio """ - 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) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index b4166e79..52c2b3a8 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -4,8 +4,10 @@ import datetime import functools 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 @@ -15,14 +17,14 @@ 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,9 +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 -from ..utils.text_decorations import html_decoration, markdown_decoration class Message(base.TelegramObject): @@ -41,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) @@ -50,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() @@ -83,7 +84,7 @@ 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() @@ -158,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): """ @@ -167,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): @@ -180,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): @@ -191,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): """ @@ -235,16 +237,15 @@ class Message(base.TelegramObject): :return: str """ if self.chat.type == ChatType.PRIVATE: - raise TypeError('Invalid chat type!') - - url = 'https://t.me/' + raise TypeError('Invalid chat type + url = "https://t.me/" if self.chat.username: # Generates public link - url += f'{self.chat.username}/' + 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}' + url += f"c/{self.chat.shifted_id}/" + url += f"{self.message_id}" return url @@ -267,15 +268,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 @@ -297,23 +304,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. @@ -337,26 +352,35 @@ 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, + thumb: typing.Union[typing.Union[base.InputFile, 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. @@ -378,6 +402,9 @@ class Message(base.TelegramObject): :type performer: :obj:`typing.Union[base.String, None]` :param title: Track name :type title: :obj:`typing.Union[base.String, None]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 320. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, 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, @@ -388,30 +415,39 @@ 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, + thumb=thumb, + 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). @@ -431,7 +467,7 @@ class Message(base.TelegramObject): :param height: Animation height :type height: :obj:`typing.Union[base.Integer, None]` :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail‘s width and height should not exceed 90. + A thumbnail‘s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters :type caption: :obj:`typing.Union[base.String, None]` @@ -448,27 +484,36 @@ 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], + 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 general files. @@ -478,6 +523,9 @@ class Message(base.TelegramObject): :param document: File to send. :type document: :obj:`typing.Union[base.InputFile, base.String]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 320. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` :param caption: Document caption (may also be used when resending documents by file_id), 0-200 characters :type caption: :obj:`typing.Union[base.String, None]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -493,26 +541,36 @@ 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, + thumb=thumb, + 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, + 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 video files, Telegram clients support mp4 videos (other formats may be sent as Document). @@ -527,6 +585,9 @@ class Message(base.TelegramObject): :type width: :obj:`typing.Union[base.Integer, None]` :param height: Video height :type height: :obj:`typing.Union[base.Integer, None]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 320. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` :param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters :type caption: :obj:`typing.Union[base.String, None]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -542,27 +603,36 @@ 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, + 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_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. @@ -591,24 +661,33 @@ 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, + thumb: typing.Union[typing.Union[base.InputFile, 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: """ 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. @@ -621,6 +700,9 @@ class Message(base.TelegramObject): :type duration: :obj:`typing.Union[base.Integer, None]` :param length: Video width and height :type length: :obj:`typing.Union[base.Integer, None]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 320. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, 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, @@ -631,17 +713,23 @@ 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, + thumb=thumb, + 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. @@ -655,20 +743,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. @@ -690,24 +786,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. @@ -733,24 +838,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. @@ -772,20 +886,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. @@ -803,19 +926,106 @@ 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 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: + async def answer_poll( + self, + 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_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: + """ + Use this method to send a native poll. A native poll can't be sent to a private chat. + On success, the sent Message is returned. + + Source: https://core.telegram.org/bots/api#sendpoll + + :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 + :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_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_poll( + chat_id=self.chat.id, + question=question, + options=options, + is_anonymous=is_anonymous, + type=type, + allows_multiple_answers=allows_multiple_answers, + correct_option_id=correct_option_id, + explanation=explanation, + explanation_parse_mode=explanation_parse_mode, + open_period=open_period, + close_date=close_date, + is_closed=is_closed, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) + + 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. @@ -836,21 +1046,29 @@ class Message(base.TelegramObject): :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) + 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: + 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 @@ -872,23 +1090,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. @@ -912,26 +1138,35 @@ 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, + thumb: typing.Union[typing.Union[base.InputFile, 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. @@ -953,6 +1188,9 @@ class Message(base.TelegramObject): :type performer: :obj:`typing.Union[base.String, None]` :param title: Track name :type title: :obj:`typing.Union[base.String, None]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 320. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, 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, @@ -963,30 +1201,39 @@ 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, + thumb=thumb, + 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). @@ -1006,7 +1253,7 @@ class Message(base.TelegramObject): :param height: Animation height :type height: :obj:`typing.Union[base.Integer, None]` :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail‘s width and height should not exceed 90. + A thumbnail‘s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters :type caption: :obj:`typing.Union[base.String, None]` @@ -1023,27 +1270,36 @@ 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], + 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 general files. @@ -1053,6 +1309,9 @@ class Message(base.TelegramObject): :param document: File to send. :type document: :obj:`typing.Union[base.InputFile, base.String]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 320. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` :param caption: Document caption (may also be used when resending documents by file_id), 0-200 characters :type caption: :obj:`typing.Union[base.String, None]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -1068,26 +1327,36 @@ 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, + 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_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, + 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 video files, Telegram clients support mp4 videos (other formats may be sent as Document). @@ -1102,6 +1371,9 @@ class Message(base.TelegramObject): :type width: :obj:`typing.Union[base.Integer, None]` :param height: Video height :type height: :obj:`typing.Union[base.Integer, None]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 320. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` :param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters :type caption: :obj:`typing.Union[base.String, None]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -1117,27 +1389,36 @@ 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, + 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_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. @@ -1166,24 +1447,33 @@ 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, + thumb: typing.Union[typing.Union[base.InputFile, 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: """ 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. @@ -1196,6 +1486,9 @@ class Message(base.TelegramObject): :type duration: :obj:`typing.Union[base.Integer, None]` :param length: Video width and height :type length: :obj:`typing.Union[base.Integer, None]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 320. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, 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, @@ -1206,17 +1499,23 @@ 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, + thumb=thumb, + 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. @@ -1230,20 +1529,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. @@ -1265,24 +1572,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. @@ -1308,24 +1624,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. @@ -1347,20 +1672,108 @@ 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_poll( + self, + 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_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: + """ + Use this method to send a native poll. A native poll can't be sent to a private chat. + On success, the sent Message is returned. + + Source: https://core.telegram.org/bots/api#sendpoll + + :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 + :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_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_poll( + chat_id=self.chat.id, + question=question, + options=options, + is_anonymous=is_anonymous, + type=type, + allows_multiple_answers=allows_multiple_answers, + correct_option_id=correct_option_id, + explanation=explanation, + explanation_parse_mode=explanation_parse_mode, + open_period=open_period, + close_date=close_date, + is_closed=is_closed, + 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: """ Use this method to send .webp stickers. @@ -1378,19 +1791,27 @@ 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_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: + 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. @@ -1411,13 +1832,18 @@ class Message(base.TelegramObject): :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) + 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: + async def forward( + self, + chat_id: typing.Union[base.Integer, base.String], + disable_notification: typing.Union[base.Boolean, None] = None, + ) -> Message: """ Forward this message @@ -1430,13 +1856,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). @@ -1455,16 +1885,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). @@ -1481,12 +1916,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. @@ -1507,12 +1949,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). @@ -1524,8 +1970,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]: """ @@ -1535,12 +1982,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 @@ -1558,13 +2009,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. @@ -1577,8 +2032,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: """ @@ -1597,7 +2053,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. @@ -1613,11 +2071,13 @@ 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], - disable_notification: typing.Optional[bool] = None, - reply_to_message_id: typing.Optional[int] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = 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 @@ -1646,7 +2106,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( @@ -1681,7 +2141,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") @@ -1692,17 +2152,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=[option.text for option in 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.") @@ -1739,6 +2203,7 @@ class ContentType(helper.Helper): :key: UNKNOWN :key: ANY """ + mode = helper.HelperMode.snake_case TEXT = helper.Item() # text @@ -1802,6 +2267,7 @@ class ContentTypes(helper.Helper): :key: UNKNOWN :key: ANY """ + mode = helper.HelperMode.snake_case TEXT = helper.ListItem() # text diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index ced20417..ffe07ae1 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -111,11 +111,13 @@ class KeyboardButton(base.TelegramObject): def __init__(self, text: base.String, request_contact: base.Boolean = None, request_location: base.Boolean = None, - request_poll: KeyboardButtonPollType = None): + request_poll: KeyboardButtonPollType = None, + **kwargs): super(KeyboardButton, self).__init__(text=text, request_contact=request_contact, request_location=request_location, - request_poll=request_poll) + request_poll=request_poll, + **kwargs) class ReplyKeyboardRemove(base.TelegramObject): diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 2146cb9d..9d1afacc 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -9,7 +9,7 @@ from .message import Message from .poll import Poll, PollAnswer from .pre_checkout_query import PreCheckoutQuery from .shipping_query import ShippingQuery -from ..utils import helper +from ..utils import helper, deprecated class Update(base.TelegramObject): @@ -55,9 +55,15 @@ class AllowedUpdates(helper.Helper): CHANNEL_POST = helper.ListItem() # channel_post EDITED_CHANNEL_POST = helper.ListItem() # edited_channel_post INLINE_QUERY = helper.ListItem() # inline_query - CHOSEN_INLINE_QUERY = helper.ListItem() # chosen_inline_result + CHOSEN_INLINE_RESULT = helper.ListItem() # chosen_inline_result 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 + + CHOSEN_INLINE_QUERY = deprecated.DeprecatedReadOnlyClassVar( + "`CHOSEN_INLINE_QUERY` is a deprecated value for allowed update. " + "Use `CHOSEN_INLINE_RESULT`", + new_value_getter=lambda cls: cls.CHOSEN_INLINE_RESULT, + ) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 83a9034c..6d0d7ee3 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -2,7 +2,7 @@ import asyncio import inspect import warnings import functools -from typing import Callable +from typing import Callable, Generic, TypeVar, Type, Optional def deprecated(reason, stacklevel=2) -> Callable: @@ -129,3 +129,36 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve return wrapped return decorator + + +_VT = TypeVar("_VT") +_OwnerCls = TypeVar("_OwnerCls") + + +class DeprecatedReadOnlyClassVar(Generic[_OwnerCls, _VT]): + """ + DeprecatedReadOnlyClassVar[Owner, ValueType] + + :param warning_message: Warning message when getter gets called + :param new_value_getter: Any callable with (owner_class: Type[Owner]) -> ValueType + signature that will be executed + + Usage example: + + >>> class MyClass: + ... some_attribute: DeprecatedReadOnlyClassVar[MyClass, int] = \ + ... DeprecatedReadOnlyClassVar( + ... "Warning message.", lambda owner: 15) + ... + >>> MyClass.some_attribute # does warning.warn with `Warning message` and returns 15 in the current case + """ + + __slots__ = "_new_value_getter", "_warning_message" + + def __init__(self, warning_message: str, new_value_getter: Callable[[_OwnerCls], _VT]): + self._warning_message = warning_message + self._new_value_getter = new_value_getter + + def __get__(self, instance: Optional[_OwnerCls], owner: Type[_OwnerCls]): + warn_deprecated(self._warning_message, stacklevel=3) + return self._new_value_getter(owner) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index cee2820a..a289be25 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -7,6 +7,7 @@ - MessageNotModified - MessageToForwardNotFound - MessageToDeleteNotFound + - MessageToPinNotFound - MessageIdentifierNotSpecified - MessageTextIsEmpty - MessageCantBeEdited @@ -182,6 +183,13 @@ class MessageToDeleteNotFound(MessageError): match = 'message to delete not found' +class MessageToPinNotFound(MessageError): + """ + Will be raised when you try to pin deleted or unknown message. + """ + match = 'message to pin not found' + + class MessageToReplyNotFound(MessageError): """ Will be raised when you try to reply to very old or deleted or unknown message. diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index b56e14b1..da27bc39 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -18,7 +18,7 @@ HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """} _HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS -def quote_html(*content, sep=" "): +def quote_html(*content, sep=" ") -> str: """ Quote HTML symbols @@ -33,7 +33,7 @@ def quote_html(*content, sep=" "): return html_decoration.quote(_join(*content, sep=sep)) -def escape_md(*content, sep=" "): +def escape_md(*content, sep=" ") -> str: """ Escape markdown text @@ -61,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) @@ -69,12 +69,12 @@ def bold(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.bold.format( + 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) @@ -82,12 +82,12 @@ def hbold(*content, sep=" "): :param sep: :return: """ - return html_decoration.bold.format( + 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) @@ -95,12 +95,12 @@ def italic(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.italic.format( + 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) @@ -108,12 +108,12 @@ def hitalic(*content, sep=" "): :param sep: :return: """ - return html_decoration.italic.format( + 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) @@ -121,12 +121,12 @@ def code(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.code.format( + 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) @@ -134,12 +134,12 @@ def hcode(*content, sep=" "): :param sep: :return: """ - return html_decoration.code.format( + 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) @@ -147,12 +147,12 @@ def pre(*content, sep="\n"): :param sep: :return: """ - return markdown_decoration.pre.format( + 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) @@ -160,12 +160,12 @@ def hpre(*content, sep="\n"): :param sep: :return: """ - return html_decoration.pre.format( + return html_decoration.pre( value=html_decoration.quote(_join(*content, sep=sep)) ) -def underline(*content, sep=" "): +def underline(*content, sep=" ") -> str: """ Make underlined text (Markdown) @@ -173,12 +173,12 @@ def underline(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.underline.format( + return markdown_decoration.underline( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hunderline(*content, sep=" "): +def hunderline(*content, sep=" ") -> str: """ Make underlined text (HTML) @@ -186,12 +186,12 @@ def hunderline(*content, sep=" "): :param sep: :return: """ - return html_decoration.underline.format( + return html_decoration.underline( value=html_decoration.quote(_join(*content, sep=sep)) ) -def strikethrough(*content, sep=" "): +def strikethrough(*content, sep=" ") -> str: """ Make strikethrough text (Markdown) @@ -199,12 +199,12 @@ def strikethrough(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.strikethrough.format( + return markdown_decoration.strikethrough( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hstrikethrough(*content, sep=" "): +def hstrikethrough(*content, sep=" ") -> str: """ Make strikethrough text (HTML) @@ -212,7 +212,7 @@ def hstrikethrough(*content, sep=" "): :param sep: :return: """ - return html_decoration.strikethrough.format( + return html_decoration.strikethrough( value=html_decoration.quote(_join(*content, sep=sep)) ) @@ -225,7 +225,7 @@ def link(title: str, url: str) -> str: :param url: :return: """ - return markdown_decoration.link.format(value=markdown_decoration.quote(title), link=url) + return markdown_decoration.link(value=markdown_decoration.quote(title), link=url) def hlink(title: str, url: str) -> str: @@ -236,7 +236,7 @@ def hlink(title: str, url: str) -> str: :param url: :return: """ - return html_decoration.link.format(value=html_decoration.quote(title), link=url) + return html_decoration.link(value=html_decoration.quote(title), link=url) def hide_link(url: str) -> str: diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index ad52c9d7..81592465 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -1,33 +1,23 @@ from __future__ import annotations + import html import re -import struct -from dataclasses import dataclass -from typing import TYPE_CHECKING, AnyStr, Callable, Generator, Iterable, List, Optional +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Generator, List, Optional, Pattern, cast -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from aiogram.types import MessageEntity __all__ = ( "TextDecoration", + "HtmlDecoration", + "MarkdownDecoration", "html_decoration", "markdown_decoration", - "add_surrogate", - "remove_surrogate", ) -@dataclass -class TextDecoration: - link: str - bold: str - italic: str - code: str - pre: str - underline: str - strikethrough: str - quote: Callable[[AnyStr], AnyStr] - +class TextDecoration(ABC): def apply_entity(self, entity: MessageEntity, text: str) -> str: """ Apply single entity to text @@ -36,24 +26,28 @@ class TextDecoration: :param text: :return: """ - if entity.type in ( - "bold", - "italic", - "code", - "pre", - "underline", - "strikethrough", - ): - return getattr(self, entity.type).format(value=text) - elif entity.type == "text_mention": - return self.link.format(value=text, link=f"tg://user?id={entity.user.id}") - elif entity.type == "text_link": - return self.link.format(value=text, link=entity.url) - elif entity.type == "url": + 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, entities: Optional[List[MessageEntity]] = None) -> str: + def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str: """ Unparse message entities @@ -61,34 +55,34 @@ class TextDecoration: :param entities: Array of MessageEntities :return: """ - text = add_surrogate(text) result = "".join( self._unparse_entities( - text, sorted(entities, key=lambda item: item.offset) if entities else [] + self._add_surrogates(text), sorted(entities, key=lambda item: item.offset) if entities else [] ) ) - return remove_surrogate(result) + return result def _unparse_entities( self, - text: str, - entities: Iterable[MessageEntity], + text: bytes, + entities: List[MessageEntity], offset: Optional[int] = None, length: Optional[int] = None, ) -> Generator[str, None, None]: - offset = offset or 0 + if offset is None: + offset = 0 length = length or len(text) for index, entity in enumerate(entities): - if entity.offset < offset: + if entity.offset * 2 < offset: continue - if entity.offset > offset: - yield self.quote(text[offset : entity.offset]) - start = entity.offset - offset = entity.offset + entity.length + if entity.offset * 2 > offset: + yield self.quote(self._remove_surrogates(text[offset : entity.offset * 2])) + start = entity.offset * 2 + offset = entity.offset * 2 + entity.length * 2 sub_entities = list( - filter(lambda e: e.offset < offset, entities[index + 1 :]) + filter(lambda e: e.offset * 2 < (offset or 0), entities[index + 1 :]) ) yield self.apply_entity( entity, @@ -100,44 +94,112 @@ class TextDecoration: ) if offset < length: - yield self.quote(text[offset:length]) + yield self.quote(self._remove_surrogates(text[offset:length])) + + @staticmethod + def _add_surrogates(text: str): + return text.encode('utf-16-le') + + @staticmethod + def _remove_surrogates(text: bytes): + return text.decode('utf-16-le') + + @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 -html_decoration = TextDecoration( - link='{value}', - bold="{value}", - italic="{value}", - code="{value}", - pre="
{value}
", - underline="{value}", - strikethrough="{value}", - quote=html.escape, -) +class HtmlDecoration(TextDecoration): + def link(self, value: str, link: str) -> str: + return f'{value}' -MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-=|{}.!])") + def bold(self, value: str) -> str: + return f"{value}" -markdown_decoration = TextDecoration( - link="[{value}]({link})", - bold="*{value}*", - italic="_{value}_\r", - code="`{value}`", - pre="```{value}```", - underline="__{value}__", - strikethrough="~{value}~", - quote=lambda text: re.sub( - pattern=MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=text - ), -) + 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, quote=False) -def add_surrogate(text: str) -> str: - return "".join( - "".join(chr(d) for d in struct.unpack("#+\-=|{}.!\\])") + + 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) -def remove_surrogate(text: str) -> str: - return text.encode("utf-16", "surrogatepass").decode("utf-16") +html_decoration = HtmlDecoration() +markdown_decoration = MarkdownDecoration() diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index af06b73e..f53a4c95 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -141,6 +141,22 @@ IsReplyFilter :show-inheritance: +ForwardedMessageFilter +------------- + +.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter + :members: + :show-inheritance: + + +ChatTypeFilter +------------- + +.. autoclass:: aiogram.dispatcher.filters.filters.ChatTypeFilter + :members: + :show-inheritance: + + Making own filters (Custom filters) =================================== diff --git a/docs/source/index.rst b/docs/source/index.rst index b18d386e..0ac6eccd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-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 diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index b0724a78..319886ce 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -28,13 +28,13 @@ If you want to handle all messages in the chat simply add handler without filter .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 35-37 + :lines: 44-49 Last step: run long polling. .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 40-41 + :lines: 52-53 Summary ------- @@ -42,4 +42,4 @@ Summary .. literalinclude:: ../../examples/echo_bot.py :language: python :linenos: - :lines: -19,27- + :lines: -27,43- diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index 9a8affe9..2335ea95 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -112,7 +112,7 @@ async def query_post_vote(query: types.CallbackQuery, callback_data: dict): @dp.errors_handler(exception=MessageNotModified) async def message_not_modified_handler(update, error): - return True + return True # errors_handler must return True if error was handled correctly if __name__ == '__main__': diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py index 5fc9c548..a6d246d5 100644 --- a/examples/callback_data_factory_simple.py +++ b/examples/callback_data_factory_simple.py @@ -61,7 +61,7 @@ async def callback_vote_action(query: types.CallbackQuery, callback_data: dict): @dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises async def message_not_modified_handler(update, error): - return True + return True # errors_handler must return True if error was handled correctly if __name__ == '__main__': diff --git a/examples/chat_type_filter.py b/examples/chat_type_filter.py new file mode 100644 index 00000000..08bb1858 --- /dev/null +++ b/examples/chat_type_filter.py @@ -0,0 +1,42 @@ +""" +This is an example with usage of ChatTypeFilter +It filters incoming object based on type of its chat type +""" + +import logging + +from aiogram import Bot, Dispatcher, executor, types +from aiogram.dispatcher.handler import SkipHandler +from aiogram.types import ChatType + +API_TOKEN = 'BOT TOKEN HERE' + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Initialize bot and dispatcher +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + + +@dp.message_handler(chat_type=[ChatType.PRIVATE, ChatType.CHANNEL]) +async def send_welcome(message: types.Message): + """ + This handler will be called when user sends `/start` or `/help` command + """ + await message.reply("Hi!\nI'm hearing your messages in private chats and channels") + + # propagate message to the next handler + raise SkipHandler + + +@dp.message_handler(chat_type=ChatType.PRIVATE) +async def send_welcome(message: types.Message): + """ + This handler will be called when user sends `/start` or `/help` command + """ + await message.reply("Hi!\nI'm hearing your messages only in private chats") + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) diff --git a/examples/i18n_example.py b/examples/i18n_example.py index b626d048..29b43210 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -1,8 +1,8 @@ """ -Internalize your bot +Internationalize your bot Step 1: extract texts - # pybabel extract i18n_example.py -o locales/mybot.pot + # pybabel extract --input-dirs=. -o locales/mybot.pot Some useful options: - Extract texts with pluralization support @@ -16,9 +16,14 @@ Step 1: extract texts - Set version # --version=2.2 -Step 2: create *.po files. For e.g. create en, ru, uk locales. - # echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l -Step 3: translate texts +Step 2: create *.po files. E.g. create en, ru, uk locales. + # pybabel init -i locales/mybot.pot -d locales -D mybot -l en + # pybabel init -i locales/mybot.pot -d locales -D mybot -l ru + # pybabel init -i locales/mybot.pot -d locales -D mybot -l uk + +Step 3: translate texts located in locales/{language}/LC_MESSAGES/mybot.po + To open .po file you can use basic text editor or any PO editor, e.g. https://poedit.net/ + Step 4: compile translations # pybabel compile -d locales -D mybot @@ -27,7 +32,8 @@ Step 5: When you change the code of your bot you need to update po & mo files. command from step 1 Step 5.2: update po files # pybabel update -d locales -D mybot -i locales/mybot.pot - Step 5.3: update your translations + Step 5.3: update your translations + location and tools you know from step 3 Step 5.4: compile mo files command from step 4 """ @@ -92,5 +98,6 @@ async def cmd_like(message: types.Message, locale): # NOTE: This is comment for a translator await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes)) + if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py index a26fc139..4f05cb22 100644 --- a/tests/test_dispatcher/test_filters/test_builtin.py +++ b/tests/test_dispatcher/test_filters/test_builtin.py @@ -1,12 +1,15 @@ from typing import Set +from datetime import datetime import pytest from aiogram.dispatcher.filters.builtin import ( Text, extract_chat_ids, - ChatIDArgumentType, + ChatIDArgumentType, ForwardedMessageFilter, IDFilter, ) +from aiogram.types import Message +from tests.types.dataset import MESSAGE, MESSAGE_FROM_CHANNEL class TestText: @@ -69,3 +72,39 @@ class TestText: ) def test_extract_chat_ids(chat_id: ChatIDArgumentType, expected: Set[int]): assert extract_chat_ids(chat_id) == expected + + +class TestForwardedMessageFilter: + + @pytest.mark.asyncio + 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) + + @pytest.mark.asyncio + 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) + + +class TestIDFilter: + + @pytest.mark.asyncio + async def test_chat_id_for_channels(self): + message_from_channel = Message(**MESSAGE_FROM_CHANNEL) + + filter = IDFilter(chat_id=message_from_channel.chat.id) + + assert await filter.check(message_from_channel) diff --git a/tests/test_dispatcher/test_handler.py b/tests/test_dispatcher/test_handler.py new file mode 100644 index 00000000..b823c8f8 --- /dev/null +++ b/tests/test_dispatcher/test_handler.py @@ -0,0 +1,66 @@ +import functools + +import pytest + +from aiogram.dispatcher.handler import Handler, _check_spec, _get_spec + + +def callback1(foo: int, bar: int, baz: int): + return locals() + + +async def callback2(foo: int, bar: int, baz: int): + return locals() + + +async def callback3(foo: int, **kwargs): + return locals() + + +class TestHandlerObj: + def test_init_decorated(self): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + @decorator + def callback1(foo, bar, baz): + pass + + @decorator + @decorator + def callback2(foo, bar, baz): + pass + + obj1 = Handler.HandlerObj(callback1, _get_spec(callback1)) + obj2 = Handler.HandlerObj(callback2, _get_spec(callback2)) + + assert set(obj1.spec.args) == {"foo", "bar", "baz"} + assert obj1.handler == callback1 + assert set(obj2.spec.args) == {"foo", "bar", "baz"} + assert obj2.handler == callback2 + + @pytest.mark.parametrize( + "callback,kwargs,result", + [ + pytest.param( + callback1, {"foo": 42, "spam": True, "baz": "fuz"}, {"foo": 42, "baz": "fuz"} + ), + pytest.param( + callback2, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + {"foo": 42, "baz": "fuz", "bar": "test"}, + ), + pytest.param( + callback3, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + ), + ], + ) + def test__check_spec(self, callback, kwargs, result): + spec = _get_spec(callback) + assert _check_spec(spec, kwargs) == result diff --git a/tests/test_utils/test_deprecated.py b/tests/test_utils/test_deprecated.py new file mode 100644 index 00000000..114d6810 --- /dev/null +++ b/tests/test_utils/test_deprecated.py @@ -0,0 +1,14 @@ +import pytest + +from aiogram.utils.deprecated import DeprecatedReadOnlyClassVar + + +def test_DeprecatedReadOnlyClassVarCD(): + assert DeprecatedReadOnlyClassVar.__slots__ == ("_new_value_getter", "_warning_message") + + new_value_of_deprecated_cls_cd = "mpa" + pseudo_owner_cls = type("OpekaCla$$", (), {}) + deprecated_cd = DeprecatedReadOnlyClassVar("mopekaa", lambda owner: new_value_of_deprecated_cls_cd) + + with pytest.warns(DeprecationWarning): + assert deprecated_cd.__get__(None, pseudo_owner_cls) == new_value_of_deprecated_cls_cd 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/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py new file mode 100644 index 00000000..dd0e595d --- /dev/null +++ b/tests/test_utils/test_text_decorations.py @@ -0,0 +1,25 @@ +from aiogram.types import MessageEntity, MessageEntityType +from aiogram.utils import text_decorations + + +class TestTextDecorations: + def test_unparse_entities_normal_text(self): + assert text_decorations.markdown_decoration.unparse( + "hi i'm bold and italic and still bold", + entities=[ + MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), + MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), + ] + ) == "hi *i'm bold _and italic_\r and still bold*" + + def test_unparse_entities_emoji_text(self): + """ + emoji is encoded as two chars in json + """ + assert text_decorations.markdown_decoration.unparse( + "🚀 i'm bold and italic and still bold", + entities=[ + MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), + MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), + ] + ) == "🚀 *i'm bold _and italic_\r and still bold*" diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 310024cb..a14ce316 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -409,6 +409,20 @@ MESSAGE_WITH_VOICE = { "voice": VOICE, } +CHANNEL = { + "type": "channel", + "username": "best_channel_ever", + "id": -1001065170817, +} + +MESSAGE_FROM_CHANNEL = { + "message_id": 123432, + "from": None, + "chat": CHANNEL, + "date": 1508768405, + "text": "Hi, world!", +} + PRE_CHECKOUT_QUERY = { "id": "262181558630368727", "from": USER, @@ -457,3 +471,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