From 79f62f9e613e91f53c646ac2790bc41d58552f3c Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Mon, 6 Apr 2020 18:20:39 +0500 Subject: [PATCH 01/26] Check whether Python is 3.7+ --- aiogram/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 169eed5f..415abb78 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,3 +1,8 @@ +import sys +if sys.version_info < (3, 7): + raise ImportError('Your Python version {0} is not supported by aiogram, please install ' + 'Python 3.7+'.format('.'.join(map(str, sys.version_info[:3])))) + import asyncio import os From 8cd781048ab598254720ffd3806a930d2b2b0ab2 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 7 Apr 2020 09:36:16 +0300 Subject: [PATCH 02/26] #296 Get bot's user_id without get_me --- aiogram/bot/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 6750e8a8..b7015881 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -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 From cd75d3c4685e771e1ed9bf7a57cd4e93d7a8e345 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 7 Apr 2020 09:43:11 +0300 Subject: [PATCH 03/26] #296 Added test for getting bot's user_id --- tests/__init__.py | 3 ++- tests/test_bot.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 3a85ce3f..920d5663 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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): diff --git a/tests/test_bot.py b/tests/test_bot.py index 92a2bd0f..cf1c3c3b 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -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 From 60bcd88b681c76b0345ed0b5a2a2f85f89301c83 Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 3 May 2020 17:26:08 +0300 Subject: [PATCH 04/26] Fix IDFilter behavior when single str id passed --- aiogram/dispatcher/filters/builtin.py | 34 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 0a81998a..2334b343 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -12,6 +12,19 @@ from aiogram.dispatcher.filters.filters import BoundFilter, Filter from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType +IDFilterArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] + + +def extract_filter_ids(id_filter_argument: IDFilterArgumentType) -> typing.List[int]: + # since "str" is also an "Iterable", we have to check for it first + if isinstance(id_filter_argument, str): + return [int(id_filter_argument)] + if isinstance(id_filter_argument, Iterable): + return [int(item) for (item) in id_filter_argument] + # the last possible type is a single "int" + return [id_filter_argument] + + class Command(Filter): """ You can handle commands by using this filter. @@ -543,12 +556,11 @@ class ExceptionsFilter(BoundFilter): except: return False - 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[IDFilterArgumentType] = None, + chat_id: Optional[IDFilterArgumentType] = 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[IDFilterArgumentType] = None + self.chat_id: Optional[IDFilterArgumentType] = 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_filter_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_filter_ids(chat_id) @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: From ac7758aeb03bf186a701fde0dd2bce4254165d82 Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 3 May 2020 18:00:35 +0300 Subject: [PATCH 05/26] Make self.user_id and self.chat_id Set[int] in IDFilter --- aiogram/dispatcher/filters/builtin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 2334b343..2c7bcaea 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -15,14 +15,14 @@ from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType IDFilterArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] -def extract_filter_ids(id_filter_argument: IDFilterArgumentType) -> typing.List[int]: +def extract_filter_ids(id_filter_argument: IDFilterArgumentType) -> typing.Set[int]: # since "str" is also an "Iterable", we have to check for it first if isinstance(id_filter_argument, str): - return [int(id_filter_argument)] + return {int(id_filter_argument),} if isinstance(id_filter_argument, Iterable): - return [int(item) for (item) in id_filter_argument] + return {int(item) for (item) in id_filter_argument} # the last possible type is a single "int" - return [id_filter_argument] + return {id_filter_argument,} class Command(Filter): @@ -569,8 +569,8 @@ 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: Optional[IDFilterArgumentType] = None - self.chat_id: Optional[IDFilterArgumentType] = None + self.user_id: Optional[typing.Set[int]] = None + self.chat_id: Optional[typing.Set[int]] = None if user_id: self.user_id = extract_filter_ids(user_id) From 65184fb126d6f4f4f97a5291bdc2deaaa24625b4 Mon Sep 17 00:00:00 2001 From: uburuntu Date: Tue, 5 May 2020 11:50:41 +0300 Subject: [PATCH 06/26] fix: change user context at poll_answer --- aiogram/dispatcher/dispatcher.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 950ce60f..b485fa49 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -236,6 +236,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) From 689a6cef65bafdf0696325d2a04123714fee673a Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Sat, 9 May 2020 08:59:41 +0300 Subject: [PATCH 07/26] Bug fix There is no method `delete_chat_description` --- aiogram/types/chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 0d3947f6..4a7287d8 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -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: From 6508235d16413d6b35e7a082ac53d41c8d94a689 Mon Sep 17 00:00:00 2001 From: mpa Date: Sun, 10 May 2020 01:13:00 +0400 Subject: [PATCH 08/26] fix(BaseBot): remove __del__ method from BaseBot implement "lazy" session property getter and new get_new_session for BaseBot --- aiogram/bot/base.py | 43 ++++++++++++++---------- tests/test_bot/test_session.py | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 tests/test_bot/test_session.py diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index b7015881..86347e88 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -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 @@ -74,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 @@ -85,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( diff --git a/tests/test_bot/test_session.py b/tests/test_bot/test_session.py new file mode 100644 index 00000000..dec6379c --- /dev/null +++ b/tests/test_bot/test_session.py @@ -0,0 +1,61 @@ +import aiohttp +import aiohttp_socks +import pytest + +from aiogram.bot.base import BaseBot + +try: + from asynctest import CoroutineMock, patch +except ImportError: + from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore + + +class TestAiohttpSession: + @pytest.mark.asyncio + async def test_create_bot(self): + bot = BaseBot(token="42:correct") + + assert bot._session is None + assert isinstance(bot._connector_init, dict) + assert all(key in {"limit", "ssl", "loop"} for key in bot._connector_init) + assert isinstance(bot._connector_class, type) + assert issubclass(bot._connector_class, aiohttp.TCPConnector) + + assert bot._session is None + + assert isinstance(bot.session, aiohttp.ClientSession) + assert bot.session == bot._session + + @pytest.mark.asyncio + async def test_create_proxy_bot(self): + socks_ver, host, port, username, password = ( + "socks5", "124.90.90.90", 9999, "login", "password" + ) + + bot = BaseBot( + token="42:correct", + proxy=f"{socks_ver}://{host}:{port}/", + proxy_auth=aiohttp.BasicAuth(username, password, "encoding"), + ) + + assert bot._connector_class == aiohttp_socks.SocksConnector + + assert isinstance(bot._connector_init, dict) + + init_kwargs = bot._connector_init + assert init_kwargs["username"] == username + assert init_kwargs["password"] == password + assert init_kwargs["host"] == host + assert init_kwargs["port"] == port + + @pytest.mark.asyncio + async def test_close_session(self): + bot = BaseBot(token="42:correct",) + aiohttp_client_0 = bot.session + + with patch("aiohttp.ClientSession.close", new=CoroutineMock()) as mocked_close: + await aiohttp_client_0.close() + mocked_close.assert_called_once() + + await aiohttp_client_0.close() + assert aiohttp_client_0 != bot.session # will create new session From 1e7f07f44326850b4fd2c2658c87833c91a23107 Mon Sep 17 00:00:00 2001 From: Daniil Kovalenko <40635760+WhiteMemory99@users.noreply.github.com> Date: Sun, 10 May 2020 22:40:37 +0700 Subject: [PATCH 09/26] Fix escaping issues in markdown.py --- aiogram/utils/markdown.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index d3c8583b..b56e14b1 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -70,7 +70,7 @@ def bold(*content, sep=" "): :return: """ return markdown_decoration.bold.format( - value=html_decoration.quote(_join(*content, sep=sep)) + value=markdown_decoration.quote(_join(*content, sep=sep)) ) @@ -96,7 +96,7 @@ def italic(*content, sep=" "): :return: """ return markdown_decoration.italic.format( - value=html_decoration.quote(_join(*content, sep=sep)) + value=markdown_decoration.quote(_join(*content, sep=sep)) ) @@ -122,7 +122,7 @@ def code(*content, sep=" "): :return: """ return markdown_decoration.code.format( - value=html_decoration.quote(_join(*content, sep=sep)) + value=markdown_decoration.quote(_join(*content, sep=sep)) ) @@ -148,7 +148,7 @@ def pre(*content, sep="\n"): :return: """ return markdown_decoration.pre.format( - value=html_decoration.quote(_join(*content, sep=sep)) + value=markdown_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.format(value=markdown_decoration.quote(title), link=url) def hlink(title: str, url: str) -> str: From 5db245f0a6f17cf20e4bb81ed4eb244ed133b875 Mon Sep 17 00:00:00 2001 From: Evgeniy Baryshkov Date: Sun, 10 May 2020 20:14:35 +0300 Subject: [PATCH 10/26] fix: python-asyncio version conflict --- dev_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 40a74f81..1cb36784 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -4,7 +4,7 @@ ujson>=1.35 python-rapidjson>=0.7.0 emoji>=0.5.2 pytest>=4.4.1,<4.6 -pytest-asyncio>=0.10.0 +pytest-asyncio==0.10.0 tox>=3.9.0 aresponses>=1.1.1 uvloop>=0.12.2 From 0c149d8b2650576820f1ed61e56b2cc3c5a3ae20 Mon Sep 17 00:00:00 2001 From: Evgeniy Baryshkov Date: Sun, 10 May 2020 23:10:41 +0300 Subject: [PATCH 11/26] Update pytest --- dev_requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 1cb36784..c0c2a39d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -3,8 +3,8 @@ ujson>=1.35 python-rapidjson>=0.7.0 emoji>=0.5.2 -pytest>=4.4.1,<4.6 -pytest-asyncio==0.10.0 +pytest>=5.4 +pytest-asyncio>=0.10.0 tox>=3.9.0 aresponses>=1.1.1 uvloop>=0.12.2 From 15861960f5510f84ad33b48f6caacf812c07d9e9 Mon Sep 17 00:00:00 2001 From: Igor Sereda Date: Tue, 12 May 2020 00:32:45 +0300 Subject: [PATCH 12/26] Fix missing InlineQueryResultPhoto parse_mode field --- aiogram/types/inline_query_result.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py index a80352d7..cf26f8a4 100644 --- a/aiogram/types/inline_query_result.py +++ b/aiogram/types/inline_query_result.py @@ -92,12 +92,13 @@ class InlineQueryResultPhoto(InlineQueryResult): title: typing.Optional[base.String] = None, description: typing.Optional[base.String] = None, caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, reply_markup: typing.Optional[InlineKeyboardMarkup] = None, input_message_content: typing.Optional[InputMessageContent] = None): super(InlineQueryResultPhoto, self).__init__(id=id, photo_url=photo_url, thumb_url=thumb_url, photo_width=photo_width, photo_height=photo_height, title=title, description=description, caption=caption, - reply_markup=reply_markup, + parse_mode=parse_mode, reply_markup=reply_markup, input_message_content=input_message_content) From eb48319f3a39bfdab395b186df4304d658d11e7e Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 17 May 2020 16:22:33 +0300 Subject: [PATCH 13/26] Change name for chat id type and helper to extract it --- aiogram/dispatcher/filters/builtin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 2c7bcaea..9a773e2d 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -12,10 +12,10 @@ from aiogram.dispatcher.filters.filters import BoundFilter, Filter from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType -IDFilterArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] +ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] -def extract_filter_ids(id_filter_argument: IDFilterArgumentType) -> typing.Set[int]: +def extract_chat_ids(id_filter_argument: ChatIDArgumentType) -> typing.Set[int]: # since "str" is also an "Iterable", we have to check for it first if isinstance(id_filter_argument, str): return {int(id_filter_argument),} @@ -559,8 +559,8 @@ class ExceptionsFilter(BoundFilter): class IDFilter(Filter): def __init__(self, - user_id: Optional[IDFilterArgumentType] = None, - chat_id: Optional[IDFilterArgumentType] = None, + user_id: Optional[ChatIDArgumentType] = None, + chat_id: Optional[ChatIDArgumentType] = None, ): """ :param user_id: @@ -573,10 +573,10 @@ class IDFilter(Filter): self.chat_id: Optional[typing.Set[int]] = None if user_id: - self.user_id = extract_filter_ids(user_id) + self.user_id = extract_chat_ids(user_id) if chat_id: - self.chat_id = extract_filter_ids(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]]: From 6d53463880e6b1b8acf02f534b3eb926ac1d478f Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 17 May 2020 16:27:50 +0300 Subject: [PATCH 14/26] Refactor AdminFilter to use extract_chat_ids helper as in IDFilter --- aiogram/dispatcher/filters/builtin.py | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 9a773e2d..a01bac7b 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -15,14 +15,14 @@ from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] -def extract_chat_ids(id_filter_argument: ChatIDArgumentType) -> typing.Set[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(id_filter_argument, str): - return {int(id_filter_argument),} - if isinstance(id_filter_argument, Iterable): - return {int(item) for (item) in id_filter_argument} + 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 {id_filter_argument,} + return {chat_id, } class Command(Filter): @@ -622,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]]: From 6b1c7d3b36a09990b3cb01175b42b1b388ecf015 Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 17 May 2020 16:56:11 +0300 Subject: [PATCH 15/26] Add tests for extract_chat_ids --- .../test_filters/test_builtin.py | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py index 86344cec..a26fc139 100644 --- a/tests/test_dispatcher/test_filters/test_builtin.py +++ b/tests/test_dispatcher/test_filters/test_builtin.py @@ -1,6 +1,12 @@ +from typing import Set + import pytest -from aiogram.dispatcher.filters.builtin import Text +from aiogram.dispatcher.filters.builtin import ( + Text, + extract_chat_ids, + ChatIDArgumentType, +) class TestText: @@ -16,3 +22,50 @@ 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 From 5bbb36372afa93ed3f2acdd4980e0e0f8396179f Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sun, 17 May 2020 19:34:24 +0500 Subject: [PATCH 16/26] Small whitespace fix --- aiogram/dispatcher/filters/builtin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index a01bac7b..5fe01dde 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -556,8 +556,8 @@ class ExceptionsFilter(BoundFilter): except: return False -class IDFilter(Filter): +class IDFilter(Filter): def __init__(self, user_id: Optional[ChatIDArgumentType] = None, chat_id: Optional[ChatIDArgumentType] = None, From 70767111c4ca74961103eae0b39f09f64dd62026 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 31 May 2020 17:49:33 +0300 Subject: [PATCH 17/26] fix: add support init fields from parent object in KeyboardButton (#344) * fix: add support init fields from parent object in KeyboardButton * fix: add tests --- aiogram/types/reply_keyboard.py | 6 ++++-- tests/types/dataset.py | 5 +++++ tests/types/test_reply_keyboard.py | 12 ++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 tests/types/test_reply_keyboard.py diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index ced20417..ffe07ae1 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -111,11 +111,13 @@ class KeyboardButton(base.TelegramObject): def __init__(self, text: base.String, request_contact: base.Boolean = None, request_location: base.Boolean = None, - request_poll: KeyboardButtonPollType = None): + request_poll: KeyboardButtonPollType = None, + **kwargs): super(KeyboardButton, self).__init__(text=text, request_contact=request_contact, request_location=request_location, - request_poll=request_poll) + request_poll=request_poll, + **kwargs) class ReplyKeyboardRemove(base.TelegramObject): diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 310024cb..739e8e2c 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -457,3 +457,8 @@ WEBHOOK_INFO = { "has_custom_certificate": False, "pending_update_count": 0, } + +REPLY_KEYBOARD_MARKUP = { + "keyboard": [[{"text": "something here"}]], + "resize_keyboard": True, +} diff --git a/tests/types/test_reply_keyboard.py b/tests/types/test_reply_keyboard.py new file mode 100644 index 00000000..ae0b6d9e --- /dev/null +++ b/tests/types/test_reply_keyboard.py @@ -0,0 +1,12 @@ +from aiogram import types +from .dataset import REPLY_KEYBOARD_MARKUP + +reply_keyboard = types.ReplyKeyboardMarkup(**REPLY_KEYBOARD_MARKUP) + + +def test_serialize(): + assert reply_keyboard.to_python() == REPLY_KEYBOARD_MARKUP + + +def test_deserialize(): + assert reply_keyboard.to_object(reply_keyboard.to_python()) == reply_keyboard From de13dbf454826f5e2b552db3730605c2f10dc489 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 8 Jun 2020 20:19:05 +0300 Subject: [PATCH 18/26] AIOG-T-61 Telegram Bot API 4.9 --- aiogram/types/dice.py | 1 + aiogram/types/inline_query_result.py | 2 ++ aiogram/types/message.py | 1 + 3 files changed, 4 insertions(+) diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py index 6dfb190f..7b3f1727 100644 --- a/aiogram/types/dice.py +++ b/aiogram/types/dice.py @@ -16,3 +16,4 @@ class Dice(base.TelegramObject): class DiceEmoji: DICE = '🎲' DART = '🎯' + BASKETBALL = '🏀' diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py index cf26f8a4..fccaa2a1 100644 --- a/aiogram/types/inline_query_result.py +++ b/aiogram/types/inline_query_result.py @@ -118,6 +118,7 @@ class InlineQueryResultGif(InlineQueryResult): gif_height: base.Integer = fields.Field() gif_duration: base.Integer = fields.Field() thumb_url: base.String = fields.Field() + thumb_mime_type: base.String = fields.Field() title: base.String = fields.Field() caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) @@ -157,6 +158,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): mpeg4_height: base.Integer = fields.Field() mpeg4_duration: base.Integer = fields.Field() thumb_url: base.String = fields.Field() + thumb_mime_type: base.String = fields.Field() title: base.String = fields.Field() caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index ddbccde6..9b9c0f82 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -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() From 50b5768759102d847efe9381dcd358de30d49cc2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 8 Jun 2020 20:20:43 +0300 Subject: [PATCH 19/26] AIOG-T-61 Bump version --- README.md | 2 +- README.rst | 2 +- aiogram/__init__.py | 4 ++-- aiogram/bot/api.py | 2 +- docs/source/index.rst | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 74de8a8d..dfef918f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index 78dc071c..1cf2765d 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/aiogram/__init__.py b/aiogram/__init__.py index cb339a25..b077ae36 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = [ 'utils' ] -__version__ = '2.8' -__api_version__ = '4.8' +__version__ = '2.9' +__api_version__ = '4.9' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 3b341ec9..1d0c4f7b 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -153,7 +153,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 4.8 + List is updated to Bot API 4.9 """ mode = HelperMode.lowerCamelCase diff --git a/docs/source/index.rst b/docs/source/index.rst index b18d386e..0ac6eccd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From 027139f1b2b7dc553012a4fe2d39f16b0f354d0b Mon Sep 17 00:00:00 2001 From: George Imedashvili Date: Mon, 8 Jun 2020 18:21:15 +0100 Subject: [PATCH 20/26] fix get_full_command (#348) The reason is that .partition() doesn't have a default param as .split has, and default split param gives possibility to split not only by whitespaces, but also whitespace consequences (so the .strip() in get_args() not needed) and newlines. It's called "fix", because without it, commands like this: '''/command arg arg1''' are resulting with ('/command\narg\narg1', '', '') --- aiogram/types/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 9b9c0f82..b9452967 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -168,7 +168,7 @@ 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) return command, args def get_command(self, pure=False): @@ -192,7 +192,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): """ From d5169a294f03c63e62589de2566725b8b7fcc08d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 8 Jun 2020 20:42:38 +0300 Subject: [PATCH 21/26] AIOG-T-23 Backport text_decorations from 3.0a --- aiogram/utils/text_decorations.py | 192 +++++++++++++++++++----------- 1 file changed, 123 insertions(+), 69 deletions(-) diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index ad52c9d7..77dc0ff3 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -1,33 +1,23 @@ from __future__ import annotations + import html import re -import struct -from dataclasses import dataclass -from typing import TYPE_CHECKING, AnyStr, Callable, Generator, Iterable, List, Optional +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Generator, List, Optional, Pattern, cast -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from aiogram.types import MessageEntity __all__ = ( "TextDecoration", + "HtmlDecoration", + "MarkdownDecoration", "html_decoration", "markdown_decoration", - "add_surrogate", - "remove_surrogate", ) -@dataclass -class TextDecoration: - link: str - bold: str - italic: str - code: str - pre: str - underline: str - strikethrough: str - quote: Callable[[AnyStr], AnyStr] - +class TextDecoration(ABC): def apply_entity(self, entity: MessageEntity, text: str) -> str: """ Apply single entity to text @@ -36,24 +26,28 @@ class TextDecoration: :param text: :return: """ - if entity.type in ( - "bold", - "italic", - "code", - "pre", - "underline", - "strikethrough", - ): - return getattr(self, entity.type).format(value=text) - elif entity.type == "text_mention": - return self.link.format(value=text, link=f"tg://user?id={entity.user.id}") - elif entity.type == "text_link": - return self.link.format(value=text, link=entity.url) - elif entity.type == "url": + if entity.type in {"bot_command", "url", "mention", "phone_number"}: + # This entities should not be changed return text + if entity.type in {"bold", "italic", "code", "underline", "strikethrough"}: + return cast(str, getattr(self, entity.type)(value=text)) + if entity.type == "pre": + return ( + self.pre_language(value=text, language=entity.language) + if entity.language + else self.pre(value=text) + ) + if entity.type == "text_mention": + from aiogram.types import User + + user = cast(User, entity.user) + return self.link(value=text, link=f"tg://user?id={user.id}") + if entity.type == "text_link": + return self.link(value=text, link=cast(str, entity.url)) + return self.quote(text) - def unparse(self, text, entities: Optional[List[MessageEntity]] = None) -> str: + def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str: """ Unparse message entities @@ -61,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='{value}', - bold="{value}", - italic="{value}", - code="{value}", - pre="
{value}
", - underline="{value}", - strikethrough="{value}", - 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(" str: + return f'{value}' + + def bold(self, value: str) -> str: + return f"{value}" + + def italic(self, value: str) -> str: + return f"{value}" + + def code(self, value: str) -> str: + return f"{value}" + + def pre(self, value: str) -> str: + return f"
{value}
" + + def pre_language(self, value: str, language: str) -> str: + return f'
{value}
' + + def underline(self, value: str) -> str: + return f"{value}" + + def strikethrough(self, value: str) -> str: + return f"{value}" + + def quote(self, value: str) -> str: + return html.escape(value) -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() From 557147ad8d39ec6d90f36a840c09b458a063e48f Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 9 Jun 2020 16:55:13 +0300 Subject: [PATCH 22/26] fix: markdown helper methods work correctly (#353) * fix: methods in markdown helper work now * chore: add return type annotations --- aiogram/utils/markdown.py | 56 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index b56e14b1..da27bc39 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -18,7 +18,7 @@ HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """} _HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS -def quote_html(*content, sep=" "): +def quote_html(*content, sep=" ") -> str: """ Quote HTML symbols @@ -33,7 +33,7 @@ def quote_html(*content, sep=" "): return html_decoration.quote(_join(*content, sep=sep)) -def escape_md(*content, sep=" "): +def escape_md(*content, sep=" ") -> str: """ Escape markdown text @@ -61,7 +61,7 @@ def text(*content, sep=" "): return _join(*content, sep=sep) -def bold(*content, sep=" "): +def bold(*content, sep=" ") -> str: """ Make bold text (Markdown) @@ -69,12 +69,12 @@ def bold(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.bold.format( + return markdown_decoration.bold( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hbold(*content, sep=" "): +def hbold(*content, sep=" ") -> str: """ Make bold text (HTML) @@ -82,12 +82,12 @@ def hbold(*content, sep=" "): :param sep: :return: """ - return html_decoration.bold.format( + return html_decoration.bold( value=html_decoration.quote(_join(*content, sep=sep)) ) -def italic(*content, sep=" "): +def italic(*content, sep=" ") -> str: """ Make italic text (Markdown) @@ -95,12 +95,12 @@ def italic(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.italic.format( + return markdown_decoration.italic( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hitalic(*content, sep=" "): +def hitalic(*content, sep=" ") -> str: """ Make italic text (HTML) @@ -108,12 +108,12 @@ def hitalic(*content, sep=" "): :param sep: :return: """ - return html_decoration.italic.format( + return html_decoration.italic( value=html_decoration.quote(_join(*content, sep=sep)) ) -def code(*content, sep=" "): +def code(*content, sep=" ") -> str: """ Make mono-width text (Markdown) @@ -121,12 +121,12 @@ def code(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.code.format( + return markdown_decoration.code( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hcode(*content, sep=" "): +def hcode(*content, sep=" ") -> str: """ Make mono-width text (HTML) @@ -134,12 +134,12 @@ def hcode(*content, sep=" "): :param sep: :return: """ - return html_decoration.code.format( + return html_decoration.code( value=html_decoration.quote(_join(*content, sep=sep)) ) -def pre(*content, sep="\n"): +def pre(*content, sep="\n") -> str: """ Make mono-width text block (Markdown) @@ -147,12 +147,12 @@ def pre(*content, sep="\n"): :param sep: :return: """ - return markdown_decoration.pre.format( + return markdown_decoration.pre( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hpre(*content, sep="\n"): +def hpre(*content, sep="\n") -> str: """ Make mono-width text block (HTML) @@ -160,12 +160,12 @@ def hpre(*content, sep="\n"): :param sep: :return: """ - return html_decoration.pre.format( + return html_decoration.pre( value=html_decoration.quote(_join(*content, sep=sep)) ) -def underline(*content, sep=" "): +def underline(*content, sep=" ") -> str: """ Make underlined text (Markdown) @@ -173,12 +173,12 @@ def underline(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.underline.format( + return markdown_decoration.underline( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hunderline(*content, sep=" "): +def hunderline(*content, sep=" ") -> str: """ Make underlined text (HTML) @@ -186,12 +186,12 @@ def hunderline(*content, sep=" "): :param sep: :return: """ - return html_decoration.underline.format( + return html_decoration.underline( value=html_decoration.quote(_join(*content, sep=sep)) ) -def strikethrough(*content, sep=" "): +def strikethrough(*content, sep=" ") -> str: """ Make strikethrough text (Markdown) @@ -199,12 +199,12 @@ def strikethrough(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.strikethrough.format( + return markdown_decoration.strikethrough( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hstrikethrough(*content, sep=" "): +def hstrikethrough(*content, sep=" ") -> str: """ Make strikethrough text (HTML) @@ -212,7 +212,7 @@ def hstrikethrough(*content, sep=" "): :param sep: :return: """ - return html_decoration.strikethrough.format( + return html_decoration.strikethrough( value=html_decoration.quote(_join(*content, sep=sep)) ) @@ -225,7 +225,7 @@ def link(title: str, url: str) -> str: :param url: :return: """ - return markdown_decoration.link.format(value=markdown_decoration.quote(title), link=url) + return markdown_decoration.link(value=markdown_decoration.quote(title), link=url) def hlink(title: str, url: str) -> str: @@ -236,7 +236,7 @@ def hlink(title: str, url: str) -> str: :param url: :return: """ - return html_decoration.link.format(value=html_decoration.quote(title), link=url) + return html_decoration.link(value=html_decoration.quote(title), link=url) def hide_link(url: str) -> str: From a8dfe86358837e909b1e192d7ba6c0ef57541a65 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 10 Jun 2020 23:07:55 +0300 Subject: [PATCH 23/26] feat: ForwardedMessage filter (#355) * feat: ForwardedMessage filter * fix: add tests * fix: attr name --- aiogram/dispatcher/dispatcher.py | 8 +++++- aiogram/dispatcher/filters/__init__.py | 3 ++- aiogram/dispatcher/filters/builtin.py | 10 +++++++ docs/source/dispatcher/filters.rst | 8 ++++++ .../test_filters/test_builtin.py | 27 ++++++++++++++++++- 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index b485fa49..164d6aad 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -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() diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 6de3cc7a..edd1959a 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,6 +1,6 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \ - Text, IDFilter, AdminFilter, IsReplyFilter, 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', ] diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 5fe01dde..c59d9b0d 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -681,3 +681,13 @@ class IsReplyFilter(BoundFilter): return {'reply': msg.reply_to_message} elif not msg.reply_to_message and not self.is_reply: return True + + +class ForwardedMessageFilter(BoundFilter): + key = 'is_forwarded' + + def __init__(self, is_forwarded: bool): + self.is_forwarded = is_forwarded + + async def check(self, message: Message): + return bool(getattr(message, "forward_date")) is self.is_forwarded diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index af06b73e..3681dfcb 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -141,6 +141,14 @@ IsReplyFilter :show-inheritance: +ForwardedMessageFilter +------------- + +.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter + :members: + :show-inheritance: + + Making own filters (Custom filters) =================================== diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py index a26fc139..4cfce465 100644 --- a/tests/test_dispatcher/test_filters/test_builtin.py +++ b/tests/test_dispatcher/test_filters/test_builtin.py @@ -1,12 +1,15 @@ from typing import Set +from datetime import datetime import pytest from aiogram.dispatcher.filters.builtin import ( Text, extract_chat_ids, - ChatIDArgumentType, + ChatIDArgumentType, ForwardedMessageFilter, ) +from aiogram.types import Message +from tests.types.dataset import MESSAGE class TestText: @@ -69,3 +72,25 @@ class TestText: ) 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) From 1389ca587401fd46961397def659f72a4dc6bffd Mon Sep 17 00:00:00 2001 From: Denis Belavin <41421345+LuckyDenis@users.noreply.github.com> Date: Wed, 10 Jun 2020 23:08:44 +0300 Subject: [PATCH 24/26] #320 - Fix: Class InputMediaAudio contains some fields from other class. (#354) Co-authored-by: Belavin Denis --- aiogram/types/input_media.py | 7 ++---- tests/types/test_input_media.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 tests/types/test_input_media.py diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 952e7a55..d42fac99 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -137,8 +137,6 @@ class InputMediaAudio(InputMedia): https://core.telegram.org/bots/api#inputmediaanimation """ - width: base.Integer = fields.Field() - height: base.Integer = fields.Field() duration: base.Integer = fields.Field() performer: base.String = fields.Field() title: base.String = fields.Field() @@ -146,13 +144,12 @@ class InputMediaAudio(InputMedia): def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, caption: base.String = None, - width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None, performer: base.String = None, title: base.String = None, parse_mode: base.String = None, **kwargs): - super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption, - width=width, height=height, duration=duration, + super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, + caption=caption, duration=duration, performer=performer, title=title, parse_mode=parse_mode, conf=kwargs) diff --git a/tests/types/test_input_media.py b/tests/types/test_input_media.py new file mode 100644 index 00000000..953197c9 --- /dev/null +++ b/tests/types/test_input_media.py @@ -0,0 +1,42 @@ +from aiogram import types +from .dataset import AUDIO, ANIMATION, \ + DOCUMENT, PHOTO, VIDEO + + +WIDTH = 'width' +HEIGHT = 'height' + +input_media_audio = types.InputMediaAudio( + types.Audio(**AUDIO)) +input_media_animation = types.InputMediaAnimation( + types.Animation(**ANIMATION)) +input_media_document = types.InputMediaDocument( + types.Document(**DOCUMENT)) +input_media_video = types.InputMediaVideo( + types.Video(**VIDEO)) +input_media_photo = types.InputMediaPhoto( + types.PhotoSize(**PHOTO)) + + +def test_field_width(): + """ + https://core.telegram.org/bots/api#inputmedia + """ + assert not hasattr(input_media_audio, WIDTH) + assert not hasattr(input_media_document, WIDTH) + assert not hasattr(input_media_photo, WIDTH) + + assert hasattr(input_media_animation, WIDTH) + assert hasattr(input_media_video, WIDTH) + + +def test_field_height(): + """ + https://core.telegram.org/bots/api#inputmedia + """ + assert not hasattr(input_media_audio, HEIGHT) + assert not hasattr(input_media_document, HEIGHT) + assert not hasattr(input_media_photo, HEIGHT) + + assert hasattr(input_media_animation, HEIGHT) + assert hasattr(input_media_video, HEIGHT) From 8d30c1dc1b9a494178835ec09e389444cd6fcbbd Mon Sep 17 00:00:00 2001 From: Egor Dementyev Date: Sat, 13 Jun 2020 19:18:30 +0300 Subject: [PATCH 25/26] Fix message.get_full_command() (#352) * Fix message.get_full_command() * Fix message.get_full_command() by JrooTJunior Co-authored-by: Alex Root Junior * fix typos Co-authored-by: Alex Root Junior --- aiogram/types/message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index b9452967..2ea65bfb 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -168,7 +168,8 @@ class Message(base.TelegramObject): :return: tuple of (command, args) """ if self.is_command(): - command, args = self.text.split(maxsplit=1) + command, *args = self.text.split(maxsplit=1) + args = args[0] if args else None return command, args def get_command(self, pure=False): From 21b4b64db1ba5ed8465a32279cd2efd4165b2b44 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 13 Jun 2020 21:30:24 +0300 Subject: [PATCH 26/26] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index b077ae36..0dba109a 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = [ 'utils' ] -__version__ = '2.9' +__version__ = '2.9.1' __api_version__ = '4.9'