mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'dev-2.x'
This commit is contained in:
commit
866011ab2b
26 changed files with 473 additions and 164 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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
import sys
|
||||
if sys.version_info < (3, 7):
|
||||
raise ImportError('Your Python version {0} is not supported by aiogram, please install '
|
||||
'Python 3.7+'.format('.'.join(map(str, sys.version_info[:3]))))
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
|
|
@ -38,5 +43,5 @@ __all__ = [
|
|||
'utils'
|
||||
]
|
||||
|
||||
__version__ = '2.8'
|
||||
__api_version__ = '4.8'
|
||||
__version__ = '2.9.1'
|
||||
__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
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import ssl
|
|||
import typing
|
||||
import warnings
|
||||
from contextvars import ContextVar
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Dict, List, Optional, Union, Type
|
||||
|
||||
import aiohttp
|
||||
import certifi
|
||||
|
|
@ -61,6 +61,7 @@ class BaseBot:
|
|||
api.check_token(token)
|
||||
self._token = None
|
||||
self.__token = token
|
||||
self.id = int(token.split(sep=':')[0])
|
||||
|
||||
self.proxy = proxy
|
||||
self.proxy_auth = proxy_auth
|
||||
|
|
@ -73,6 +74,12 @@ class BaseBot:
|
|||
# aiohttp main session
|
||||
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
self._connector_class: Type[aiohttp.TCPConnector] = aiohttp.TCPConnector
|
||||
self._connector_init = dict(
|
||||
limit=connections_limit, ssl=ssl_context, loop=self.loop
|
||||
)
|
||||
|
||||
if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')):
|
||||
from aiohttp_socks import SocksConnector
|
||||
from aiohttp_socks.utils import parse_proxy_url
|
||||
|
|
@ -84,30 +91,31 @@ class BaseBot:
|
|||
if not password:
|
||||
password = proxy_auth.password
|
||||
|
||||
connector = SocksConnector(socks_ver=socks_ver, host=host, port=port,
|
||||
username=username, password=password,
|
||||
limit=connections_limit, ssl_context=ssl_context,
|
||||
rdns=True, loop=self.loop)
|
||||
|
||||
self._connector_class = SocksConnector
|
||||
self._connector_init.update(
|
||||
socks_ver=socks_ver, host=host, port=port,
|
||||
username=username, password=password, rdns=True,
|
||||
)
|
||||
self.proxy = None
|
||||
self.proxy_auth = None
|
||||
else:
|
||||
connector = aiohttp.TCPConnector(limit=connections_limit, ssl=ssl_context, loop=self.loop)
|
||||
|
||||
self._timeout = None
|
||||
self.timeout = timeout
|
||||
|
||||
self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, json_serialize=json.dumps)
|
||||
|
||||
self.parse_mode = parse_mode
|
||||
|
||||
def __del__(self):
|
||||
if not hasattr(self, 'loop') or not hasattr(self, 'session'):
|
||||
return
|
||||
if self.loop.is_running():
|
||||
self.loop.create_task(self.close())
|
||||
return
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(self.close())
|
||||
def get_new_session(self) -> aiohttp.ClientSession:
|
||||
return aiohttp.ClientSession(
|
||||
connector=self._connector_class(**self._connector_init),
|
||||
loop=self.loop,
|
||||
json_serialize=json.dumps
|
||||
)
|
||||
|
||||
@property
|
||||
def session(self) -> Optional[aiohttp.ClientSession]:
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = self.get_new_session()
|
||||
return self._session
|
||||
|
||||
@staticmethod
|
||||
def _prepare_timeout(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from aiohttp.helpers import sentinel
|
|||
|
||||
from aiogram.utils.deprecated import renamed_argument
|
||||
from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \
|
||||
RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter
|
||||
RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter, ForwardedMessageFilter
|
||||
from .filters.builtin import IsSenderContact
|
||||
from .handler import Handler
|
||||
from .middlewares import MiddlewareManager
|
||||
|
|
@ -160,6 +160,12 @@ 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
|
||||
])
|
||||
|
||||
def __del__(self):
|
||||
self.stop_polling()
|
||||
|
|
@ -236,6 +242,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin):
|
|||
if update.poll:
|
||||
return await self.poll_handlers.notify(update.poll)
|
||||
if update.poll_answer:
|
||||
types.User.set_current(update.poll_answer.user)
|
||||
return await self.poll_answer_handlers.notify(update.poll_answer)
|
||||
except Exception as e:
|
||||
err = await self.errors_handlers.notify(update, e)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
from .factory import FiltersFactory
|
||||
from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \
|
||||
check_filters, get_filter_spec, get_filters_spec
|
||||
|
|
@ -32,4 +32,5 @@ __all__ = [
|
|||
'get_filters_spec',
|
||||
'execute_filter',
|
||||
'check_filters',
|
||||
'ForwardedMessageFilter',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,19 @@ from aiogram.dispatcher.filters.filters import BoundFilter, Filter
|
|||
from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType
|
||||
|
||||
|
||||
ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int]
|
||||
|
||||
|
||||
def extract_chat_ids(chat_id: ChatIDArgumentType) -> typing.Set[int]:
|
||||
# since "str" is also an "Iterable", we have to check for it first
|
||||
if isinstance(chat_id, str):
|
||||
return {int(chat_id), }
|
||||
if isinstance(chat_id, Iterable):
|
||||
return {int(item) for (item) in chat_id}
|
||||
# the last possible type is a single "int"
|
||||
return {chat_id, }
|
||||
|
||||
|
||||
class Command(Filter):
|
||||
"""
|
||||
You can handle commands by using this filter.
|
||||
|
|
@ -545,10 +558,9 @@ class ExceptionsFilter(BoundFilter):
|
|||
|
||||
|
||||
class IDFilter(Filter):
|
||||
|
||||
def __init__(self,
|
||||
user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
|
||||
chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None,
|
||||
user_id: Optional[ChatIDArgumentType] = None,
|
||||
chat_id: Optional[ChatIDArgumentType] = None,
|
||||
):
|
||||
"""
|
||||
:param user_id:
|
||||
|
|
@ -557,18 +569,14 @@ class IDFilter(Filter):
|
|||
if user_id is None and chat_id is None:
|
||||
raise ValueError("Both user_id and chat_id can't be None")
|
||||
|
||||
self.user_id = None
|
||||
self.chat_id = None
|
||||
self.user_id: Optional[typing.Set[int]] = None
|
||||
self.chat_id: Optional[typing.Set[int]] = None
|
||||
|
||||
if user_id:
|
||||
if isinstance(user_id, Iterable):
|
||||
self.user_id = list(map(int, user_id))
|
||||
else:
|
||||
self.user_id = [int(user_id), ]
|
||||
self.user_id = extract_chat_ids(user_id)
|
||||
|
||||
if chat_id:
|
||||
if isinstance(chat_id, Iterable):
|
||||
self.chat_id = list(map(int, chat_id))
|
||||
else:
|
||||
self.chat_id = [int(chat_id), ]
|
||||
self.chat_id = extract_chat_ids(chat_id)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
|
|
@ -614,22 +622,20 @@ class AdminFilter(Filter):
|
|||
is_chat_admin is required for InlineQuery.
|
||||
"""
|
||||
|
||||
def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None):
|
||||
def __init__(self, is_chat_admin: Optional[Union[ChatIDArgumentType, bool]] = None):
|
||||
self._check_current = False
|
||||
self._chat_ids = None
|
||||
|
||||
if is_chat_admin is False:
|
||||
raise ValueError("is_chat_admin cannot be False")
|
||||
|
||||
if is_chat_admin:
|
||||
if isinstance(is_chat_admin, bool):
|
||||
self._check_current = is_chat_admin
|
||||
if isinstance(is_chat_admin, Iterable):
|
||||
self._chat_ids = list(is_chat_admin)
|
||||
else:
|
||||
self._chat_ids = [is_chat_admin]
|
||||
else:
|
||||
if not is_chat_admin:
|
||||
self._check_current = True
|
||||
return
|
||||
|
||||
if isinstance(is_chat_admin, bool):
|
||||
self._check_current = is_chat_admin
|
||||
self._chat_ids = extract_chat_ids(is_chat_admin)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
|
|
@ -675,3 +681,13 @@ class IsReplyFilter(BoundFilter):
|
|||
return {'reply': msg.reply_to_message}
|
||||
elif not msg.reply_to_message and not self.is_reply:
|
||||
return True
|
||||
|
||||
|
||||
class ForwardedMessageFilter(BoundFilter):
|
||||
key = 'is_forwarded'
|
||||
|
||||
def __init__(self, is_forwarded: bool):
|
||||
self.is_forwarded = is_forwarded
|
||||
|
||||
async def check(self, message: Message):
|
||||
return bool(getattr(message, "forward_date")) is self.is_forwarded
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ class Chat(base.TelegramObject):
|
|||
:return: Returns True on success.
|
||||
:rtype: :obj:`base.Boolean`
|
||||
"""
|
||||
return await self.bot.delete_chat_description(self.id, description)
|
||||
return await self.bot.set_chat_description(self.id, description)
|
||||
|
||||
async def kick(self, user_id: base.Integer,
|
||||
until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None) -> base.Boolean:
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ class Dice(base.TelegramObject):
|
|||
class DiceEmoji:
|
||||
DICE = '🎲'
|
||||
DART = '🎯'
|
||||
BASKETBALL = '🏀'
|
||||
|
|
|
|||
|
|
@ -92,12 +92,13 @@ class InlineQueryResultPhoto(InlineQueryResult):
|
|||
title: typing.Optional[base.String] = None,
|
||||
description: typing.Optional[base.String] = None,
|
||||
caption: typing.Optional[base.String] = None,
|
||||
parse_mode: typing.Optional[base.String] = None,
|
||||
reply_markup: typing.Optional[InlineKeyboardMarkup] = None,
|
||||
input_message_content: typing.Optional[InputMessageContent] = None):
|
||||
super(InlineQueryResultPhoto, self).__init__(id=id, photo_url=photo_url, thumb_url=thumb_url,
|
||||
photo_width=photo_width, photo_height=photo_height, title=title,
|
||||
description=description, caption=caption,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode=parse_mode, reply_markup=reply_markup,
|
||||
input_message_content=input_message_content)
|
||||
|
||||
|
||||
|
|
@ -117,6 +118,7 @@ class InlineQueryResultGif(InlineQueryResult):
|
|||
gif_height: base.Integer = fields.Field()
|
||||
gif_duration: base.Integer = fields.Field()
|
||||
thumb_url: base.String = fields.Field()
|
||||
thumb_mime_type: base.String = fields.Field()
|
||||
title: base.String = fields.Field()
|
||||
caption: base.String = fields.Field()
|
||||
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
|
||||
|
|
@ -156,6 +158,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
|
|||
mpeg4_height: base.Integer = fields.Field()
|
||||
mpeg4_duration: base.Integer = fields.Field()
|
||||
thumb_url: base.String = fields.Field()
|
||||
thumb_mime_type: base.String = fields.Field()
|
||||
title: base.String = fields.Field()
|
||||
caption: base.String = fields.Field()
|
||||
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
|
||||
|
|
|
|||
|
|
@ -137,8 +137,6 @@ class InputMediaAudio(InputMedia):
|
|||
https://core.telegram.org/bots/api#inputmediaanimation
|
||||
"""
|
||||
|
||||
width: base.Integer = fields.Field()
|
||||
height: base.Integer = fields.Field()
|
||||
duration: base.Integer = fields.Field()
|
||||
performer: base.String = fields.Field()
|
||||
title: base.String = fields.Field()
|
||||
|
|
@ -146,13 +144,12 @@ class InputMediaAudio(InputMedia):
|
|||
def __init__(self, media: base.InputFile,
|
||||
thumb: typing.Union[base.InputFile, base.String] = None,
|
||||
caption: base.String = None,
|
||||
width: base.Integer = None, height: base.Integer = None,
|
||||
duration: base.Integer = None,
|
||||
performer: base.String = None,
|
||||
title: base.String = None,
|
||||
parse_mode: base.String = None, **kwargs):
|
||||
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
|
||||
width=width, height=height, duration=duration,
|
||||
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb,
|
||||
caption=caption, duration=duration,
|
||||
performer=performer, title=title,
|
||||
parse_mode=parse_mode, conf=kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class Message(base.TelegramObject):
|
|||
forward_signature: base.String = fields.Field()
|
||||
forward_date: datetime.datetime = fields.DateTimeField()
|
||||
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()
|
||||
|
|
@ -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[0] if args else None
|
||||
return command, args
|
||||
|
||||
def get_command(self, pure=False):
|
||||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
value=html_decoration.quote(_join(*content, sep=sep))
|
||||
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(
|
||||
value=html_decoration.quote(_join(*content, sep=sep))
|
||||
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(
|
||||
value=html_decoration.quote(_join(*content, sep=sep))
|
||||
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(
|
||||
value=html_decoration.quote(_join(*content, sep=sep))
|
||||
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=html_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,22 +55,22 @@ 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 []
|
||||
)
|
||||
)
|
||||
return remove_surrogate(result)
|
||||
return result
|
||||
|
||||
def _unparse_entities(
|
||||
self,
|
||||
text: str,
|
||||
entities: Iterable[MessageEntity],
|
||||
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):
|
||||
|
|
@ -88,7 +82,7 @@ class TextDecoration:
|
|||
offset = entity.offset + entity.length
|
||||
|
||||
sub_entities = list(
|
||||
filter(lambda e: e.offset < offset, entities[index + 1 :])
|
||||
filter(lambda e: e.offset < (offset or 0), entities[index + 1 :])
|
||||
)
|
||||
yield self.apply_entity(
|
||||
entity,
|
||||
|
|
@ -102,42 +96,102 @@ class TextDecoration:
|
|||
if offset < length:
|
||||
yield self.quote(text[offset:length])
|
||||
|
||||
@abstractmethod
|
||||
def link(self, value: str, link: 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,
|
||||
)
|
||||
@abstractmethod
|
||||
def bold(self, value: str) -> str: # pragma: no cover
|
||||
pass
|
||||
|
||||
MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-=|{}.!])")
|
||||
@abstractmethod
|
||||
def italic(self, value: str) -> str: # pragma: no cover
|
||||
pass
|
||||
|
||||
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
|
||||
),
|
||||
)
|
||||
@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
|
||||
|
||||
|
||||
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 HtmlDecoration(TextDecoration):
|
||||
def link(self, value: str, link: str) -> str:
|
||||
return f'<a href="{link}">{value}</a>'
|
||||
|
||||
def bold(self, value: str) -> str:
|
||||
return f"<b>{value}</b>"
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def remove_surrogate(text: str) -> str:
|
||||
return text.encode("utf-16", "surrogatepass").decode("utf-16")
|
||||
class MarkdownDecoration(TextDecoration):
|
||||
MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-|{}.!])")
|
||||
|
||||
def link(self, value: str, link: str) -> str:
|
||||
return f"[{value}]({link})"
|
||||
|
||||
def bold(self, value: str) -> str:
|
||||
return f"*{value}*"
|
||||
|
||||
def italic(self, value: str) -> str:
|
||||
return f"_{value}_\r"
|
||||
|
||||
def code(self, value: str) -> str:
|
||||
return f"`{value}`"
|
||||
|
||||
def pre(self, value: str) -> str:
|
||||
return f"```{value}```"
|
||||
|
||||
def pre_language(self, value: str, language: str) -> str:
|
||||
return f"```{language}\n{value}\n```"
|
||||
|
||||
def underline(self, value: str) -> str:
|
||||
return f"__{value}__"
|
||||
|
||||
def strikethrough(self, value: str) -> str:
|
||||
return f"~{value}~"
|
||||
|
||||
def quote(self, value: str) -> str:
|
||||
return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value)
|
||||
|
||||
|
||||
html_decoration = HtmlDecoration()
|
||||
markdown_decoration = MarkdownDecoration()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
ujson>=1.35
|
||||
python-rapidjson>=0.7.0
|
||||
emoji>=0.5.2
|
||||
pytest>=4.4.1,<4.6
|
||||
pytest>=5.4
|
||||
pytest-asyncio>=0.10.0
|
||||
tox>=3.9.0
|
||||
aresponses>=1.1.1
|
||||
|
|
|
|||
|
|
@ -141,6 +141,14 @@ IsReplyFilter
|
|||
:show-inheritance:
|
||||
|
||||
|
||||
ForwardedMessageFilter
|
||||
-------------
|
||||
|
||||
.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter
|
||||
: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
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import aresponses
|
|||
|
||||
from aiogram import Bot
|
||||
|
||||
TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890'
|
||||
BOT_ID = 123456789
|
||||
TOKEN = f'{BOT_ID}:AABBCCDDEEFFaabbccddeeff-1234567890'
|
||||
|
||||
|
||||
class FakeTelegram(aresponses.ResponsesMockServer):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from aiogram import Bot, types
|
||||
from . import FakeTelegram, TOKEN
|
||||
from . import FakeTelegram, TOKEN, BOT_ID
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
|
@ -525,3 +525,9 @@ async def test_set_sticker_set_thumb(bot: Bot, event_loop):
|
|||
result = await bot.set_sticker_set_thumb(name='test', user_id=123456789, thumb='file_id')
|
||||
assert isinstance(result, bool)
|
||||
assert result is True
|
||||
|
||||
|
||||
async def test_bot_id(bot: Bot):
|
||||
""" Check getting id from token. """
|
||||
bot = Bot(TOKEN)
|
||||
assert bot.id == BOT_ID # BOT_ID is a correct id from TOKEN
|
||||
|
|
|
|||
61
tests/test_bot/test_session.py
Normal file
61
tests/test_bot/test_session.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import aiohttp
|
||||
import aiohttp_socks
|
||||
import pytest
|
||||
|
||||
from aiogram.bot.base import BaseBot
|
||||
|
||||
try:
|
||||
from asynctest import CoroutineMock, patch
|
||||
except ImportError:
|
||||
from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore
|
||||
|
||||
|
||||
class TestAiohttpSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_bot(self):
|
||||
bot = BaseBot(token="42:correct")
|
||||
|
||||
assert bot._session is None
|
||||
assert isinstance(bot._connector_init, dict)
|
||||
assert all(key in {"limit", "ssl", "loop"} for key in bot._connector_init)
|
||||
assert isinstance(bot._connector_class, type)
|
||||
assert issubclass(bot._connector_class, aiohttp.TCPConnector)
|
||||
|
||||
assert bot._session is None
|
||||
|
||||
assert isinstance(bot.session, aiohttp.ClientSession)
|
||||
assert bot.session == bot._session
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_proxy_bot(self):
|
||||
socks_ver, host, port, username, password = (
|
||||
"socks5", "124.90.90.90", 9999, "login", "password"
|
||||
)
|
||||
|
||||
bot = BaseBot(
|
||||
token="42:correct",
|
||||
proxy=f"{socks_ver}://{host}:{port}/",
|
||||
proxy_auth=aiohttp.BasicAuth(username, password, "encoding"),
|
||||
)
|
||||
|
||||
assert bot._connector_class == aiohttp_socks.SocksConnector
|
||||
|
||||
assert isinstance(bot._connector_init, dict)
|
||||
|
||||
init_kwargs = bot._connector_init
|
||||
assert init_kwargs["username"] == username
|
||||
assert init_kwargs["password"] == password
|
||||
assert init_kwargs["host"] == host
|
||||
assert init_kwargs["port"] == port
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_session(self):
|
||||
bot = BaseBot(token="42:correct",)
|
||||
aiohttp_client_0 = bot.session
|
||||
|
||||
with patch("aiohttp.ClientSession.close", new=CoroutineMock()) as mocked_close:
|
||||
await aiohttp_client_0.close()
|
||||
mocked_close.assert_called_once()
|
||||
|
||||
await aiohttp_client_0.close()
|
||||
assert aiohttp_client_0 != bot.session # will create new session
|
||||
|
|
@ -1,6 +1,15 @@
|
|||
from typing import Set
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from aiogram.dispatcher.filters.builtin import Text
|
||||
from aiogram.dispatcher.filters.builtin import (
|
||||
Text,
|
||||
extract_chat_ids,
|
||||
ChatIDArgumentType, ForwardedMessageFilter,
|
||||
)
|
||||
from aiogram.types import Message
|
||||
from tests.types.dataset import MESSAGE
|
||||
|
||||
|
||||
class TestText:
|
||||
|
|
@ -16,3 +25,72 @@ class TestText:
|
|||
config = {param: value}
|
||||
res = Text.validate(config)
|
||||
assert res == {key: value}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('chat_id', 'expected'),
|
||||
(
|
||||
pytest.param('-64856280', {-64856280,}, id='single negative int as string'),
|
||||
pytest.param('64856280', {64856280,}, id='single positive int as string'),
|
||||
pytest.param(-64856280, {-64856280,}, id='single negative int'),
|
||||
pytest.param(64856280, {64856280,}, id='single positive negative int'),
|
||||
pytest.param(
|
||||
['-64856280'], {-64856280,}, id='list of single negative int as string'
|
||||
),
|
||||
pytest.param([-64856280], {-64856280,}, id='list of single negative int'),
|
||||
pytest.param(
|
||||
['-64856280', '-64856280'],
|
||||
{-64856280,},
|
||||
id='list of two duplicated negative ints as strings',
|
||||
),
|
||||
pytest.param(
|
||||
['-64856280', -64856280],
|
||||
{-64856280,},
|
||||
id='list of one negative int as string and one negative int',
|
||||
),
|
||||
pytest.param(
|
||||
[-64856280, -64856280],
|
||||
{-64856280,},
|
||||
id='list of two duplicated negative ints',
|
||||
),
|
||||
pytest.param(
|
||||
iter(['-64856280']),
|
||||
{-64856280,},
|
||||
id='iterator from a list of single negative int as string',
|
||||
),
|
||||
pytest.param(
|
||||
[10000000, 20000000, 30000000],
|
||||
{10000000, 20000000, 30000000},
|
||||
id='list of several positive ints',
|
||||
),
|
||||
pytest.param(
|
||||
[10000000, '20000000', -30000000],
|
||||
{10000000, 20000000, -30000000},
|
||||
id='list of positive int, positive int as string, negative int',
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_extract_chat_ids(chat_id: ChatIDArgumentType, expected: Set[int]):
|
||||
assert extract_chat_ids(chat_id) == expected
|
||||
|
||||
|
||||
class TestForwardedMessageFilter:
|
||||
async def test_filter_forwarded_messages(self):
|
||||
filter = ForwardedMessageFilter(is_forwarded=True)
|
||||
|
||||
forwarded_message = Message(forward_date=round(datetime(2020, 5, 21, 5, 1).timestamp()), **MESSAGE)
|
||||
|
||||
not_forwarded_message = Message(**MESSAGE)
|
||||
|
||||
assert await filter.check(forwarded_message)
|
||||
assert not await filter.check(not_forwarded_message)
|
||||
|
||||
async def test_filter_not_forwarded_messages(self):
|
||||
filter = ForwardedMessageFilter(is_forwarded=False)
|
||||
|
||||
forwarded_message = Message(forward_date=round(datetime(2020, 5, 21, 5, 1).timestamp()), **MESSAGE)
|
||||
|
||||
not_forwarded_message = Message(**MESSAGE)
|
||||
|
||||
assert await filter.check(not_forwarded_message)
|
||||
assert not await filter.check(forwarded_message)
|
||||
|
|
|
|||
|
|
@ -457,3 +457,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