mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'dev-2.x' into aiov2
This commit is contained in:
commit
9fe12e3aea
42 changed files with 1719 additions and 707 deletions
|
|
@ -6,7 +6,7 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](https://core.telegram.org/bots/api)
|
||||
[](http://docs.aiogram.dev/en/latest/?badge=latest)
|
||||
[](https://github.com/aiogram/aiogram/issues)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -43,5 +43,5 @@ __all__ = [
|
|||
'utils'
|
||||
]
|
||||
|
||||
__version__ = '2.8'
|
||||
__api_version__ = '4.8'
|
||||
__version__ = '2.9.2'
|
||||
__api_version__ = '4.9'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
"""
|
||||
This module has mongo storage for finite-state machine
|
||||
based on `aiomongo <https://github.com/ZeoAlliance/aiomongo`_ driver
|
||||
based on `motor <https://github.com/mongodb/motor>`_ 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ class Dice(base.TelegramObject):
|
|||
class DiceEmoji:
|
||||
DICE = '🎲'
|
||||
DART = '🎯'
|
||||
BASKETBALL = '🏀'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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='<a href="{link}">{value}</a>',
|
||||
bold="<b>{value}</b>",
|
||||
italic="<i>{value}</i>",
|
||||
code="<code>{value}</code>",
|
||||
pre="<pre>{value}</pre>",
|
||||
underline="<u>{value}</u>",
|
||||
strikethrough="<s>{value}</s>",
|
||||
quote=html.escape,
|
||||
)
|
||||
class HtmlDecoration(TextDecoration):
|
||||
def link(self, value: str, link: str) -> str:
|
||||
return f'<a href="{link}">{value}</a>'
|
||||
|
||||
MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-=|{}.!])")
|
||||
def bold(self, value: str) -> str:
|
||||
return f"<b>{value}</b>"
|
||||
|
||||
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"<i>{value}</i>"
|
||||
|
||||
def code(self, value: str) -> str:
|
||||
return f"<code>{value}</code>"
|
||||
|
||||
def pre(self, value: str) -> str:
|
||||
return f"<pre>{value}</pre>"
|
||||
|
||||
def pre_language(self, value: str, language: str) -> str:
|
||||
return f'<pre><code class="language-{language}">{value}</code></pre>'
|
||||
|
||||
def underline(self, value: str) -> str:
|
||||
return f"<u>{value}</u>"
|
||||
|
||||
def strikethrough(self, value: str) -> str:
|
||||
return f"<s>{value}</s>"
|
||||
|
||||
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("<HH", s.encode("utf-16-le")))
|
||||
if (0x10000 <= ord(s) <= 0x10FFFF)
|
||||
else s
|
||||
for s in text
|
||||
)
|
||||
class MarkdownDecoration(TextDecoration):
|
||||
MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")
|
||||
|
||||
def link(self, value: str, link: str) -> str:
|
||||
return f"[{value}]({link})"
|
||||
|
||||
def bold(self, value: str) -> str:
|
||||
return f"*{value}*"
|
||||
|
||||
def italic(self, value: str) -> str:
|
||||
return f"_{value}_\r"
|
||||
|
||||
def code(self, value: str) -> str:
|
||||
return f"`{value}`"
|
||||
|
||||
def pre(self, value: str) -> str:
|
||||
return f"```{value}```"
|
||||
|
||||
def pre_language(self, value: str, language: str) -> str:
|
||||
return f"```{language}\n{value}\n```"
|
||||
|
||||
def underline(self, value: str) -> str:
|
||||
return f"__{value}__"
|
||||
|
||||
def strikethrough(self, value: str) -> str:
|
||||
return f"~{value}~"
|
||||
|
||||
def quote(self, value: str) -> str:
|
||||
return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value)
|
||||
|
||||
|
||||
def remove_surrogate(text: str) -> str:
|
||||
return text.encode("utf-16", "surrogatepass").decode("utf-16")
|
||||
html_decoration = HtmlDecoration()
|
||||
markdown_decoration = MarkdownDecoration()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
===================================
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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-
|
||||
|
|
|
|||
|
|
@ -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__':
|
||||
|
|
|
|||
|
|
@ -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__':
|
||||
|
|
|
|||
42
examples/chat_type_filter.py
Normal file
42
examples/chat_type_filter.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
66
tests/test_dispatcher/test_handler.py
Normal file
66
tests/test_dispatcher/test_handler.py
Normal file
|
|
@ -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
|
||||
14
tests/test_utils/test_deprecated.py
Normal file
14
tests/test_utils/test_deprecated.py
Normal file
|
|
@ -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
|
||||
11
tests/test_utils/test_markdown.py
Normal file
11
tests/test_utils/test_markdown.py
Normal file
|
|
@ -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\\\."
|
||||
25
tests/test_utils/test_text_decorations.py
Normal file
25
tests/test_utils/test_text_decorations.py
Normal file
|
|
@ -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*"
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
42
tests/types/test_input_media.py
Normal file
42
tests/types/test_input_media.py
Normal file
|
|
@ -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)
|
||||
12
tests/types/test_reply_keyboard.py
Normal file
12
tests/types/test_reply_keyboard.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue