From 35b0e150c2bff884e83e5d0138caf51fa43825ab Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 9 Jun 2019 21:54:53 +0300 Subject: [PATCH 001/128] Change `deprecated until` versions --- aiogram/types/chat_member.py | 6 +++--- aiogram/types/message.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 6679c5e0..12789462 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -33,7 +33,7 @@ class ChatMember(base.TelegramObject): def is_admin(self): warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.2', + 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', DeprecationWarning, stacklevel=2) return self.is_chat_admin() @@ -63,14 +63,14 @@ class ChatMemberStatus(helper.Helper): @classmethod def is_admin(cls, role): warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.2', + 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', DeprecationWarning, stacklevel=2) return cls.is_chat_admin(role) @classmethod def is_member(cls, role): warnings.warn('`is_member` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_member` and will be available until aiogram 2.2', + 'This method renamed to `is_chat_member` and will be available until aiogram 2.3', DeprecationWarning, stacklevel=2) return cls.is_chat_member(role) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index a8e65a32..cafb8116 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1008,7 +1008,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - warn_deprecated('"Message.send_animation" method will be removed in 2.2 version.\n' + warn_deprecated('"Message.send_animation" method will be removed in 2.3 version.\n' 'Use "Message.reply_animation" instead.', stacklevel=8) @@ -1358,7 +1358,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - warn_deprecated('"Message.send_venue" method will be removed in 2.2 version.\n' + warn_deprecated('"Message.send_venue" method will be removed in 2.3 version.\n' 'Use "Message.reply_venue" instead.', stacklevel=8) @@ -1446,7 +1446,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - warn_deprecated('"Message.send_contact" method will be removed in 2.2 version.\n' + warn_deprecated('"Message.send_contact" method will be removed in 2.3 version.\n' 'Use "Message.reply_contact" instead.', stacklevel=8) From 2083bfb965770fb47f041c4d8b93fba802e4ff45 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 9 Jun 2019 21:55:06 +0300 Subject: [PATCH 002/128] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 6fe7d1c6..c2dd8f24 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.2.dev1' +__version__ = '2.2' __api_version__ = '4.3' From 9b9c1b086da0d6d033247261f55df7d934d96c5d Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 9 Jun 2019 21:55:41 +0300 Subject: [PATCH 003/128] Remove unused import --- aiogram/types/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index cafb8116..7637cf42 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -14,7 +14,7 @@ from .contact import Contact from .document import Document from .force_reply import ForceReply from .game import Game -from .inline_keyboard import InlineKeyboardMarkup, InlineKeyboardButton +from .inline_keyboard import InlineKeyboardMarkup from .input_media import MediaGroup, InputMedia from .invoice import Invoice from .location import Location From 5b9d82f1ca5e0214bc43a75442bf4da4d358ffc0 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Mon, 10 Jun 2019 20:58:02 +0300 Subject: [PATCH 004/128] Bump dev version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index c2dd8f24..a1c2736b 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.2' +__version__ = '2.2.1.dev1' __api_version__ = '4.3' From 550c41e1752aa08c493d7cb4ec5fec402d8e849c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 11 Jun 2019 15:09:39 +0300 Subject: [PATCH 005/128] Update FUNDING.yml Cleanup --- .github/FUNDING.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ab12d702..82ea7257 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,8 +1,2 @@ -# These are supported funding model platforms - -github: [JRootJunior]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: aiogram # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -custom: # Replace with a single custom sponsorship URL +github: [JRootJunior] +open_collective: aiogram From 61d5c3a41a696673ddd940a0c0eb9f46fcb60cad Mon Sep 17 00:00:00 2001 From: "bohdan.lushchyk" Date: Fri, 21 Jun 2019 15:06:36 +0300 Subject: [PATCH 006/128] data to filter from middlwares --- aiogram/dispatcher/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 17b715d1..859cb47e 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -105,7 +105,7 @@ class Handler: try: for handler_obj in self.handlers: try: - data.update(await check_filters(handler_obj.filters, args)) + data.update(await check_filters(handler_obj.filters, args + (data,))) except FilterNotPassed: continue else: From 0b125014980a2c0da1d767c212581d6416f271ba Mon Sep 17 00:00:00 2001 From: Kylmakalle Date: Wed, 26 Jun 2019 00:04:30 +0300 Subject: [PATCH 007/128] Update i18n example --- examples/i18n_example.py | 28 ++++++++++++++++++++++++ examples/locales/mybot.pot | 4 ++++ examples/locales/ru/LC_MESSAGES/mybot.po | 5 +++++ 3 files changed, 37 insertions(+) diff --git a/examples/i18n_example.py b/examples/i18n_example.py index 6469ed5b..bf23c8d1 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -3,6 +3,19 @@ Internalize your bot Step 1: extract texts # pybabel extract i18n_example.py -o locales/mybot.pot + + Some useful options: + - Extract texts with pluralization support + # -k __:1,2 + - Add comments for translators, you can use another tag if you want (TR) + # --add-comments=NOTE + - Disable comments with string location in code + # --no-location + - Set project name + # --project=MySuperBot + - Set version + # --version=2.2 + Step 2: create *.po files. For e.g. create en, ru, uk locales. # echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l Step 3: translate texts @@ -51,6 +64,21 @@ async def cmd_start(message: types.Message): async def cmd_lang(message: types.Message, locale): await message.reply(_('Your current language: {language}').format(language=locale)) +# If you care about pluralization, here's small handler +# And also, there's and example of comments for translators. Most translation tools support them. + +# Alias for gettext method, parser will understand double underscore as plural (aka ngettext) +__ = i18n.gettext + +# Some pseudo numeric value +TOTAL_LIKES = 0 + +@dp.message_handler(commands=['like']) +async def cmd_like(message: types.Message, locale): + TOTAL_LIKES += 1 + + # NOTE: This is comment for a translator + await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', TOTAL_LIKES).format(number=TOTAL_LIKES)) if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) diff --git a/examples/locales/mybot.pot b/examples/locales/mybot.pot index 988ed463..b0736569 100644 --- a/examples/locales/mybot.pot +++ b/examples/locales/mybot.pot @@ -25,3 +25,7 @@ msgstr "" msgid "Your current language: {language}" msgstr "" +msgid "Aiogram has {number} like!" +msgid_plural "Aiogram has {number} likes!" +msgstr[0] "" +msgstr[1] "" diff --git a/examples/locales/ru/LC_MESSAGES/mybot.po b/examples/locales/ru/LC_MESSAGES/mybot.po index 73876f30..8180af42 100644 --- a/examples/locales/ru/LC_MESSAGES/mybot.po +++ b/examples/locales/ru/LC_MESSAGES/mybot.po @@ -27,3 +27,8 @@ msgstr "Привет, {user}!" msgid "Your current language: {language}" msgstr "Твой язык: {language}" +msgid "Aiogram has {number} like!" +msgid_plural "Aiogram has {number} likes!" +msgstr[0] "Aiogram имеет {number} лайк!" +msgstr[1] "Aiogram имеет {number} лайка!" +msgstr[2] "Aiogram имеет {number} лайков!" \ No newline at end of file From cf95a9080c886352548c55b62f552f66ade0075a Mon Sep 17 00:00:00 2001 From: Ilya Makarov Date: Wed, 26 Jun 2019 22:28:39 +0300 Subject: [PATCH 008/128] Fix #143: Imposible to download file into directory --- aiogram/types/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/mixins.py b/aiogram/types/mixins.py index f11a1760..13f8412f 100644 --- a/aiogram/types/mixins.py +++ b/aiogram/types/mixins.py @@ -24,7 +24,7 @@ class Downloadable: if destination is None: destination = file.file_path elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination): - os.path.join(destination, file.file_path) + destination = os.path.join(destination, file.file_path) else: is_path = False From c6accd1a53d43e4cd63c105a370a4830bd168b4a Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 19:34:38 +0500 Subject: [PATCH 009/128] Add "expire" argument Add "expire" argument to all set_ and update_ methods in RedisStorage2 --- aiogram/contrib/fsm_storage/redis.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index c3a91f00..6b96869b 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -287,29 +287,29 @@ class RedisStorage2(BaseStorage): return default or {} async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - state: typing.Optional[typing.AnyStr] = None): + state: typing.Optional[typing.AnyStr] = None, expire: int = 0): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) redis = await self.redis() if state is None: await redis.delete(key) else: - await redis.set(key, state) + await redis.set(key, state, expire=expire) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None): + data: typing.Dict = None, expire: int = 0): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_DATA_KEY) redis = await self.redis() - await redis.set(key, json.dumps(data)) + await redis.set(key, json.dumps(data), expire=expire) async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None, **kwargs): + data: typing.Dict = None, expire: int = 0, **kwargs): if data is None: data = {} temp_data = await self.get_data(chat=chat, user=user, default={}) temp_data.update(data, **kwargs) - await self.set_data(chat=chat, user=user, data=temp_data) + await self.set_data(chat=chat, user=user, data=temp_data, expire=expire) def has_bucket(self): return True @@ -325,20 +325,20 @@ class RedisStorage2(BaseStorage): return default or {} async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None): + bucket: typing.Dict = None, expire: int = 0): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_BUCKET_KEY) redis = await self.redis() - await redis.set(key, json.dumps(bucket)) + await redis.set(key, json.dumps(bucket), expire=expire) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, **kwargs): + bucket: typing.Dict = None, expire: int = 0, **kwargs): if bucket is None: bucket = {} temp_bucket = await self.get_bucket(chat=chat, user=user) temp_bucket.update(bucket, **kwargs) - await self.set_bucket(chat=chat, user=user, data=temp_bucket) + await self.set_bucket(chat=chat, user=user, data=temp_bucket, expire=expire) async def reset_all(self, full=True): """ From e2842944fa1e051648341b07cb920489f4f55e29 Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 20:39:26 +0500 Subject: [PATCH 010/128] Update redis.py Update docstrings --- aiogram/contrib/fsm_storage/redis.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 6b96869b..e75e7f40 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -62,8 +62,6 @@ class RedisStorage(BaseStorage): async def redis(self) -> aioredis.RedisConnection: """ Get Redis connection - - This property is awaitable. """ # Use thread-safe asyncio Lock because this method without that is not safe async with self._connection_lock: @@ -241,8 +239,6 @@ class RedisStorage2(BaseStorage): async def redis(self) -> aioredis.Redis: """ Get Redis connection - - This property is awaitable. """ # Use thread-safe asyncio Lock because this method without that is not safe async with self._connection_lock: From 8a819eb7ed0758f7342e234c1b10f6c9282e72f1 Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 21:00:25 +0500 Subject: [PATCH 011/128] Fix update_bucket --- aiogram/contrib/fsm_storage/redis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index e75e7f40..0e89eaea 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -334,7 +334,7 @@ class RedisStorage2(BaseStorage): bucket = {} temp_bucket = await self.get_bucket(chat=chat, user=user) temp_bucket.update(bucket, **kwargs) - await self.set_bucket(chat=chat, user=user, data=temp_bucket, expire=expire) + await self.set_bucket(chat=chat, user=user, bucket=temp_bucket, expire=expire) async def reset_all(self, full=True): """ From cbb019283a9b5776ffcb7f86093421551bfc0bdc Mon Sep 17 00:00:00 2001 From: Gabben Date: Fri, 28 Jun 2019 14:35:49 +0500 Subject: [PATCH 012/128] ChatMember update Deprecated methods removed Added new RESTRICTED status is_chat_member method has been updated: user could be restricted but remain a member. Added tests for chat_member --- aiogram/types/chat_member.py | 39 +++++-------------- tests/types/test_chat_member.py | 68 +++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 tests/types/test_chat_member.py diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 12789462..288b6d76 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,5 +1,6 @@ import datetime import warnings +from typing import Optional from . import base from . import fields @@ -31,19 +32,13 @@ class ChatMember(base.TelegramObject): can_send_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() - def is_admin(self): - warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', - DeprecationWarning, stacklevel=2) - return self.is_chat_admin() + def is_chat_admin(self) -> bool: + return ChatMemberStatus.is_chat_admin(self.status) - def is_chat_admin(self): - return ChatMemberStatus.is_admin(self.status) + def is_chat_member(self) -> bool: + return ChatMemberStatus.is_chat_member(self.status, self.is_member) - def is_chat_member(self): - return ChatMemberStatus.is_member(self.status) - - def __int__(self): + def __int__(self) -> int: return self.user.id @@ -51,33 +46,19 @@ class ChatMemberStatus(helper.Helper): """ Chat member status """ - mode = helper.HelperMode.lowercase CREATOR = helper.Item() # creator ADMINISTRATOR = helper.Item() # administrator MEMBER = helper.Item() # member + RESTRICTED = helper.Item() # restricted LEFT = helper.Item() # left KICKED = helper.Item() # kicked @classmethod - def is_admin(cls, role): - warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', - DeprecationWarning, stacklevel=2) - return cls.is_chat_admin(role) - - @classmethod - def is_member(cls, role): - warnings.warn('`is_member` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_member` and will be available until aiogram 2.3', - DeprecationWarning, stacklevel=2) - return cls.is_chat_member(role) - - @classmethod - def is_chat_admin(cls, role): + def is_chat_admin(cls, role: str) -> bool: return role in [cls.ADMINISTRATOR, cls.CREATOR] @classmethod - def is_chat_member(cls, role): - return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR] + def is_chat_member(cls, role: str, is_member: Optional[bool] = None) -> bool: + return (role == cls.RESTRICTED and is_member is True) or role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR] diff --git a/tests/types/test_chat_member.py b/tests/types/test_chat_member.py new file mode 100644 index 00000000..62fad60f --- /dev/null +++ b/tests/types/test_chat_member.py @@ -0,0 +1,68 @@ +from aiogram import types +from .dataset import CHAT_MEMBER + +chat_member = types.ChatMember(**CHAT_MEMBER) + + +def test_export(): + exported = chat_member.to_python() + assert isinstance(exported, dict) + assert exported == CHAT_MEMBER + + +def test_user(): + assert isinstance(chat_member.user, types.User) + + +def test_status(): + assert isinstance(chat_member.status, str) + assert chat_member.status == CHAT_MEMBER['status'] + + +def test_privileges(): + assert isinstance(chat_member.can_be_edited, bool) + assert chat_member.can_be_edited == CHAT_MEMBER['can_be_edited'] + + assert isinstance(chat_member.can_change_info, bool) + assert chat_member.can_change_info == CHAT_MEMBER['can_change_info'] + + assert isinstance(chat_member.can_delete_messages, bool) + assert chat_member.can_delete_messages == CHAT_MEMBER['can_delete_messages'] + + assert isinstance(chat_member.can_invite_users, bool) + assert chat_member.can_invite_users == CHAT_MEMBER['can_invite_users'] + + assert isinstance(chat_member.can_restrict_members, bool) + assert chat_member.can_restrict_members == CHAT_MEMBER['can_restrict_members'] + + assert isinstance(chat_member.can_pin_messages, bool) + assert chat_member.can_pin_messages == CHAT_MEMBER['can_pin_messages'] + + assert isinstance(chat_member.can_promote_members, bool) + assert chat_member.can_promote_members == CHAT_MEMBER['can_promote_members'] + + +def test_int(): + assert int(chat_member) == chat_member.user.id + assert isinstance(int(chat_member), int) + + +def test_chat_member_status(): + assert types.ChatMemberStatus.CREATOR == 'creator' + assert types.ChatMemberStatus.ADMINISTRATOR == 'administrator' + assert types.ChatMemberStatus.MEMBER == 'member' + assert types.ChatMemberStatus.RESTRICTED == 'restricted' + assert types.ChatMemberStatus.LEFT == 'left' + assert types.ChatMemberStatus.KICKED == 'kicked' + + +def test_chat_member_status_filters(): + assert types.ChatMemberStatus.is_chat_admin(chat_member.status) + assert types.ChatMemberStatus.is_chat_member(chat_member.status) + assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED, True) + assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED, False) + + +def test_chat_member_filters(): + assert chat_member.is_chat_admin() + assert chat_member.is_chat_member() From a00b854533fc2bac326cacdb790e0b2ed541801f Mon Sep 17 00:00:00 2001 From: Gabben Date: Fri, 28 Jun 2019 14:46:56 +0500 Subject: [PATCH 013/128] Update test_chat_member.py --- tests/types/test_chat_member.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/types/test_chat_member.py b/tests/types/test_chat_member.py index 62fad60f..97e365ec 100644 --- a/tests/types/test_chat_member.py +++ b/tests/types/test_chat_member.py @@ -59,8 +59,19 @@ def test_chat_member_status(): def test_chat_member_status_filters(): assert types.ChatMemberStatus.is_chat_admin(chat_member.status) assert types.ChatMemberStatus.is_chat_member(chat_member.status) + + assert types.ChatMemberStatus.is_chat_admin(types.ChatMemberStatus.CREATOR) + assert types.ChatMemberStatus.is_chat_admin(types.ChatMemberStatus.ADMINISTRATOR) + + assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.CREATOR) + assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.ADMINISTRATOR) + assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.MEMBER) assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED, True) + + assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED) assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED, False) + assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.LEFT) + assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.KICKED) def test_chat_member_filters(): From 607e3ea13514942bc10192a81604e00404f209c8 Mon Sep 17 00:00:00 2001 From: Gabben Date: Fri, 28 Jun 2019 15:26:07 +0500 Subject: [PATCH 014/128] Update redis.py --- aiogram/contrib/fsm_storage/redis.py | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 0e89eaea..106a7b97 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -35,7 +35,6 @@ class RedisStorage(BaseStorage): await dp.storage.wait_closed() """ - def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs): self._host = host self._port = port @@ -220,9 +219,12 @@ class RedisStorage2(BaseStorage): await dp.storage.wait_closed() """ - - def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, - pool_size=10, loop=None, prefix='fsm', **kwargs): + def __init__(self, host: str = 'localhost', port=6379, db=None, password=None, + ssl=None, pool_size=10, loop=None, prefix='fsm', + state_ttl: int = 0, + data_ttl: int = 0, + bucket_ttl: int = 0, + **kwargs): self._host = host self._port = port self._db = db @@ -233,6 +235,10 @@ class RedisStorage2(BaseStorage): self._kwargs = kwargs self._prefix = (prefix,) + self._state_ttl = state_ttl + self._data_ttl = data_ttl + self._bucket_ttl = bucket_ttl + self._redis: aioredis.RedisConnection = None self._connection_lock = asyncio.Lock(loop=self._loop) @@ -283,29 +289,29 @@ class RedisStorage2(BaseStorage): return default or {} async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - state: typing.Optional[typing.AnyStr] = None, expire: int = 0): + state: typing.Optional[typing.AnyStr] = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) redis = await self.redis() if state is None: await redis.delete(key) else: - await redis.set(key, state, expire=expire) + await redis.set(key, state, expire=self._state_ttl) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None, expire: int = 0): + data: typing.Dict = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_DATA_KEY) redis = await self.redis() - await redis.set(key, json.dumps(data), expire=expire) + await redis.set(key, json.dumps(data), expire=self._data_ttl) async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None, expire: int = 0, **kwargs): + data: typing.Dict = None, **kwargs): if data is None: data = {} temp_data = await self.get_data(chat=chat, user=user, default={}) temp_data.update(data, **kwargs) - await self.set_data(chat=chat, user=user, data=temp_data, expire=expire) + await self.set_data(chat=chat, user=user, data=temp_data) def has_bucket(self): return True @@ -321,20 +327,20 @@ class RedisStorage2(BaseStorage): return default or {} async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, expire: int = 0): + bucket: typing.Dict = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_BUCKET_KEY) redis = await self.redis() - await redis.set(key, json.dumps(bucket), expire=expire) + await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, expire: int = 0, **kwargs): + bucket: typing.Dict = None, **kwargs): if bucket is None: bucket = {} temp_bucket = await self.get_bucket(chat=chat, user=user) temp_bucket.update(bucket, **kwargs) - await self.set_bucket(chat=chat, user=user, bucket=temp_bucket, expire=expire) + await self.set_bucket(chat=chat, user=user, bucket=temp_bucket) async def reset_all(self, full=True): """ From eae7dc5a2e09b1d12224e0d7112abf1c47c7306b Mon Sep 17 00:00:00 2001 From: SetazeR Date: Mon, 1 Jul 2019 09:19:31 +0700 Subject: [PATCH 015/128] unify finishing state machine interaction --- examples/finite_state_machine_example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py index 58b8053c..90ab8aba 100644 --- a/examples/finite_state_machine_example.py +++ b/examples/finite_state_machine_example.py @@ -112,8 +112,8 @@ async def process_gender(message: types.Message, state: FSMContext): md.text('Gender:', data['gender']), sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN) - # Finish conversation - data.state = None + # Finish conversation + await state.finish() if __name__ == '__main__': From 1b8bcbd1d92eb9ddfd07192bbf1d8cee3e7dd0c4 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 2 Jul 2019 02:05:20 +0300 Subject: [PATCH 016/128] added mongo storage --- aiogram/contrib/fsm_storage/mongo.py | 201 +++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 aiogram/contrib/fsm_storage/mongo.py diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py new file mode 100644 index 00000000..e0a6b3cc --- /dev/null +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -0,0 +1,201 @@ +""" +This module has mongo storage for finite-state machine + based on `aiomongo AioMongoClient: + if isinstance(self._mongo, AioMongoClient): + return self._mongo + + uri = 'mongodb://' + + # set username + password + if self._username and self._password: + uri += f'{self._username}:{self._password}@' + + # set host and port (optional) + uri += f'{self._host}' if self._host else 'localhost' + uri += f':{self._port}' if self._port else '/' + + # define and return client + self._mongo = await aiomongo.create_client(uri) + return self._mongo + + async def get_db(self) -> Database: + """ + Get Mongo db + + This property is awaitable. + """ + if isinstance(self._db, Database): + return self._db + + mongo = await self.get_client() + self._db = mongo.get_database(self._db_name) + + if self._index: + await self.apply_index(self._db) + return self._db + + @staticmethod + async def apply_index(db): + for collection in COLLECTIONS: + await db[collection].create_index(keys=[('chat', 1), ('user', 1)], + name="chat_user_idx", unique=True, background=True) + + async def close(self): + if self._mongo: + self._mongo.close() + + async def wait_closed(self): + if self._mongo: + return await self._mongo.wait_closed() + return True + + async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, + state: Optional[AnyStr] = None): + chat, user = self.check_address(chat=chat, user=user) + db = await self.get_db() + + if state is None: + await db[STATE].delete_one(filter={'chat': chat, 'user': user}) + else: + await db[STATE].update_one(filter={'chat': chat, 'user': user}, + update={'state': state}, upsert=True) + + async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, + default: Optional[str] = None) -> Optional[str]: + chat, user = self.check_address(chat=chat, user=user) + db = await self.get_db() + result = await db[STATE].find_one(filter={'chat': chat, 'user': user}) + + return result.get('state') if result else default + + async def set_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, + data: Dict = None): + chat, user = self.check_address(chat=chat, user=user) + db = await self.get_db() + + await db[DATA].update_one(filter={'chat': chat, 'user': user}, + update={'data': data}, upsert=True) + + async def get_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, + default: Optional[dict] = None) -> Dict: + chat, user = self.check_address(chat=chat, user=user) + db = await self.get_db() + result = await db[DATA].find_one(filter={'chat': chat, 'user': user}) + + return result.get('data') if result else default or {} + + async def update_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, + data: Dict = None, **kwargs): + if data is None: + data = {} + temp_data = await self.get_data(chat=chat, user=user, default={}) + temp_data.update(data, **kwargs) + await self.set_data(chat=chat, user=user, data=temp_data) + + def has_bucket(self): + return True + + async def get_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, + default: Optional[dict] = None) -> Dict: + chat, user = self.check_address(chat=chat, user=user) + db = await self.get_db() + result = await db[BUCKET].find_one(filter={'chat': chat, 'user': user}) + return result.get('bucket') if result else default or {} + + async def set_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, + bucket: Dict = None): + chat, user = self.check_address(chat=chat, user=user) + db = await self.get_db() + + await db[BUCKET].update_one(filter={'chat': chat, 'user': user}, + update={'bucket': bucket}, upsert=True) + + async def update_bucket(self, *, chat: Union[str, int, None] = None, + user: Union[str, int, None] = None, + bucket: Dict = None, **kwargs): + if bucket is None: + bucket = {} + temp_bucket = await self.get_bucket(chat=chat, user=user) + temp_bucket.update(bucket, **kwargs) + await self.set_bucket(chat=chat, user=user, bucket=temp_bucket) + + async def reset_all(self, full=True): + """ + Reset states in DB + + :param full: clean DB or clean only states + :return: + """ + db = await self.get_db() + + await db[STATE].drop() + + if full: + await db[DATA].drop() + await db[BUCKET].drop() + + async def get_states_list(self) -> List[Tuple[int, int]]: + """ + Get list of all stored chat's and user's + + :return: list of tuples where first element is chat id and second is user id + """ + db = await self.get_db() + result = [] + + items = await db[STATE].find().to_list() + for item in items: + result.append( + (int(item['chat']), int(item['user'])) + ) + + return result From a339b7c5712c6dcab332430ef21dd7c137966101 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 2 Jul 2019 10:05:10 +0300 Subject: [PATCH 017/128] Revert "Send data from middlewares to filters " --- aiogram/dispatcher/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 859cb47e..17b715d1 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -105,7 +105,7 @@ class Handler: try: for handler_obj in self.handlers: try: - data.update(await check_filters(handler_obj.filters, args + (data,))) + data.update(await check_filters(handler_obj.filters, args)) except FilterNotPassed: continue else: From cc6ecefbf730ee54aba7eb60304367db924cc440 Mon Sep 17 00:00:00 2001 From: Jess Date: Sun, 7 Jul 2019 09:59:03 -0700 Subject: [PATCH 018/128] Added financial contributors to the README --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f977023..979b38e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AIOGram -[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) +[![Financial Contributors on Open Collective](https://opencollective.com/aiogram/all/badge.svg?label=financial+contributors)](https://opencollective.com/aiogram) [![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) [![PyPi Package Version](https://img.shields.io/pypi/v/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![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) @@ -24,3 +24,33 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/). - Source: [Github repo](https://github.com/aiogram/aiogram) - Issues/Bug tracker: [Github issues tracker](https://github.com/aiogram/aiogram/issues) - Test bot: [@aiogram_bot](https://t.me/aiogram_bot) + +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/aiogram/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/aiogram/contribute)] + + + + + + + + + + + From a1531d4e2064c6dd94f5f1d1325f828b07e67e9b Mon Sep 17 00:00:00 2001 From: Oleg A Date: Mon, 8 Jul 2019 23:39:24 +0300 Subject: [PATCH 019/128] logged callback sender id instead of message sender id --- aiogram/contrib/middlewares/logging.py | 33 ++++++++++---------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index 1a3566c6..d0f257d6 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -89,34 +89,27 @@ class LoggingMiddleware(BaseMiddleware): async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict): if callback_query.message: - if callback_query.message.from_user: - self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] " - f"from user [ID:{callback_query.message.from_user.id}]") - else: - self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + self.logger.info(f"Received callback query [ID:{callback_query.id}] " + f"from user [ID:{callback_query.from_user.id}] " + f"for message [ID:{callback_query.message.message_id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") else: self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"from inline message [ID:{callback_query.inline_message_id}] " - f"from user [ID:{callback_query.from_user.id}]") + f"from user [ID:{callback_query.from_user.id}] " + f"for inline message [ID:{callback_query.inline_message_id}] ") async def on_post_process_callback_query(self, callback_query, results, data: dict): if callback_query.message: - if callback_query.message.from_user: - self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " - f"callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] " - f"from user [ID:{callback_query.message.from_user.id}]") - else: - self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " - f"callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " + f"callback query [ID:{callback_query.id}] " + f"from user [ID:{callback_query.from_user.id}] " + f"for message [ID:{callback_query.message.message_id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") else: self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " f"callback query [ID:{callback_query.id}] " - f"from inline message [ID:{callback_query.inline_message_id}] " - f"from user [ID:{callback_query.from_user.id}]") + f"from user [ID:{callback_query.from_user.id}]" + f"from inline message [ID:{callback_query.inline_message_id}]") async def on_pre_process_shipping_query(self, shipping_query: types.ShippingQuery, data: dict): self.logger.info(f"Received shipping query [ID:{shipping_query.id}] " From 4a2b569e5ff0ca278e6d60755153710021b4cbca Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 9 Jul 2019 00:05:46 +0300 Subject: [PATCH 020/128] fixed uri; fixed $set --- aiogram/contrib/fsm_storage/mongo.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index e0a6b3cc..9ec18090 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -60,8 +60,7 @@ class MongoStorage(BaseStorage): uri += f'{self._username}:{self._password}@' # set host and port (optional) - uri += f'{self._host}' if self._host else 'localhost' - uri += f':{self._port}' if self._port else '/' + uri += f'{self._host}:{self._port}' if self._host else f'localhost:{self._port}' # define and return client self._mongo = await aiomongo.create_client(uri) @@ -107,7 +106,7 @@ class MongoStorage(BaseStorage): await db[STATE].delete_one(filter={'chat': chat, 'user': user}) else: await db[STATE].update_one(filter={'chat': chat, 'user': user}, - update={'state': state}, upsert=True) + update={'$set': {'state': state}}, upsert=True) async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, default: Optional[str] = None) -> Optional[str]: @@ -123,7 +122,7 @@ class MongoStorage(BaseStorage): db = await self.get_db() await db[DATA].update_one(filter={'chat': chat, 'user': user}, - update={'data': data}, upsert=True) + update={'$set': {'data': data}}, upsert=True) async def get_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, default: Optional[dict] = None) -> Dict: @@ -157,7 +156,7 @@ class MongoStorage(BaseStorage): db = await self.get_db() await db[BUCKET].update_one(filter={'chat': chat, 'user': user}, - update={'bucket': bucket}, upsert=True) + update={'$set': {'bucket': bucket}}, upsert=True) async def update_bucket(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, From 49e15545fa9035c3e69de2e6fd98a1cf04a6defc Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 9 Jul 2019 12:29:53 +0300 Subject: [PATCH 021/128] added `originally posted by` to callback --- aiogram/contrib/middlewares/logging.py | 30 ++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index d0f257d6..c5d61aac 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -89,10 +89,16 @@ class LoggingMiddleware(BaseMiddleware): async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict): if callback_query.message: - self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"from user [ID:{callback_query.from_user.id}] " - f"for message [ID:{callback_query.message.message_id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + text = (f"Received callback query [ID:{callback_query.id}] " + f"from user [ID:{callback_query.from_user.id}] " + f"for message [ID:{callback_query.message.message_id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + + if callback_query.message.from_user: + text += f" originally posted by [{callback_query.message.from_user.id}]" + + self.logger.info(text) + else: self.logger.info(f"Received callback query [ID:{callback_query.id}] " f"from user [ID:{callback_query.from_user.id}] " @@ -100,11 +106,17 @@ class LoggingMiddleware(BaseMiddleware): async def on_post_process_callback_query(self, callback_query, results, data: dict): if callback_query.message: - self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " - f"callback query [ID:{callback_query.id}] " - f"from user [ID:{callback_query.from_user.id}] " - f"for message [ID:{callback_query.message.message_id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + text = (f"{HANDLED_STR[bool(len(results))]} " + f"callback query [ID:{callback_query.id}] " + f"from user [ID:{callback_query.from_user.id}] " + f"for message [ID:{callback_query.message.message_id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + + if callback_query.message.from_user: + text += f" originally posted by [{callback_query.message.from_user.id}]" + + self.logger.info(text) + else: self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " f"callback query [ID:{callback_query.id}] " From 1dd462fcf3572ff56af3c2d694f8cbfe576f6c76 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 9 Jul 2019 12:38:54 +0300 Subject: [PATCH 022/128] added `user` --- aiogram/contrib/middlewares/logging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index c5d61aac..9f389b60 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -95,7 +95,7 @@ class LoggingMiddleware(BaseMiddleware): f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") if callback_query.message.from_user: - text += f" originally posted by [{callback_query.message.from_user.id}]" + text += f" originally posted by user [ID:{callback_query.message.from_user.id}]" self.logger.info(text) @@ -113,7 +113,7 @@ class LoggingMiddleware(BaseMiddleware): f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") if callback_query.message.from_user: - text += f" originally posted by [{callback_query.message.from_user.id}]" + text += f" originally posted by user [ID:{callback_query.message.from_user.id}]" self.logger.info(text) From af889ad6b20a939c2380b76f6059628ff3449975 Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 10 Jul 2019 12:04:03 +0300 Subject: [PATCH 023/128] Add regular keyboard usage example --- examples/regular_keyboard_example.py | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 examples/regular_keyboard_example.py diff --git a/examples/regular_keyboard_example.py b/examples/regular_keyboard_example.py new file mode 100644 index 00000000..350e007e --- /dev/null +++ b/examples/regular_keyboard_example.py @@ -0,0 +1,61 @@ +""" +This bot is created for the demonstration of a usage of regular keyboards. +""" + +import logging + +from aiogram import Bot, Dispatcher, executor, types + +API_TOKEN = 'BOT_TOKEN_HERE' + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Initialize bot and dispatcher +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + + +@dp.message_handler(commands=['start']) +async def start_cmd_handler(message: types.Message): + keyboard_markup = types.ReplyKeyboardMarkup(row_width=3) + # default row_width is 3, so here we can omit it actually + # kept for clearness + + keyboard_markup.row(types.KeyboardButton("Yes!"), + types.KeyboardButton("No!")) + # adds buttons as a new row to the existing keyboard + # the behaviour doesn't depend on row_width attribute + + keyboard_markup.add(types.KeyboardButton("I don't know"), + types.KeyboardButton("Who am i?"), + types.KeyboardButton("Where am i?"), + types.KeyboardButton("Who is there?")) + # adds buttons. New rows is formed according to row_width parameter + + await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) + + +@dp.message_handler() +async def all_msg_handler(message: types.Message): + # pressing of a KeyboardButton is the same as sending the regular message with the same text + # so, to handle the responses from the keyboard, we need to use a message_handler + # in real bot, it's better to define message_handler(text="...") for each button + # but here for the simplicity only one handler is defined + + text_of_button = message.text + logger.debug(text_of_button) # print the text we got + + if text_of_button == 'Yes!': + await message.reply("That's great", reply_markup=types.ReplyKeyboardRemove()) + elif text_of_button == 'No!': + await message.reply("Oh no! Why?", reply_markup=types.ReplyKeyboardRemove()) + else: + await message.reply("Keep calm...Everything is fine", reply_markup=types.ReplyKeyboardRemove()) + # with message, we send types.ReplyKeyboardRemove() to hide the keyboard + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) From 31b92e1b95fb1aa2461f04f087dcddded3d3d7ea Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 10 Jul 2019 12:16:00 +0300 Subject: [PATCH 024/128] Add inline keyboard usage example --- examples/inline_keyboard_example.py | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 examples/inline_keyboard_example.py diff --git a/examples/inline_keyboard_example.py b/examples/inline_keyboard_example.py new file mode 100644 index 00000000..2478b9e0 --- /dev/null +++ b/examples/inline_keyboard_example.py @@ -0,0 +1,56 @@ +""" +This bot is created for the demonstration of a usage of inline keyboards. +""" + +import logging + +from aiogram import Bot, Dispatcher, executor, types + +API_TOKEN = 'BOT_TOKEN_HERE' + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Initialize bot and dispatcher +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + + +@dp.message_handler(commands=['start']) +async def start_cmd_handler(message: types.Message): + keyboard_markup = types.InlineKeyboardMarkup(row_width=3) + # default row_width is 3, so here we can omit it actually + # kept for clearness + + keyboard_markup.row(types.InlineKeyboardButton("Yes!", callback_data='yes'), + # in real life for the callback_data the callback data factory should be used + # here the raw string is used for the simplicity + types.InlineKeyboardButton("No!", callback_data='no')) + + keyboard_markup.add(types.InlineKeyboardButton("aiogram link", + url='https://github.com/aiogram/aiogram')) + # url buttons has no callback data + + await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) + + +@dp.callback_query_handler(lambda cb: cb.data in ['yes', 'no']) # if cb.data is either 'yes' or 'no' +# @dp.callback_query_handler(text='yes') # if cb.data == 'yes' +async def inline_kb_answer_callback_handler(query: types.CallbackQuery): + await query.answer() # send answer to close the rounding circle + + answer_data = query.data + logger.debug(f"answer_data={answer_data}") + # here we can work with query.data + if answer_data == 'yes': + await bot.send_message(query.from_user.id, "That's great!") + elif answer_data == 'no': + await bot.send_message(query.from_user.id, "Oh no...Why so?") + else: + await bot.send_message(query.from_user.id, "Invalid callback data!") + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) From 7b9422f627b493b13789683971b52c5fb09c4e53 Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 10 Jul 2019 21:08:29 +0300 Subject: [PATCH 025/128] Add simple example of usage of callback data factory --- examples/callback_data_factory_simple.py | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 examples/callback_data_factory_simple.py diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py new file mode 100644 index 00000000..2c6a8358 --- /dev/null +++ b/examples/callback_data_factory_simple.py @@ -0,0 +1,70 @@ +""" +This is a simple example of usage of CallbackData factory +For more comprehensive example see callback_data_factory.py +""" + +import logging + +from aiogram import Bot, Dispatcher, executor, types +from aiogram.contrib.middlewares.logging import LoggingMiddleware +from aiogram.utils.callback_data import CallbackData +from aiogram.utils.exceptions import MessageNotModified + +logging.basicConfig(level=logging.INFO) + +API_TOKEN = 'BOT_TOKEN_HERE' + + +bot = Bot(token=API_TOKEN) + +dp = Dispatcher(bot) +dp.middleware.setup(LoggingMiddleware()) + +vote_cb = CallbackData('vote', 'action') # vote: +likes = {} # user_id: amount_of_likes + + +def get_keyboard(): + return types.InlineKeyboardMarkup().row( + types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')), + types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down'))) + + +@dp.message_handler(commands=['start']) +async def cmd_start(message: types.Message): + amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0 + await message.reply(f'Vote! Now you have {amount_of_likes} votes.', reply_markup=get_keyboard()) + + +@dp.callback_query_handler(vote_cb.filter(action='up')) +async def vote_up_cb_handler(query: types.CallbackQuery, callback_data: dict): + logging.info(callback_data) # callback_data contains all info from callback data + likes[query.from_user.id] = likes.get(query.from_user.id, 0) + 1 # update amount of likes in storage + amount_of_likes = likes[query.from_user.id] + + await bot.edit_message_text(f'You voted up! Now you have {amount_of_likes} votes.', + query.from_user.id, + query.message.message_id, + reply_markup=get_keyboard()) + + +@dp.callback_query_handler(vote_cb.filter(action='down')) +async def vote_down_cb_handler(query: types.CallbackQuery, callback_data: dict): + logging.info(callback_data) # callback_data contains all info from callback data + likes[query.from_user.id] = likes.get(query.from_user.id, 0) - 1 # update amount of likes in storage + amount_of_likes = likes[query.from_user.id] + + await bot.edit_message_text(f'You voted down! Now you have {amount_of_likes} votes.', + query.from_user.id, + query.message.message_id, + reply_markup=get_keyboard()) + + +@dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises +async def message_not_modified_handler(update, error): + # pass + return True + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) From 1889597aec9b5af083597d68b323067312725583 Mon Sep 17 00:00:00 2001 From: birdi Date: Fri, 12 Jul 2019 20:25:34 +0300 Subject: [PATCH 026/128] Closes #138 --- aiogram/contrib/fsm_storage/files.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/contrib/fsm_storage/files.py b/aiogram/contrib/fsm_storage/files.py index f67a6f69..455ca3f0 100644 --- a/aiogram/contrib/fsm_storage/files.py +++ b/aiogram/contrib/fsm_storage/files.py @@ -20,7 +20,8 @@ class _FileStorage(MemoryStorage): pass async def close(self): - self.write(self.path) + if self.data: + self.write(self.path) await super(_FileStorage, self).close() def read(self, path: pathlib.Path): From 4a4eb5cff90f7ae4c44f5e09c186c90ae0f13798 Mon Sep 17 00:00:00 2001 From: AmirHossein Falahati Date: Sat, 13 Jul 2019 02:33:02 +0430 Subject: [PATCH 027/128] fix BotKicked --- aiogram/utils/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index afd623cc..a6612547 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -490,7 +490,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin): class BotKicked(Unauthorized): - match = 'Bot was kicked from a chat' + match = 'bot was kicked from a chat' class BotBlocked(Unauthorized): From 9d44ed1a460e5fdfc297a4b558a2917146fe3afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCller?= Date: Sat, 13 Jul 2019 10:43:53 +0200 Subject: [PATCH 028/128] Typo fix --- aiogram/dispatcher/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 4c06c2af..8e483255 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -77,7 +77,7 @@ class WebhookRequestHandler(web.View): .. code-block:: python3 - app.router.add_route('*', '/your/webhook/path', WebhookRequestHadler, name='webhook_handler') + app.router.add_route('*', '/your/webhook/path', WebhookRequestHandler, name='webhook_handler') But first you need to configure application for getting Dispatcher instance from request handler! It must always be with key 'BOT_DISPATCHER' From 6cd63cb7bc5a51e88280fb8ec364b157f17d8c3d Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Sun, 14 Jul 2019 02:43:15 +0500 Subject: [PATCH 029/128] Add warn in validate_ip Refer to issue #135 --- aiogram/dispatcher/webhook.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 8e483255..c56f20a6 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -5,6 +5,7 @@ import functools import ipaddress import itertools import typing +import logging from typing import Dict, List, Optional, Union from aiohttp import web @@ -35,6 +36,8 @@ TELEGRAM_SUBNET_2 = ipaddress.IPv4Network('91.108.4.0/22') allowed_ips = set() +log = logging.getLogger(__name__) + def _check_ip(ip: str) -> bool: """ @@ -258,7 +261,9 @@ class WebhookRequestHandler(web.View): if self.request.app.get('_check_ip', False): ip_address, accept = self.check_ip() if not accept: + log.warning(f"Blocking request from a unauthorized IP: {ip_address}") raise web.HTTPUnauthorized() + # context.set_value('TELEGRAM_IP', ip_address) From a0ad19888ed39c4f9ec28a07eae7012b06ee9f2f Mon Sep 17 00:00:00 2001 From: Arslan Sakhapov Date: Sun, 14 Jul 2019 03:11:41 +0500 Subject: [PATCH 030/128] Fix typo --- aiogram/dispatcher/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index c56f20a6..bee635ae 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -261,7 +261,7 @@ class WebhookRequestHandler(web.View): if self.request.app.get('_check_ip', False): ip_address, accept = self.check_ip() if not accept: - log.warning(f"Blocking request from a unauthorized IP: {ip_address}") + log.warning(f"Blocking request from an unauthorized IP: {ip_address}") raise web.HTTPUnauthorized() # context.set_value('TELEGRAM_IP', ip_address) From dacc8226a7f0fa32ced7d79d272d932e1b7899f8 Mon Sep 17 00:00:00 2001 From: Arseny Boykov <36469655+MrMrRobat@users.noreply.github.com> Date: Sun, 14 Jul 2019 03:26:46 +0300 Subject: [PATCH 031/128] Fixed parse_mode type --- aiogram/types/input_media.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 7bb58a7a..95ca75ae 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -26,7 +26,7 @@ class InputMedia(base.TelegramObject): media: base.String = fields.Field(alias='media', on_change='_media_changed') thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed') caption: base.String = fields.Field() - parse_mode: base.Boolean = fields.Field() + parse_mode: base.String = fields.Field() def __init__(self, *args, **kwargs): self._thumb_file = None @@ -110,7 +110,7 @@ class InputMediaAnimation(InputMedia): thumb: typing.Union[base.InputFile, base.String] = None, caption: base.String = None, width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None, - parse_mode: base.Boolean = None, **kwargs): + parse_mode: base.String = None, **kwargs): super(InputMediaAnimation, self).__init__(type='animation', media=media, thumb=thumb, caption=caption, width=width, height=height, duration=duration, parse_mode=parse_mode, conf=kwargs) @@ -124,7 +124,7 @@ class InputMediaDocument(InputMedia): """ def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, - caption: base.String = None, parse_mode: base.Boolean = None, **kwargs): + caption: base.String = None, parse_mode: base.String = None, **kwargs): super(InputMediaDocument, self).__init__(type='document', media=media, thumb=thumb, caption=caption, parse_mode=parse_mode, conf=kwargs) @@ -150,7 +150,7 @@ class InputMediaAudio(InputMedia): duration: base.Integer = None, performer: base.String = None, title: base.String = None, - parse_mode: base.Boolean = None, **kwargs): + parse_mode: base.String = None, **kwargs): super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption, width=width, height=height, duration=duration, performer=performer, title=title, @@ -165,7 +165,7 @@ class InputMediaPhoto(InputMedia): """ def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, - caption: base.String = None, parse_mode: base.Boolean = None, **kwargs): + caption: base.String = None, parse_mode: base.String = None, **kwargs): super(InputMediaPhoto, self).__init__(type='photo', media=media, thumb=thumb, caption=caption, parse_mode=parse_mode, conf=kwargs) @@ -186,7 +186,7 @@ class InputMediaVideo(InputMedia): thumb: typing.Union[base.InputFile, base.String] = None, caption: base.String = None, width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None, - parse_mode: base.Boolean = None, + parse_mode: base.String = None, supports_streaming: base.Boolean = None, **kwargs): super(InputMediaVideo, self).__init__(type='video', media=media, thumb=thumb, caption=caption, width=width, height=height, duration=duration, @@ -277,7 +277,7 @@ class MediaGroup(base.TelegramObject): duration: base.Integer = None, performer: base.String = None, title: base.String = None, - parse_mode: base.Boolean = None): + parse_mode: base.String = None): """ Attach animation @@ -299,7 +299,7 @@ class MediaGroup(base.TelegramObject): self.attach(audio) def attach_document(self, document: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, - caption: base.String = None, parse_mode: base.Boolean = None): + caption: base.String = None, parse_mode: base.String = None): """ Attach document From 8f570315bc395a44ef374ff58720dc85c7f466bf Mon Sep 17 00:00:00 2001 From: jess Date: Sun, 14 Jul 2019 18:01:40 -0700 Subject: [PATCH 032/128] Update README.md changed badge style to flat square --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 979b38e4..f25d7906 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # AIOGram -[![Financial Contributors on Open Collective](https://opencollective.com/aiogram/all/badge.svg?label=financial+contributors)](https://opencollective.com/aiogram) [![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) +[![Financial Contributors on Open Collective](https://opencollective.com/aiogram/all/badge.svg?style=flat-square)](https://opencollective.com/aiogram) +[![\[Telegram\] aiogram live](https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square)](https://t.me/aiogram_live) [![PyPi Package Version](https://img.shields.io/pypi/v/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![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) From 1ea76cd902cfc19986389a52adda70a8bb4555db Mon Sep 17 00:00:00 2001 From: SetazeR Date: Mon, 15 Jul 2019 14:00:38 +0700 Subject: [PATCH 033/128] remove unnecessary loop definitions --- examples/broadcast_example.py | 5 ++--- examples/callback_data_factory.py | 4 ++-- examples/check_user_language.py | 4 ++-- examples/finite_state_machine_example.py | 6 ++---- examples/inline_bot.py | 5 ++--- examples/media_group.py | 5 ++--- examples/middleware_and_antiflood.py | 6 ++---- examples/payments.py | 5 ++--- examples/proxy_and_emojize.py | 5 ++--- examples/throtling_example.py | 5 ++--- examples/webhook_example.py | 3 +-- examples/webhook_example_2.py | 3 +-- 12 files changed, 22 insertions(+), 34 deletions(-) diff --git a/examples/broadcast_example.py b/examples/broadcast_example.py index 9e654d44..2891bb30 100644 --- a/examples/broadcast_example.py +++ b/examples/broadcast_example.py @@ -9,9 +9,8 @@ API_TOKEN = 'BOT TOKEN HERE' logging.basicConfig(level=logging.INFO) log = logging.getLogger('broadcast') -loop = asyncio.get_event_loop() -bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML) -dp = Dispatcher(bot, loop=loop) +bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML) +dp = Dispatcher(bot) def get_users(): diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index 3dd7d35e..8fd197df 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -13,8 +13,8 @@ logging.basicConfig(level=logging.INFO) API_TOKEN = 'BOT TOKEN HERE' -loop = asyncio.get_event_loop() -bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML) + +bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.HTML) storage = MemoryStorage() dp = Dispatcher(bot, storage=storage) dp.middleware.setup(LoggingMiddleware()) diff --git a/examples/check_user_language.py b/examples/check_user_language.py index bd0ba7f9..f59246cf 100644 --- a/examples/check_user_language.py +++ b/examples/check_user_language.py @@ -11,8 +11,8 @@ API_TOKEN = 'BOT TOKEN HERE' logging.basicConfig(level=logging.INFO) -loop = asyncio.get_event_loop() -bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.MARKDOWN) + +bot = Bot(token=API_TOKEN, parse_mode=types.ParseMode.MARKDOWN) dp = Dispatcher(bot) diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py index 90ab8aba..66f89fb2 100644 --- a/examples/finite_state_machine_example.py +++ b/examples/finite_state_machine_example.py @@ -11,9 +11,7 @@ from aiogram.utils import executor API_TOKEN = 'BOT TOKEN HERE' -loop = asyncio.get_event_loop() - -bot = Bot(token=API_TOKEN, loop=loop) +bot = Bot(token=API_TOKEN) # For example use simple MemoryStorage for Dispatcher. storage = MemoryStorage() @@ -117,4 +115,4 @@ async def process_gender(message: types.Message, state: FSMContext): if __name__ == '__main__': - executor.start_polling(dp, loop=loop, skip_updates=True) + executor.start_polling(dp, skip_updates=True) diff --git a/examples/inline_bot.py b/examples/inline_bot.py index 4a771210..f1a81bb4 100644 --- a/examples/inline_bot.py +++ b/examples/inline_bot.py @@ -7,8 +7,7 @@ API_TOKEN = 'BOT TOKEN HERE' logging.basicConfig(level=logging.DEBUG) -loop = asyncio.get_event_loop() -bot = Bot(token=API_TOKEN, loop=loop) +bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) @@ -21,4 +20,4 @@ async def inline_echo(inline_query: types.InlineQuery): if __name__ == '__main__': - executor.start_polling(dp, loop=loop, skip_updates=True) + executor.start_polling(dp, skip_updates=True) diff --git a/examples/media_group.py b/examples/media_group.py index b1f5246a..eafbac6a 100644 --- a/examples/media_group.py +++ b/examples/media_group.py @@ -4,8 +4,7 @@ from aiogram import Bot, Dispatcher, executor, filters, types API_TOKEN = 'BOT TOKEN HERE' -loop = asyncio.get_event_loop() -bot = Bot(token=API_TOKEN, loop=loop) +bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) @@ -40,4 +39,4 @@ async def send_welcome(message: types.Message): if __name__ == '__main__': - executor.start_polling(dp, loop=loop, skip_updates=True) + executor.start_polling(dp, skip_updates=True) diff --git a/examples/middleware_and_antiflood.py b/examples/middleware_and_antiflood.py index c579aecc..4a0cc491 100644 --- a/examples/middleware_and_antiflood.py +++ b/examples/middleware_and_antiflood.py @@ -9,12 +9,10 @@ from aiogram.utils.exceptions import Throttled TOKEN = 'BOT TOKEN HERE' -loop = asyncio.get_event_loop() - # In this example Redis storage is used storage = RedisStorage2(db=5) -bot = Bot(token=TOKEN, loop=loop) +bot = Bot(token=TOKEN) dp = Dispatcher(bot, storage=storage) @@ -119,4 +117,4 @@ if __name__ == '__main__': dp.middleware.setup(ThrottlingMiddleware()) # Start long-polling - executor.start_polling(dp, loop=loop) + executor.start_polling(dp) diff --git a/examples/payments.py b/examples/payments.py index e8e37011..a01fbaf3 100644 --- a/examples/payments.py +++ b/examples/payments.py @@ -9,9 +9,8 @@ from aiogram.utils import executor BOT_TOKEN = 'BOT TOKEN HERE' PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1234567890abcdef1234567890abcdef' -loop = asyncio.get_event_loop() bot = Bot(BOT_TOKEN) -dp = Dispatcher(bot, loop=loop) +dp = Dispatcher(bot) # Setup prices prices = [ @@ -96,4 +95,4 @@ async def got_payment(message: types.Message): if __name__ == '__main__': - executor.start_polling(dp, loop=loop) + executor.start_polling(dp) diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py index 7e4452ee..17e33872 100644 --- a/examples/proxy_and_emojize.py +++ b/examples/proxy_and_emojize.py @@ -25,8 +25,7 @@ GET_IP_URL = 'http://bot.whatismyipaddress.com/' logging.basicConfig(level=logging.INFO) -loop = asyncio.get_event_loop() -bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL) +bot = Bot(token=API_TOKEN, proxy=PROXY_URL) dp = Dispatcher(bot) @@ -62,4 +61,4 @@ async def cmd_start(message: types.Message): if __name__ == '__main__': - start_polling(dp, loop=loop, skip_updates=True) + start_polling(dp, skip_updates=True) diff --git a/examples/throtling_example.py b/examples/throtling_example.py index b979a979..2641b44b 100644 --- a/examples/throtling_example.py +++ b/examples/throtling_example.py @@ -17,8 +17,7 @@ API_TOKEN = 'BOT TOKEN HERE' logging.basicConfig(level=logging.INFO) -loop = asyncio.get_event_loop() -bot = Bot(token=API_TOKEN, loop=loop) +bot = Bot(token=API_TOKEN) # Throttling manager does not work without Leaky Bucket. # Then need to use storages. For example use simple in-memory storage. @@ -40,4 +39,4 @@ async def send_welcome(message: types.Message): if __name__ == '__main__': - start_polling(dp, loop=loop, skip_updates=True) + start_polling(dp, skip_updates=True) diff --git a/examples/webhook_example.py b/examples/webhook_example.py index 86520988..0f6ae3cd 100644 --- a/examples/webhook_example.py +++ b/examples/webhook_example.py @@ -37,8 +37,7 @@ WEBAPP_PORT = 3001 BAD_CONTENT = ContentTypes.PHOTO & ContentTypes.DOCUMENT & ContentTypes.STICKER & ContentTypes.AUDIO -loop = asyncio.get_event_loop() -bot = Bot(TOKEN, loop=loop) +bot = Bot(TOKEN) storage = MemoryStorage() dp = Dispatcher(bot, storage=storage) diff --git a/examples/webhook_example_2.py b/examples/webhook_example_2.py index 75b29c75..e2d9225c 100644 --- a/examples/webhook_example_2.py +++ b/examples/webhook_example_2.py @@ -18,8 +18,7 @@ WEBAPP_PORT = 3001 logging.basicConfig(level=logging.INFO) -loop = asyncio.get_event_loop() -bot = Bot(token=API_TOKEN, loop=loop) +bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) From 40b9b80ce9f520a74a082a6509c3b54deb8a5a89 Mon Sep 17 00:00:00 2001 From: Gabben Date: Tue, 16 Jul 2019 19:48:59 +0500 Subject: [PATCH 034/128] Fix builtin filters Add inline_query_handlers to Text and Regexp bind Fix type hints --- aiogram/dispatcher/dispatcher.py | 4 ++-- aiogram/dispatcher/filters/builtin.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index e11ff536..39eb7810 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -97,7 +97,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(Text, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers + self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers ]) filters_factory.bind(HashTag, event_handlers=[ self.message_handlers, self.edited_message_handlers, @@ -106,7 +106,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(Regexp, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers + self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers ]) filters_factory.bind(RegexpCommandsFilter, event_handlers=[ self.message_handlers, self.edited_message_handlers diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 011b9b67..c68bae72 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -249,7 +249,7 @@ class Text(Filter): elif 'text_endswith' in full_config: return {'endswith': full_config.pop('text_endswith')} - async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]): + async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]): if isinstance(obj, Message): text = obj.text or obj.caption or '' if not text and obj.poll: @@ -359,13 +359,17 @@ class Regexp(Filter): if 'regexp' in full_config: return {'regexp': full_config.pop('regexp')} - async def check(self, obj: Union[Message, CallbackQuery]): + async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]): if isinstance(obj, Message): content = obj.text or obj.caption or '' if not content and obj.poll: content = obj.poll.question elif isinstance(obj, CallbackQuery) and obj.data: content = obj.data + elif isinstance(obj, InlineQuery): + content = obj.query + elif isinstance(obj, Poll): + content = obj.question else: return False From fa1b1aa3ffa85fd7cda437bc91c28e21302c1d0f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 17 Jul 2019 21:45:19 +0300 Subject: [PATCH 035/128] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f25d7906..c0a5bc26 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/). ### Code Contributors -This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. +This project exists thanks to all the people who contribute. [[Code of conduct](CODE_OF_CONDUCT.md)]. ### Financial Contributors From edfc74069efc55cc70437c24dc93658109171c20 Mon Sep 17 00:00:00 2001 From: WeatherControl Date: Thu, 18 Jul 2019 15:38:00 +0300 Subject: [PATCH 036/128] --- aiogram/bot/bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index b0fc3725..e7fd5d6e 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -337,12 +337,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) - payload = generate_payload(**locals(), exclude=['audio']) + payload = generate_payload(**locals(), exclude=['audio', 'thumb']) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) files = {} prepare_file(payload, files, 'audio', audio) + prepare_attachment(payload, files, 'thumb', thumb) + result = await self.request(api.Methods.SEND_AUDIO, payload, files) return types.Message(**result) From 92334a57bea60def7b1dfd9ee3f64c09be604a26 Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 16:19:53 +0300 Subject: [PATCH 037/128] Add enable_cache parameter to lazy_gettext --- aiogram/contrib/middlewares/i18n.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 264bc653..8373f3d6 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -107,7 +107,7 @@ class I18nMiddleware(BaseMiddleware): else: return translator.ngettext(singular, plural, n) - def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy: + def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy: """ Lazy get text @@ -115,9 +115,10 @@ class I18nMiddleware(BaseMiddleware): :param plural: :param n: :param locale: + :param enable_cache: :return: """ - return LazyProxy(self.gettext, singular, plural, n, locale) + return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache) # noinspection PyMethodMayBeStatic,PyUnusedLocal async def get_user_locale(self, action: str, args: Tuple[Any]) -> str: From 0c9c83765295c9421adfb191d58dff81e2cffd42 Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 20:52:41 +0300 Subject: [PATCH 038/128] Add tests for text filter --- tests/test_filters.py | 177 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/test_filters.py diff --git a/tests/test_filters.py b/tests/test_filters.py new file mode 100644 index 00000000..a2095306 --- /dev/null +++ b/tests/test_filters.py @@ -0,0 +1,177 @@ +import pytest + +from aiogram.dispatcher.filters import Text +from aiogram.types import Message, CallbackQuery, InlineQuery, Poll + + +class TestTextFilter: + @pytest.mark.asyncio + @pytest.mark.parametrize("test_prefix, test_text, ignore_case", + [('example_string', 'example_string', True), + ('example_string', 'exAmple_string', True), + ('exAmple_string', 'example_string', True), + + ('example_string', 'example_string', False), + ('example_string', 'exAmple_string', False), + ('exAmple_string', 'example_string', False), + + ('example_string', 'example_string_dsf', True), + ('example_string', 'example_striNG_dsf', True), + ('example_striNG', 'example_string_dsf', True), + + ('example_string', 'example_string_dsf', False), + ('example_string', 'example_striNG_dsf', False), + ('example_striNG', 'example_string_dsf', False), + + ('example_string', 'not_example_string', True), + ('example_string', 'not_eXample_string', True), + ('EXample_string', 'not_example_string', True), + + ('example_string', 'not_example_string', False), + ('example_string', 'not_eXample_string', False), + ('EXample_string', 'not_example_string', False), + ]) + async def test_startswith(self, test_prefix, test_text, ignore_case): + test_filter = Text(startswith=test_prefix, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_prefix = test_prefix.lower() + _test_text = test_text.lower() + else: + _test_prefix = test_prefix + _test_text = test_text + + return result is _test_text.startswith(_test_prefix) + + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_postfix, test_text, ignore_case", + [('example_string', 'example_string', True), + ('example_string', 'exAmple_string', True), + ('exAmple_string', 'example_string', True), + + ('example_string', 'example_string', False), + ('example_string', 'exAmple_string', False), + ('exAmple_string', 'example_string', False), + + ('example_string', 'example_string_dsf', True), + ('example_string', 'example_striNG_dsf', True), + ('example_striNG', 'example_string_dsf', True), + + ('example_string', 'example_string_dsf', False), + ('example_string', 'example_striNG_dsf', False), + ('example_striNG', 'example_string_dsf', False), + + ('example_string', 'not_example_string', True), + ('example_string', 'not_eXample_string', True), + ('EXample_string', 'not_eXample_string', True), + + ('example_string', 'not_example_string', False), + ('example_string', 'not_eXample_string', False), + ('EXample_string', 'not_example_string', False), + ]) + async def test_endswith(self, test_postfix, test_text, ignore_case): + test_filter = Text(endswith=test_postfix, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_postfix = test_postfix.lower() + _test_text = test_text.lower() + else: + _test_postfix = test_postfix + _test_text = test_text + + return result is _test_text.endswith(_test_postfix) + + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_string, test_text, ignore_case", + [('example_string', 'example_string', True), + ('example_string', 'exAmple_string', True), + ('exAmple_string', 'example_string', True), + + ('example_string', 'example_string', False), + ('example_string', 'exAmple_string', False), + ('exAmple_string', 'example_string', False), + + ('example_string', 'example_string_dsf', True), + ('example_string', 'example_striNG_dsf', True), + ('example_striNG', 'example_string_dsf', True), + + ('example_string', 'example_string_dsf', False), + ('example_string', 'example_striNG_dsf', False), + ('example_striNG', 'example_string_dsf', False), + + ('example_string', 'not_example_strin', True), + ('example_string', 'not_eXample_strin', True), + ('EXample_string', 'not_eXample_strin', True), + + ('example_string', 'not_example_strin', False), + ('example_string', 'not_eXample_strin', False), + ('EXample_string', 'not_example_strin', False), + ]) + async def test_contains(self, test_string, test_text, ignore_case): + test_filter = Text(endswith=test_string, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_string = test_string.lower() + _test_text = test_text.lower() + else: + _test_string = test_string + _test_text = test_text + + return result is (_test_string in _test_text) + + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_filter_text, test_text, ignore_case", + [('example_string', 'example_string', True), + ('example_string', 'exAmple_string', True), + ('exAmple_string', 'example_string', True), + + ('example_string', 'example_string', False), + ('example_string', 'exAmple_string', False), + ('exAmple_string', 'example_string', False), + + ('example_string', 'not_example_string', True), + ('example_string', 'not_eXample_string', True), + ('EXample_string', 'not_eXample_string', True), + + ('example_string', 'not_example_string', False), + ('example_string', 'not_eXample_string', False), + ('EXample_string', 'not_example_string', False), + ]) + async def test_equals_string(self, test_filter_text, test_text, ignore_case): + test_filter = Text(equals=test_filter_text) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_filter_text = test_filter_text.lower() + _test_text = test_text.lower() + else: + _test_filter_text = test_filter_text + _test_text = test_text + return result is (_test_text == _test_filter_text) + + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) From 46e5c7a3d000b50cea743af94d7b274be5f29ddf Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 22:57:59 +0300 Subject: [PATCH 039/128] Add ignore_case to equals test --- tests/test_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index a2095306..153c31f6 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -159,7 +159,7 @@ class TestTextFilter: ('EXample_string', 'not_example_string', False), ]) async def test_equals_string(self, test_filter_text, test_text, ignore_case): - test_filter = Text(equals=test_filter_text) + test_filter = Text(equals=test_filter_text, ignore_case=ignore_case) async def check(obj): result = await test_filter.check(obj) From c982fd54ccb4182556b28f9bd6e27a65c9fe348f Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 23:02:03 +0300 Subject: [PATCH 040/128] Fix contains test --- tests/test_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 153c31f6..f68b7c44 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -122,7 +122,7 @@ class TestTextFilter: ('EXample_string', 'not_example_strin', False), ]) async def test_contains(self, test_string, test_text, ignore_case): - test_filter = Text(endswith=test_string, ignore_case=ignore_case) + test_filter = Text(contains=test_string, ignore_case=ignore_case) async def check(obj): result = await test_filter.check(obj) From 6d22f9591a1292a3a3a2ae243d80c9de3ec540c4 Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 18 Jul 2019 23:14:00 +0300 Subject: [PATCH 041/128] Add ignore_case support in Text filter. Fixes #169 --- aiogram/dispatcher/filters/builtin.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index c68bae72..7c8caaa8 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -267,13 +267,25 @@ class Text(Filter): text = text.lower() if self.equals: - return text == str(self.equals) + self.equals = str(self.equals) + if self.ignore_case: + self.equals = self.equals.lower() + return text == self.equals elif self.contains: - return str(self.contains) in text + self.contains = str(self.contains) + if self.ignore_case: + self.contains = self.contains.lower() + return self.contains in text elif self.startswith: - return text.startswith(str(self.startswith)) + self.startswith = str(self.startswith) + if self.ignore_case: + self.startswith = self.startswith.lower() + return text.startswith(self.startswith) elif self.endswith: - return text.endswith(str(self.endswith)) + self.endswith = str(self.endswith) + if self.ignore_case: + self.endswith = self.endswith.lower() + return text.endswith(self.endswith) return False From 5c72bb2b589bd3d0727e58ebc75e6a084096a244 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:15:22 +0300 Subject: [PATCH 042/128] Add IdFilter --- aiogram/dispatcher/dispatcher.py | 7 ++- aiogram/dispatcher/filters/__init__.py | 3 +- aiogram/dispatcher/filters/builtin.py | 66 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 39eb7810..0da5f621 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -9,7 +9,7 @@ import aiohttp from aiohttp.helpers import sentinel from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \ - RegexpCommandsFilter, StateFilter, Text + RegexpCommandsFilter, StateFilter, Text, IdFilter from .handler import Handler from .middlewares import MiddlewareManager from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \ @@ -114,6 +114,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(ExceptionsFilter, event_handlers=[ self.errors_handlers ]) + filters_factory.bind(IdFilter, event_handlers=[ + self.message_handlers, self.edited_message_handlers, + self.channel_post_handlers, self.edited_channel_post_handlers, + self.callback_query_handlers, self.inline_query_handlers + ]) def __del__(self): self.stop_polling() diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 2ae959cf..eb4a5a52 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,5 +1,5 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ - ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text + ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text, IdFilter from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -23,6 +23,7 @@ __all__ = [ 'Regexp', 'StateFilter', 'Text', + 'IdFilter', 'get_filter_spec', 'get_filters_spec', 'execute_filter', diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index c68bae72..f3bbdba7 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -491,3 +491,69 @@ class ExceptionsFilter(BoundFilter): return True except: return False + + +class IdFilter(Filter): + + def __init__(self, + user_id: Optional[Union[str, int]] = None, + chat_id: Optional[Union[str, int]] = None, + ): + """ + :param user_id: + :param chat_id: + """ + 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 = user_id + self.chat_id = chat_id + + # both params should be convertible to int if they aren't None + # here we checks it + # also, by default in Telegram chat_id and user_id are Integer, + # so for convenience we cast them to int + if self.user_id: + self.user_id = int(self.user_id) + if self.chat_id: + self.chat_id = int(self.chat_id) + + @classmethod + def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: + result = {} + if 'user' in full_config: + result['user_id'] = full_config.pop('user') + elif 'user_id' in full_config: + result['user_id'] = full_config.pop('user_id') + + if 'chat' in full_config: + result['chat_id'] = full_config.pop('chat') + elif 'chat_id' in full_config: + result['chat_id'] = full_config.pop('chat_id') + + return result + + async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]): + if isinstance(obj, Message): + user_id = obj.from_user.id + chat_id = obj.chat.id + elif isinstance(obj, CallbackQuery): + user_id = obj.from_user.id + chat_id = None + if obj.message is not None: + # if the button was sent with message + chat_id = obj.message.chat.id + elif isinstance(obj, InlineQuery): + user_id = obj.from_user.id + chat_id = None + else: + return False + + if self.user_id and self.chat_id: + return self.user_id == user_id and self.chat_id == chat_id + elif self.user_id: + return self.user_id == user_id + elif self.chat_id: + return self.chat_id == chat_id + + return False From c1fc41bd7e4da3cbd2b070197001d70c4b761245 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:16:30 +0300 Subject: [PATCH 043/128] Add example of usage of IdFilter --- examples/id-filter-example.py/tmp_test.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 examples/id-filter-example.py/tmp_test.py diff --git a/examples/id-filter-example.py/tmp_test.py b/examples/id-filter-example.py/tmp_test.py new file mode 100644 index 00000000..297e23e4 --- /dev/null +++ b/examples/id-filter-example.py/tmp_test.py @@ -0,0 +1,46 @@ +from aiogram import Bot, Dispatcher, executor, types +from aiogram.dispatcher.handler import SkipHandler + +API_TOKEN = 'API_TOKE_HERE' +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + +user_id_to_test = None # todo: Set id here +chat_id_to_test = user_id_to_test + + +@dp.message_handler(user=user_id_to_test) +async def handler1(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello, checking with user=") + raise SkipHandler + + +@dp.message_handler(user_id=user_id_to_test) +async def handler2(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello, checking with user_id=") + raise SkipHandler + + +@dp.message_handler(chat=chat_id_to_test) +async def handler3(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello, checking with chat=") + raise SkipHandler + + +@dp.message_handler(chat_id=chat_id_to_test) +async def handler4(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello, checking with chat_id=") + raise SkipHandler + + +@dp.message_handler(user=user_id_to_test, chat_id=chat_id_to_test) +async def handler5(msg: types.Message): + await bot.send_message(msg.chat.id, + "Hello from user= & chat_id=") + +if __name__ == '__main__': + executor.start_polling(dp) From 6a3c13ed50c720305f27958a9233cb00b93c0286 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:21:16 +0300 Subject: [PATCH 044/128] Fix example directory --- .../{id-filter-example.py/tmp_test.py => id-filter-example.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{id-filter-example.py/tmp_test.py => id-filter-example.py} (100%) diff --git a/examples/id-filter-example.py/tmp_test.py b/examples/id-filter-example.py similarity index 100% rename from examples/id-filter-example.py/tmp_test.py rename to examples/id-filter-example.py From 6bbc808a3b2f08dd0882fb4e8c7c080f67383cca Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:33:55 +0300 Subject: [PATCH 045/128] Add support for a list of ids and update the example --- aiogram/dispatcher/filters/builtin.py | 33 ++++++++++++++------------- examples/id-filter-example.py | 6 +++++ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index f3bbdba7..d64fb97a 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -496,8 +496,8 @@ class ExceptionsFilter(BoundFilter): class IdFilter(Filter): def __init__(self, - user_id: Optional[Union[str, int]] = None, - chat_id: Optional[Union[str, int]] = None, + user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None, + chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None, ): """ :param user_id: @@ -506,17 +506,18 @@ 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 = user_id - self.chat_id = chat_id - - # both params should be convertible to int if they aren't None - # here we checks it - # also, by default in Telegram chat_id and user_id are Integer, - # so for convenience we cast them to int - if self.user_id: - self.user_id = int(self.user_id) - if self.chat_id: - self.chat_id = int(self.chat_id) + self.user_id = None + self.chat_id = None + if user_id: + if isinstance(user_id, Iterable): + self.user_id = list(map(int, user_id)) + else: + self.user_id = [int(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), ] @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: @@ -550,10 +551,10 @@ class IdFilter(Filter): return False if self.user_id and self.chat_id: - return self.user_id == user_id and self.chat_id == chat_id + return user_id in self.user_id and chat_id in self.chat_id elif self.user_id: - return self.user_id == user_id + return user_id in self.user_id elif self.chat_id: - return self.chat_id == chat_id + return chat_id in self.chat_id return False diff --git a/examples/id-filter-example.py b/examples/id-filter-example.py index 297e23e4..b46ab056 100644 --- a/examples/id-filter-example.py +++ b/examples/id-filter-example.py @@ -42,5 +42,11 @@ async def handler5(msg: types.Message): await bot.send_message(msg.chat.id, "Hello from user= & chat_id=") + +@dp.message_handler(user=[user_id_to_test, 123]) # todo: add second id here +async def handler6(msg: types.Message): + print("Checked with list!") + + if __name__ == '__main__': executor.start_polling(dp) From 4f72f5cb0592d33a1d66c731f175d768a78ad546 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 04:36:13 +0300 Subject: [PATCH 046/128] Update docs --- docs/source/dispatcher/filters.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index d103ac36..e3149b8c 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -111,6 +111,14 @@ ExceptionsFilter :show-inheritance: +ExceptionsFilter +---------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.IdFilter + :members: + :show-inheritance: + + Making own filters (Custom filters) =================================== From 80b11684801446a4e457214738adde4be1018ccd Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 16:56:47 +0300 Subject: [PATCH 047/128] Fix docs and rename id-filter-example.py -> id_filter_example.py --- docs/source/dispatcher/filters.rst | 2 +- examples/{id-filter-example.py => id_filter_example.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/{id-filter-example.py => id_filter_example.py} (100%) diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index e3149b8c..af03e163 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -111,7 +111,7 @@ ExceptionsFilter :show-inheritance: -ExceptionsFilter +IdFilter ---------------- .. autoclass:: aiogram.dispatcher.filters.builtin.IdFilter diff --git a/examples/id-filter-example.py b/examples/id_filter_example.py similarity index 100% rename from examples/id-filter-example.py rename to examples/id_filter_example.py From 7d1c4e260cfd48da141958608e2f38afedd72b60 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 22 Jul 2019 17:10:03 +0300 Subject: [PATCH 048/128] Fix typo in pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 65baad51..1c33729f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,7 +8,7 @@ Fixes # (issue) Please delete options that are not relevant. -- [ ] Documentstion (typos, code examples or any documentation update) +- [ ] Documentation (typos, code examples or any documentation update) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) From 8b028693b6dfd932bef4b59e7af8e3dfe55b3e89 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 22 Jul 2019 17:49:30 +0300 Subject: [PATCH 049/128] Remove user and chat arguments, update the example --- aiogram/dispatcher/filters/builtin.py | 8 ++------ examples/id_filter_example.py | 28 +++++++-------------------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index d64fb97a..5615964c 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -522,14 +522,10 @@ class IdFilter(Filter): @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: result = {} - if 'user' in full_config: - result['user_id'] = full_config.pop('user') - elif 'user_id' in full_config: + if 'user_id' in full_config: result['user_id'] = full_config.pop('user_id') - if 'chat' in full_config: - result['chat_id'] = full_config.pop('chat') - elif 'chat_id' in full_config: + if 'chat_id' in full_config: result['chat_id'] = full_config.pop('chat_id') return result diff --git a/examples/id_filter_example.py b/examples/id_filter_example.py index b46ab056..64dc3b3f 100644 --- a/examples/id_filter_example.py +++ b/examples/id_filter_example.py @@ -9,43 +9,29 @@ user_id_to_test = None # todo: Set id here chat_id_to_test = user_id_to_test -@dp.message_handler(user=user_id_to_test) -async def handler1(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello, checking with user=") - raise SkipHandler - - @dp.message_handler(user_id=user_id_to_test) -async def handler2(msg: types.Message): +async def handler1(msg: types.Message): await bot.send_message(msg.chat.id, "Hello, checking with user_id=") raise SkipHandler -@dp.message_handler(chat=chat_id_to_test) -async def handler3(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello, checking with chat=") - raise SkipHandler - - @dp.message_handler(chat_id=chat_id_to_test) -async def handler4(msg: types.Message): +async def handler2(msg: types.Message): await bot.send_message(msg.chat.id, "Hello, checking with chat_id=") raise SkipHandler -@dp.message_handler(user=user_id_to_test, chat_id=chat_id_to_test) -async def handler5(msg: types.Message): +@dp.message_handler(user_id=user_id_to_test, chat_id=chat_id_to_test) +async def handler3(msg: types.Message): await bot.send_message(msg.chat.id, "Hello from user= & chat_id=") -@dp.message_handler(user=[user_id_to_test, 123]) # todo: add second id here -async def handler6(msg: types.Message): - print("Checked with list!") +@dp.message_handler(user_id=[user_id_to_test, 123]) # todo: add second id here +async def handler4(msg: types.Message): + print("Checked user_id with list!") if __name__ == '__main__': From ab9264e2c9033651e1dcd8a12ce2576bd1e5c67e Mon Sep 17 00:00:00 2001 From: birdi Date: Tue, 23 Jul 2019 00:43:14 +0300 Subject: [PATCH 050/128] Add tests which fall because empty string isn't a correct match string --- tests/test_filters.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index f68b7c44..da530910 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -7,7 +7,12 @@ from aiogram.types import Message, CallbackQuery, InlineQuery, Poll class TestTextFilter: @pytest.mark.asyncio @pytest.mark.parametrize("test_prefix, test_text, ignore_case", - [('example_string', 'example_string', True), + [('', '', True), + ('', 'exAmple_string', True), + ('', '', False), + ('', 'exAmple_string', False), + + ('example_string', 'example_string', True), ('example_string', 'exAmple_string', True), ('exAmple_string', 'example_string', True), @@ -52,7 +57,12 @@ class TestTextFilter: @pytest.mark.asyncio @pytest.mark.parametrize("test_postfix, test_text, ignore_case", - [('example_string', 'example_string', True), + [('', '', True), + ('', 'exAmple_string', True), + ('', '', False), + ('', 'exAmple_string', False), + + ('example_string', 'example_string', True), ('example_string', 'exAmple_string', True), ('exAmple_string', 'example_string', True), @@ -97,7 +107,12 @@ class TestTextFilter: @pytest.mark.asyncio @pytest.mark.parametrize("test_string, test_text, ignore_case", - [('example_string', 'example_string', True), + [('', '', True), + ('', 'exAmple_string', True), + ('', '', False), + ('', 'exAmple_string', False), + + ('example_string', 'example_string', True), ('example_string', 'exAmple_string', True), ('exAmple_string', 'example_string', True), @@ -142,7 +157,12 @@ class TestTextFilter: @pytest.mark.asyncio @pytest.mark.parametrize("test_filter_text, test_text, ignore_case", - [('example_string', 'example_string', True), + [('', '', True), + ('', 'exAmple_string', True), + ('', '', False), + ('', 'exAmple_string', False), + + ('example_string', 'example_string', True), ('example_string', 'exAmple_string', True), ('exAmple_string', 'example_string', True), From 974f19a614242a1a61a5594813f881e9b82957b8 Mon Sep 17 00:00:00 2001 From: birdi Date: Tue, 23 Jul 2019 00:49:26 +0300 Subject: [PATCH 051/128] Fix empty match string in text filter --- 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 9826c63b..15cd73dd 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -221,13 +221,13 @@ class Text(Filter): :param ignore_case: case insensitive """ # Only one mode can be used. check it. - check = sum(map(bool, (equals, contains, startswith, endswith))) + check = sum(map(lambda s: s is not None, (equals, contains, startswith, endswith))) if check > 1: args = "' and '".join([arg[0] for arg in [('equals', equals), ('contains', contains), ('startswith', startswith), ('endswith', endswith) - ] if arg[1]]) + ] if arg[1] is not None]) raise ValueError(f"Arguments '{args}' cannot be used together.") elif check == 0: raise ValueError(f"No one mode is specified!") @@ -266,22 +266,22 @@ class Text(Filter): if self.ignore_case: text = text.lower() - if self.equals: + if self.equals is not None: self.equals = str(self.equals) if self.ignore_case: self.equals = self.equals.lower() return text == self.equals - elif self.contains: + elif self.contains is not None: self.contains = str(self.contains) if self.ignore_case: self.contains = self.contains.lower() return self.contains in text - elif self.startswith: + elif self.startswith is not None: self.startswith = str(self.startswith) if self.ignore_case: self.startswith = self.startswith.lower() return text.startswith(self.startswith) - elif self.endswith: + elif self.endswith is not None: self.endswith = str(self.endswith) if self.ignore_case: self.endswith = self.endswith.lower() From 07ff9d77d642eb3fd0cb8dc7ac0f4a7b853f243a Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 24 Jul 2019 11:57:17 +0300 Subject: [PATCH 052/128] Add tests for multiple Text filter --- tests/test_filters.py | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index da530910..e0002381 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -195,3 +195,60 @@ class TestTextFilter: assert await check(CallbackQuery(data=test_text)) assert await check(InlineQuery(query=test_text)) assert await check(Poll(question=test_text)) + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_filter_list, test_text, ignore_case", + [(['', 'new_string'], '', True), + (['new_string', ''], 'exAmple_string', True), + (['new_string', ''], '', False), + (['', 'new_string'], 'exAmple_string', False), + + (['example_string'], 'example_string', True), + (['example_string'], 'exAmple_string', True), + (['exAmple_string'], 'example_string', True), + + (['example_string'], 'example_string', False), + (['example_string'], 'exAmple_string', False), + (['exAmple_string'], 'example_string', False), + + (['example_string'], 'not_example_string', True), + (['example_string'], 'not_eXample_string', True), + (['EXample_string'], 'not_eXample_string', True), + + (['example_string'], 'not_example_string', False), + (['example_string'], 'not_eXample_string', False), + (['EXample_string'], 'not_example_string', False), + + (['example_string', 'new_string'], 'example_string', True), + (['new_string', 'example_string'], 'exAmple_string', True), + (['exAmple_string', 'new_string'], 'example_string', True), + + (['example_string', 'new_string'], 'example_string', False), + (['new_string', 'example_string'], 'exAmple_string', False), + (['exAmple_string', 'new_string'], 'example_string', False), + + (['example_string', 'new_string'], 'not_example_string', True), + (['new_string', 'example_string'], 'not_eXample_string', True), + (['EXample_string', 'new_string'], 'not_eXample_string', True), + + (['example_string', 'new_string'], 'not_example_string', False), + (['new_string', 'example_string'], 'not_eXample_string', False), + (['EXample_string', 'new_string'], 'not_example_string', False), + ]) + async def test_equals_list(self, test_filter_list, test_text, ignore_case): + test_filter = Text(equals=test_filter_list, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_filter_list = list(map(str.lower, test_filter_list)) + _test_text = test_text.lower() + else: + _test_filter_list = test_filter_list + _test_text = test_text + assert result is (_test_text in _test_filter_list) + + await check(Message(text=test_text)) + await check(CallbackQuery(data=test_text)) + await check(InlineQuery(query=test_text)) + await check(Poll(question=test_text)) From 1a9a11f3fd8e6c6d1a02b757ca313478e3e4d7d3 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 27 Jul 2019 12:20:08 +0300 Subject: [PATCH 053/128] add multiple text filter --- aiogram/dispatcher/filters/builtin.py | 35 ++++++++++++--------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 15cd73dd..eb3845e1 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -206,10 +206,10 @@ class Text(Filter): """ def __init__(self, - equals: Optional[Union[str, LazyProxy]] = None, - contains: Optional[Union[str, LazyProxy]] = None, - startswith: Optional[Union[str, LazyProxy]] = None, - endswith: Optional[Union[str, LazyProxy]] = None, + equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, + contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, + startswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, + endswith: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, ignore_case=False): """ Check text for one of pattern. Only one mode can be used in one filter. @@ -232,6 +232,9 @@ class Text(Filter): elif check == 0: raise ValueError(f"No one mode is specified!") + equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy) + else e, + (equals, contains, endswith, startswith)) self.equals = equals self.contains = contains self.endswith = endswith @@ -267,25 +270,17 @@ class Text(Filter): text = text.lower() if self.equals is not None: - self.equals = str(self.equals) - if self.ignore_case: - self.equals = self.equals.lower() - return text == self.equals + self.equals = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.equals)) + return text in self.equals elif self.contains is not None: - self.contains = str(self.contains) - if self.ignore_case: - self.contains = self.contains.lower() - return self.contains in text + self.contains = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.contains)) + return any(map(text.__contains__, self.contains)) elif self.startswith is not None: - self.startswith = str(self.startswith) - if self.ignore_case: - self.startswith = self.startswith.lower() - return text.startswith(self.startswith) + self.startswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.startswith)) + return any(map(text.startswith, self.startswith)) elif self.endswith is not None: - self.endswith = str(self.endswith) - if self.ignore_case: - self.endswith = self.endswith.lower() - return text.endswith(self.endswith) + self.endswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.endswith)) + return any(map(text.endswith, self.endswith)) return False From a057558ecd24acc23f92f9b5063372f344d02cf0 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 27 Jul 2019 12:55:08 +0300 Subject: [PATCH 054/128] Fix contains to check if text contains everything from list instead of just something --- 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 eb3845e1..386ed7ef 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -274,7 +274,7 @@ class Text(Filter): return text in self.equals elif self.contains is not None: self.contains = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.contains)) - return any(map(text.__contains__, self.contains)) + return all(map(text.__contains__, self.contains)) elif self.startswith is not None: self.startswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.startswith)) return any(map(text.startswith, self.startswith)) From 55c7c2792542016efc69d8c8ef87a14ab7e1878c Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 27 Jul 2019 13:26:57 +0300 Subject: [PATCH 055/128] add tests for multiple text filter --- tests/test_filters.py | 130 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index e0002381..288aa0a4 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -55,6 +55,56 @@ class TestTextFilter: assert await check(InlineQuery(query=test_text)) assert await check(Poll(question=test_text)) + @pytest.mark.asyncio + @pytest.mark.parametrize("test_prefix_list, test_text, ignore_case", + [(['not_example', ''], '', True), + (['', 'not_example'], 'exAmple_string', True), + (['not_example', ''], '', False), + (['', 'not_example'], 'exAmple_string', False), + + (['example_string', 'not_example'], 'example_string', True), + (['not_example', 'example_string'], 'exAmple_string', True), + (['exAmple_string', 'not_example'], 'example_string', True), + + (['not_example', 'example_string'], 'example_string', False), + (['example_string', 'not_example'], 'exAmple_string', False), + (['not_example', 'exAmple_string'], 'example_string', False), + + (['example_string', 'not_example'], 'example_string_dsf', True), + (['not_example', 'example_string'], 'example_striNG_dsf', True), + (['example_striNG', 'not_example'], 'example_string_dsf', True), + + (['not_example', 'example_string'], 'example_string_dsf', False), + (['example_string', 'not_example'], 'example_striNG_dsf', False), + (['not_example', 'example_striNG'], 'example_string_dsf', False), + + (['example_string', 'not_example'], 'not_example_string', True), + (['not_example', 'example_string'], 'not_eXample_string', True), + (['EXample_string', 'not_example'], 'not_example_string', True), + + (['not_example', 'example_string'], 'not_example_string', False), + (['example_string', 'not_example'], 'not_eXample_string', False), + (['not_example', 'EXample_string'], 'not_example_string', False), + ]) + async def test_startswith_list(self, test_prefix_list, test_text, ignore_case): + test_filter = Text(startswith=test_prefix_list, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_prefix_list = map(str.lower, test_prefix_list) + _test_text = test_text.lower() + else: + _test_prefix_list = test_prefix_list + _test_text = test_text + + return result is any(map(_test_text.startswith, _test_prefix_list)) + + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + @pytest.mark.asyncio @pytest.mark.parametrize("test_postfix, test_text, ignore_case", [('', '', True), @@ -105,6 +155,55 @@ class TestTextFilter: assert await check(InlineQuery(query=test_text)) assert await check(Poll(question=test_text)) + @pytest.mark.asyncio + @pytest.mark.parametrize("test_postfix_list, test_text, ignore_case", + [(['', 'not_example'], '', True), + (['not_example', ''], 'exAmple_string', True), + (['', 'not_example'], '', False), + (['not_example', ''], 'exAmple_string', False), + + (['example_string', 'not_example'], 'example_string', True), + (['not_example', 'example_string'], 'exAmple_string', True), + (['exAmple_string', 'not_example'], 'example_string', True), + + (['example_string', 'not_example'], 'example_string', False), + (['not_example', 'example_string'], 'exAmple_string', False), + (['exAmple_string', 'not_example'], 'example_string', False), + + (['example_string', 'not_example'], 'example_string_dsf', True), + (['not_example', 'example_string'], 'example_striNG_dsf', True), + (['example_striNG', 'not_example'], 'example_string_dsf', True), + + (['not_example', 'example_string'], 'example_string_dsf', False), + (['example_string', 'not_example'], 'example_striNG_dsf', False), + (['not_example', 'example_striNG'], 'example_string_dsf', False), + + (['not_example', 'example_string'], 'not_example_string', True), + (['example_string', 'not_example'], 'not_eXample_string', True), + (['not_example', 'EXample_string'], 'not_eXample_string', True), + + (['not_example', 'example_string'], 'not_example_string', False), + (['example_string', 'not_example'], 'not_eXample_string', False), + (['not_example', 'EXample_string'], 'not_example_string', False), + ]) + async def test_endswith_list(self, test_postfix_list, test_text, ignore_case): + test_filter = Text(endswith=test_postfix_list, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_postfix_list = map(str.lower, test_postfix_list) + _test_text = test_text.lower() + else: + _test_postfix_list = test_postfix_list + _test_text = test_text + + return result is any(map(_test_text.endswith, _test_postfix_list)) + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + @pytest.mark.asyncio @pytest.mark.parametrize("test_string, test_text, ignore_case", [('', '', True), @@ -155,6 +254,37 @@ class TestTextFilter: assert await check(InlineQuery(query=test_text)) assert await check(Poll(question=test_text)) + @pytest.mark.asyncio + @pytest.mark.parametrize("test_filter_list, test_text, ignore_case", + [(['a', 'ab', 'abc'], 'A', True), + (['a', 'ab', 'abc'], 'ab', True), + (['a', 'ab', 'abc'], 'aBc', True), + (['a', 'ab', 'abc'], 'd', True), + + (['a', 'ab', 'abc'], 'A', False), + (['a', 'ab', 'abc'], 'ab', False), + (['a', 'ab', 'abc'], 'aBc', False), + (['a', 'ab', 'abc'], 'd', False), + ]) + async def test_contains_list(self, test_filter_list, test_text, ignore_case): + test_filter = Text(contains=test_filter_list, ignore_case=ignore_case) + + async def check(obj): + result = await test_filter.check(obj) + if ignore_case: + _test_filter_list = list(map(str.lower, test_filter_list)) + _test_text = test_text.lower() + else: + _test_filter_list = test_filter_list + _test_text = test_text + + return result is all(map(_test_text.__contains__, _test_filter_list)) + + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + @pytest.mark.asyncio @pytest.mark.parametrize("test_filter_text, test_text, ignore_case", [('', '', True), From e4b0c2a3dfa8b30f72e6e4dcced1356498455319 Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 28 Jul 2019 11:59:20 +0300 Subject: [PATCH 056/128] Add example of usage of multiple text filter --- examples/text_filter_example.py | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 examples/text_filter_example.py diff --git a/examples/text_filter_example.py b/examples/text_filter_example.py new file mode 100644 index 00000000..a47a5c92 --- /dev/null +++ b/examples/text_filter_example.py @@ -0,0 +1,50 @@ +""" +This is a bot to show the usage of the builtin Text filter +Instead of a list, a single element can be passed to any filter, it will be treated as list with an element +""" + +import logging + +from aiogram import Bot, Dispatcher, executor, types + +API_TOKEN = 'API_TOKEN_HERE' + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Initialize bot and dispatcher +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + +# if the text from user in the list +@dp.message_handler(text=['text1', 'text2']) +async def text_in_handler(message: types.Message): + await message.answer("The message text is in the list!") + +# if the text contains any string +@dp.message_handler(text_contains='example1') +@dp.message_handler(text_contains='example2') +async def text_contains_any_handler(message: types.Message): + await message.answer("The message text contains any of strings") + + +# if the text contains all the strings from the list +@dp.message_handler(text_contains=['str1', 'str2']) +async def text_contains_all_handler(message: types.Message): + await message.answer("The message text contains all strings from the list") + + +# if the text starts with any string from the list +@dp.message_handler(text_startswith=['prefix1', 'prefix2']) +async def text_startswith_handler(message: types.Message): + await message.answer("The message text starts with any of prefixes") + + +# if the text ends with any string from the list +@dp.message_handler(text_endswith=['postfix1', 'postfix2']) +async def text_endswith_handler(message: types.Message): + await message.answer("The message text ends with any of postfixes") + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) From 8db663c21817c53d731a763c8b7152ca837678f4 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 28 Jul 2019 12:18:55 +0300 Subject: [PATCH 057/128] Fix filter docs The comments were in the wrong place --- aiogram/dispatcher/filters/filters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aiogram/dispatcher/filters/filters.py b/aiogram/dispatcher/filters/filters.py index 46e44fc9..4806c55a 100644 --- a/aiogram/dispatcher/filters/filters.py +++ b/aiogram/dispatcher/filters/filters.py @@ -202,14 +202,14 @@ class BoundFilter(Filter): You need to implement ``__init__`` method with single argument related with key attribute and ``check`` method where you need to implement filter logic. """ - - """Unique name of the filter argument. You need to override this attribute.""" + key = None - """If :obj:`True` this filter will be added to the all of the registered handlers""" + """Unique name of the filter argument. You need to override this attribute.""" required = False - """Default value for configure required filters""" + """If :obj:`True` this filter will be added to the all of the registered handlers""" default = None - + """Default value for configure required filters""" + @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: """ From ddd92acc09067b383ea22bf850326afe0371cf47 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 29 Jul 2019 21:59:32 +0300 Subject: [PATCH 058/128] Wrapped function can be registered as handler --- aiogram/dispatcher/handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 17b715d1..2a77d580 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -23,11 +23,11 @@ class CancelHandler(Exception): def _get_spec(func: callable): + wrapped_function = func while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks func = func.__wrapped__ - spec = inspect.getfullargspec(func) - return spec, func + return spec, wrapped_function def _check_spec(spec: inspect.FullArgSpec, kwargs: dict): From d2d49282f55544886a38a577d6692c0d7349115c Mon Sep 17 00:00:00 2001 From: birdi Date: Tue, 30 Jul 2019 12:25:42 +0300 Subject: [PATCH 059/128] make handler._get_spec return only specs --- aiogram/dispatcher/handler.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 2a77d580..889dc8d6 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -23,11 +23,10 @@ class CancelHandler(Exception): def _get_spec(func: callable): - wrapped_function = func while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks func = func.__wrapped__ spec = inspect.getfullargspec(func) - return spec, wrapped_function + return spec def _check_spec(spec: inspect.FullArgSpec, kwargs: dict): @@ -56,7 +55,7 @@ class Handler: :param filters: list of filters :param index: you can reorder handlers """ - spec, handler = _get_spec(handler) + spec = _get_spec(handler) if filters and not isinstance(filters, (list, tuple, set)): filters = [filters] From 828f6f2fbfa8d6888711d139fff0f08efffbc2f1 Mon Sep 17 00:00:00 2001 From: birdi Date: Fri, 2 Aug 2019 20:50:40 +0300 Subject: [PATCH 060/128] add throttled decorator. see #166 for more info --- aiogram/dispatcher/dispatcher.py | 63 ++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 0da5f621..657a7d38 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -1030,3 +1030,66 @@ class Dispatcher(DataMixin, ContextInstanceMixin): if run_task: return self.async_task(callback) return callback + + def throttled(self, on_throttled: typing.Callable, *, + key=None, rate=None, + user_id=None, chat_id=None): + """ + Meta-decorator for throttling. + Invokes on_throttled if the handler was throttled. + + Example: + + .. code-block:: python3 + async def handler_throttled(message: types.Message, **kwargs): + message.answer("Throttled!") + + @dp.throttled(handler_throttled) + async def some_handler(message: types.Message ) + + :param on_throttled: the callable object that should be either a function or return a coroutine + :param key: key in storage + :param rate: limit (by default is equal to default rate limit) + :param user_id: user id + :param chat_id: chat id + :return: decorator + """ + + def decorator(func): + nonlocal on_throttled, key, rate, user_id, chat_id + if key is None: + key = func.__name__ + + @functools.wraps(func) + async def wrapped(*args, **kwargs): + is_not_throttled = await self.throttle(key, rate=rate, + user_id=user_id, chat_id=chat_id, + no_error=True) + if is_not_throttled: + return await func(*args, **kwargs) + else: + kwargs.update( + { + 'rate': rate, + 'key': key, + 'user_id': user_id, + 'chat_id': chat_id + } + ) # update kwargs with parameters which were given to throttled + + if asyncio.iscoroutinefunction(on_throttled): + await on_throttled(*args, **kwargs) + else: + kwargs.update( + { + 'loop': asyncio.get_running_loop() + } + ) + partial_func = functools.partial(on_throttled, *args, **kwargs) + asyncio.get_running_loop().run_in_executor(None, + partial_func + ) + + return wrapped + + return decorator From 769be286f7606e0104cec36e72fac58f990c94af Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 3 Aug 2019 15:27:20 +0300 Subject: [PATCH 061/128] Add support of None argument in dp.throttled --- aiogram/dispatcher/dispatcher.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 657a7d38..9948c119 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -1077,18 +1077,19 @@ class Dispatcher(DataMixin, ContextInstanceMixin): } ) # update kwargs with parameters which were given to throttled - if asyncio.iscoroutinefunction(on_throttled): - await on_throttled(*args, **kwargs) - else: - kwargs.update( - { - 'loop': asyncio.get_running_loop() - } - ) - partial_func = functools.partial(on_throttled, *args, **kwargs) - asyncio.get_running_loop().run_in_executor(None, - partial_func - ) + if on_throttled: + if asyncio.iscoroutinefunction(on_throttled): + await on_throttled(*args, **kwargs) + else: + kwargs.update( + { + 'loop': asyncio.get_running_loop() + } + ) + partial_func = functools.partial(on_throttled, *args, **kwargs) + asyncio.get_running_loop().run_in_executor(None, + partial_func + ) return wrapped From 0c715add94e79b0d4c24cb8347b65d513a011c2e Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 3 Aug 2019 15:28:36 +0300 Subject: [PATCH 062/128] Update throttling example --- examples/throtling_example.py | 42 -------------------- examples/throttling_example.py | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 42 deletions(-) delete mode 100644 examples/throtling_example.py create mode 100644 examples/throttling_example.py diff --git a/examples/throtling_example.py b/examples/throtling_example.py deleted file mode 100644 index 2641b44b..00000000 --- a/examples/throtling_example.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Example for throttling manager. - -You can use that for flood controlling. -""" - -import asyncio -import logging - -from aiogram import Bot, types -from aiogram.contrib.fsm_storage.memory import MemoryStorage -from aiogram.dispatcher import Dispatcher -from aiogram.utils.exceptions import Throttled -from aiogram.utils.executor import start_polling - -API_TOKEN = 'BOT TOKEN HERE' - -logging.basicConfig(level=logging.INFO) - -bot = Bot(token=API_TOKEN) - -# Throttling manager does not work without Leaky Bucket. -# Then need to use storages. For example use simple in-memory storage. -storage = MemoryStorage() -dp = Dispatcher(bot, storage=storage) - - -@dp.message_handler(commands=['start', 'help']) -async def send_welcome(message: types.Message): - try: - # Execute throttling manager with rate-limit equal to 2 seconds for key "start" - await dp.throttle('start', rate=2) - except Throttled: - # If request is throttled, the `Throttled` exception will be raised - await message.reply('Too many requests!') - else: - # Otherwise do something - await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") - - -if __name__ == '__main__': - start_polling(dp, skip_updates=True) diff --git a/examples/throttling_example.py b/examples/throttling_example.py new file mode 100644 index 00000000..3b2fe8cd --- /dev/null +++ b/examples/throttling_example.py @@ -0,0 +1,70 @@ +""" +Example for throttling manager. + +You can use that for flood controlling. +""" + +import logging + +from aiogram import Bot, types +from aiogram.contrib.fsm_storage.memory import MemoryStorage +from aiogram.dispatcher import Dispatcher +from aiogram.utils.exceptions import Throttled +from aiogram.utils.executor import start_polling + +API_TOKEN = 'BOT_TOKEN_HERE' + +logging.basicConfig(level=logging.INFO) + +bot = Bot(token=API_TOKEN) + +# Throttling manager does not work without Leaky Bucket. +# Then need to use storages. For example use simple in-memory storage. +storage = MemoryStorage() +dp = Dispatcher(bot, storage=storage) + + +@dp.message_handler(commands=['start']) +async def send_welcome(message: types.Message): + try: + # Execute throttling manager with rate-limit equal to 2 seconds for key "start" + await dp.throttle('start', rate=2) + except Throttled: + # If request is throttled, the `Throttled` exception will be raised + await message.reply('Too many requests!') + else: + # Otherwise do something + await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") + + +@dp.message_handler(commands=['hi']) +@dp.throttled(lambda msg, loop, *args, **kwargs: loop.create_task(bot.send_message(msg.from_user.id, "Throttled")), + rate=5) +# loop is added to the function to run coroutines from it +async def say_hi(message: types.Message): + await message.answer("Hi") + + +# the on_throttled object can be either a regular function or coroutine +async def hello_throttled(*args, **kwargs): + # args will be the same as in the original handler + # kwargs will be updated with parameters given to .throttled (rate, key, user_id, chat_id) + print(f"hello_throttled was called with args={args} and kwargs={kwargs}") + message = args[0] # as message was the first argument in the original handler + await message.answer("Throttled") + + +@dp.message_handler(commands=['hello']) +@dp.throttled(hello_throttled, rate=4) +async def say_hello(message: types.Message): + await message.answer("Hello!") + + +@dp.message_handler(commands=['help']) +@dp.throttled(None, rate=5) +# nothing will happen if the handler will be throttled +async def help_handler(message: types.Message): + await message.answer('Help!') + +if __name__ == '__main__': + start_polling(dp, skip_updates=True) From 288c7099b0496d15fae2990b99e09e0fb076a88e Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 3 Aug 2019 18:39:54 +0300 Subject: [PATCH 063/128] add support of no argument in dp.throttled --- aiogram/dispatcher/dispatcher.py | 2 +- examples/throttling_example.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 9948c119..199db1d2 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -1031,7 +1031,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return self.async_task(callback) return callback - def throttled(self, on_throttled: typing.Callable, *, + def throttled(self, on_throttled: typing.Optional[typing.Callable] = None, key=None, rate=None, user_id=None, chat_id=None): """ diff --git a/examples/throttling_example.py b/examples/throttling_example.py index 3b2fe8cd..3c780974 100644 --- a/examples/throttling_example.py +++ b/examples/throttling_example.py @@ -61,7 +61,7 @@ async def say_hello(message: types.Message): @dp.message_handler(commands=['help']) -@dp.throttled(None, rate=5) +@dp.throttled(rate=5) # nothing will happen if the handler will be throttled async def help_handler(message: types.Message): await message.answer('Help!') From 6ce617cfd2d92c604ba5b46efbe193675964d074 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 19:38:12 +0300 Subject: [PATCH 064/128] Add support for animated stickers --- aiogram/types/sticker.py | 1 + aiogram/types/sticker_set.py | 1 + 2 files changed, 2 insertions(+) diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index b2fd7ef6..8da1e9eb 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -14,6 +14,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable): file_id: base.String = fields.Field() width: base.Integer = fields.Field() height: base.Integer = fields.Field() + is_animated: base.Boolean = fields.Field() thumb: PhotoSize = fields.Field(base=PhotoSize) emoji: base.String = fields.Field() set_name: base.String = fields.Field() diff --git a/aiogram/types/sticker_set.py b/aiogram/types/sticker_set.py index 9d302bae..cb30abe6 100644 --- a/aiogram/types/sticker_set.py +++ b/aiogram/types/sticker_set.py @@ -13,5 +13,6 @@ class StickerSet(base.TelegramObject): """ name: base.String = fields.Field() title: base.String = fields.Field() + is_animated: base.Boolean = fields.Field() contains_masks: base.Boolean = fields.Field() stickers: typing.List[Sticker] = fields.ListField(base=Sticker) From cbaf826be1b0dd8f958c6e23b0dc928ede859ab6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 19:43:07 +0300 Subject: [PATCH 065/128] Add support for new chat permissions --- aiogram/bot/api.py | 1 + aiogram/bot/bot.py | 36 ++++++++++++++++++++++++++++++- aiogram/types/__init__.py | 1 + aiogram/types/chat.py | 2 ++ aiogram/types/chat_member.py | 1 + aiogram/types/chat_permissions.py | 18 ++++++++++++++++ 6 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 aiogram/types/chat_permissions.py diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 6c51b295..4867e7c9 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -182,6 +182,7 @@ class Methods(Helper): UNBAN_CHAT_MEMBER = Item() # unbanChatMember RESTRICT_CHAT_MEMBER = Item() # restrictChatMember PROMOTE_CHAT_MEMBER = Item() # promoteChatMember + SET_CHAT_PERMISSIONS = Item() # setChatPermissions EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink SET_CHAT_PHOTO = Item() # setChatPhoto DELETE_CHAT_PHOTO = Item() # deleteChatPhoto diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index e7fd5d6e..53e49997 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing +import warnings from .base import BaseBot, api from .. import types @@ -345,7 +346,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): prepare_file(payload, files, 'audio', audio) prepare_attachment(payload, files, 'thumb', thumb) - result = await self.request(api.Methods.SEND_AUDIO, payload, files) return types.Message(**result) @@ -1016,6 +1016,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer, + permissions: typing.Optional[types.ChatPermissions] = None, + # permissions argument need to be required after removing other `can_*` arguments until_date: typing.Union[base.Integer, None] = None, can_send_messages: typing.Union[base.Boolean, None] = None, can_send_media_messages: typing.Union[base.Boolean, None] = None, @@ -1032,6 +1034,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type chat_id: :obj:`typing.Union[base.Integer, base.String]` :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` + :param permissions: New user permissions + :type permissions: :obj:`ChatPermissions` :param until_date: Date when restrictions will be lifted for the user, unix time :type until_date: :obj:`typing.Union[base.Integer, None]` :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues @@ -1049,8 +1053,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`base.Boolean` """ until_date = prepare_arg(until_date) + permissions = prepare_arg(permissions) payload = generate_payload(**locals()) + for permission in ['can_send_messages', + 'can_send_media_messages', + 'can_send_other_messages', + 'can_add_web_page_previews']: + if permission in payload: + warnings.warn(f"The method `restrict_chat_member` now takes the new user permissions " + f"in a single argument of the type ChatPermissions instead of " + f"passing regular argument {payload[permission]}", + DeprecationWarning, stacklevel=2) + result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload) return result @@ -1101,6 +1116,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload) return result + async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String], + permissions: types.ChatPermissions) -> base.Boolean: + """ + Use this method to set default chat permissions for all members. + The bot must be an administrator in the group or a supergroup for this to work and must have the + can_restrict_members admin rights. + + Returns True on success. + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + :param permissions: New default chat permissions + :return: True on success. + """ + permissions = prepare_arg(permissions) + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.SET_CHAT_PERMISSIONS) + return result + async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String: """ Use this method to generate a new invite link for a chat; any previously generated link is revoked. diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 5395e486..37dc4b3e 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -7,6 +7,7 @@ from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType from .chat_member import ChatMember, ChatMemberStatus +from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .chosen_inline_result import ChosenInlineResult from .contact import Contact diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index cd34f1be..68746f0e 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -5,6 +5,7 @@ import typing from . import base from . import fields +from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from ..utils import helper from ..utils import markdown @@ -27,6 +28,7 @@ class Chat(base.TelegramObject): description: base.String = fields.Field() invite_link: base.String = fields.Field() pinned_message: 'Message' = fields.Field(base='Message') + permissions: ChatPermissions = fields.Field(base=ChatPermissions) sticker_set_name: base.String = fields.Field() can_set_sticker_set: base.Boolean = fields.Field() diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 12789462..3548f734 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -28,6 +28,7 @@ class ChatMember(base.TelegramObject): is_member: base.Boolean = fields.Field() can_send_messages: base.Boolean = fields.Field() can_send_media_messages: base.Boolean = fields.Field() + can_send_polls: base.Boolean = fields.Field() can_send_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() diff --git a/aiogram/types/chat_permissions.py b/aiogram/types/chat_permissions.py new file mode 100644 index 00000000..0d93256f --- /dev/null +++ b/aiogram/types/chat_permissions.py @@ -0,0 +1,18 @@ +from . import base +from . import fields + + +class ChatPermissions(base.TelegramObject): + """ + Describes actions that a non-administrator user is allowed to take in a chat. + + https://core.telegram.org/bots/api#chatpermissions + """ + can_send_messages: base.Boolean = fields.Field() + can_send_media_messages: base.Boolean = fields.Field() + can_send_polls: base.Boolean = fields.Field() + can_send_other_messages: base.Boolean = fields.Field() + can_add_web_page_previews: base.Boolean = fields.Field() + can_change_info: base.Boolean = fields.Field() + can_invite_users: base.Boolean = fields.Field() + can_pin_messages: base.Boolean = fields.Field() From 14f273702b136d3167b953eb83a1e7e0649f0264 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 19:43:25 +0300 Subject: [PATCH 066/128] 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 c0a5bc26..155b2848 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.3-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.4-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square)](http://aiogram.readthedocs.io/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 0377aad9..e4768f68 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.3-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-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 a1c2736b..b81ceedb 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.2.1.dev1' -__api_version__ = '4.3' +__version__ = '2.3.dev1' +__api_version__ = '4.4' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 4867e7c9..675626ac 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -147,7 +147,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 4.3 + List is updated to Bot API 4.4 """ mode = HelperMode.lowerCamelCase diff --git a/docs/source/index.rst b/docs/source/index.rst index 89cdbf79..543c793a 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.3-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From 47bc628f2b653e2476d2ca7f52cd4b2e7fc36596 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 19:46:39 +0300 Subject: [PATCH 067/128] Remove obsolete code --- aiogram/types/chat_member.py | 24 +----- aiogram/types/message.py | 153 ----------------------------------- 2 files changed, 2 insertions(+), 175 deletions(-) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 3548f734..73222a31 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -32,17 +32,11 @@ class ChatMember(base.TelegramObject): can_send_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() - def is_admin(self): - warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', - DeprecationWarning, stacklevel=2) - return self.is_chat_admin() - def is_chat_admin(self): - return ChatMemberStatus.is_admin(self.status) + return ChatMemberStatus.is_chat_admin(self.status) def is_chat_member(self): - return ChatMemberStatus.is_member(self.status) + return ChatMemberStatus.is_chat_member(self.status) def __int__(self): return self.user.id @@ -61,20 +55,6 @@ class ChatMemberStatus(helper.Helper): LEFT = helper.Item() # left KICKED = helper.Item() # kicked - @classmethod - def is_admin(cls, role): - warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', - DeprecationWarning, stacklevel=2) - return cls.is_chat_admin(role) - - @classmethod - def is_member(cls, role): - warnings.warn('`is_member` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_member` and will be available until aiogram 2.3', - DeprecationWarning, stacklevel=2) - return cls.is_chat_member(role) - @classmethod def is_chat_admin(cls, role): return role in [cls.ADMINISTRATOR, cls.CREATOR] diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 7637cf42..67fd07fa 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -959,70 +959,6 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def send_animation(self, animation: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - width: typing.Union[base.Integer, None] = None, - height: typing.Union[base.Integer, None] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: - """ - Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). - - On success, the sent Message is returned. - Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. - - Source https://core.telegram.org/bots/api#sendanimation - - :param animation: Animation to send. Pass a file_id as String to send an animation that exists - on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation - from the Internet, or upload a new animation using multipart/form-data - :type animation: :obj:`typing.Union[base.InputFile, base.String]` - :param duration: Duration of sent animation in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` - :param width: Animation width - :type width: :obj:`typing.Union[base.Integer, None]` - :param height: Animation height - :type height: :obj:`typing.Union[base.Integer, None]` - :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnail‘s width and height should not exceed 90. - :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` - :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` - :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, - fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user - :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, - types.ReplyKeyboardRemove, types.ForceReply], None]` - :param reply: fill 'reply_to_message_id' - :return: On success, the sent Message is returned - :rtype: :obj:`types.Message` - """ - warn_deprecated('"Message.send_animation" method will be removed in 2.3 version.\n' - 'Use "Message.reply_animation" instead.', - stacklevel=8) - - return await self.bot.send_animation(self.chat.id, - animation=animation, - duration=duration, - width=width, - height=height, - thumb=thumb, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) async def reply_animation(self, animation: typing.Union[base.InputFile, base.String], duration: typing.Union[base.Integer, None] = None, @@ -1323,55 +1259,6 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def send_venue(self, - latitude: base.Float, longitude: base.Float, - title: base.String, address: base.String, - foursquare_id: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: - """ - Use this method to send information about a venue. - - Source: https://core.telegram.org/bots/api#sendvenue - - :param latitude: Latitude of the venue - :type latitude: :obj:`base.Float` - :param longitude: Longitude of the venue - :type longitude: :obj:`base.Float` - :param title: Name of the venue - :type title: :obj:`base.String` - :param address: Address of the venue - :type address: :obj:`base.String` - :param foursquare_id: Foursquare identifier of the venue - :type foursquare_id: :obj:`typing.Union[base.String, None]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, - types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param reply: fill 'reply_to_message_id' - :return: On success, the sent Message is returned. - :rtype: :obj:`types.Message` - """ - warn_deprecated('"Message.send_venue" method will be removed in 2.3 version.\n' - 'Use "Message.reply_venue" instead.', - stacklevel=8) - - return await self.bot.send_venue(chat_id=self.chat.id, - latitude=latitude, - longitude=longitude, - title=title, - address=address, - foursquare_id=foursquare_id, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) - async def reply_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, @@ -1417,46 +1304,6 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def send_contact(self, phone_number: base.String, - first_name: base.String, last_name: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: - """ - Use this method to send phone contacts. - - Source: https://core.telegram.org/bots/api#sendcontact - - :param phone_number: Contact's phone number - :type phone_number: :obj:`base.String` - :param first_name: Contact's first name - :type first_name: :obj:`base.String` - :param last_name: Contact's last name - :type last_name: :obj:`typing.Union[base.String, None]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, - types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param reply: fill 'reply_to_message_id' - :return: On success, the sent Message is returned. - :rtype: :obj:`types.Message` - """ - warn_deprecated('"Message.send_contact" method will be removed in 2.3 version.\n' - 'Use "Message.reply_contact" instead.', - stacklevel=8) - - return await self.bot.send_contact(chat_id=self.chat.id, - phone_number=phone_number, - first_name=first_name, last_name=last_name, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) - async def reply_contact(self, phone_number: base.String, first_name: base.String, last_name: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, From 870e79d7f20f6935a6a374db5aa8fa886e58a796 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 20:03:58 +0300 Subject: [PATCH 068/128] Add new permissions argument to Chat.restrict method --- aiogram/types/chat.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 68746f0e..f5c521a5 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -204,6 +204,7 @@ class Chat(base.TelegramObject): return await self.bot.unban_chat_member(self.id, user_id=user_id) async def restrict(self, user_id: base.Integer, + permissions: typing.Optional[ChatPermissions] = None, until_date: typing.Union[base.Integer, None] = None, can_send_messages: typing.Union[base.Boolean, None] = None, can_send_media_messages: typing.Union[base.Boolean, None] = None, @@ -218,6 +219,8 @@ class Chat(base.TelegramObject): :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` + :param permissions: New user permissions + :type permissions: :obj:`ChatPermissions` :param until_date: Date when restrictions will be lifted for the user, unix time. :type until_date: :obj:`typing.Union[base.Integer, None]` :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues @@ -234,7 +237,9 @@ class Chat(base.TelegramObject): :return: Returns True on success. :rtype: :obj:`base.Boolean` """ - return await self.bot.restrict_chat_member(self.id, user_id=user_id, until_date=until_date, + return await self.bot.restrict_chat_member(self.id, user_id=user_id, + permissions=permissions, + until_date=until_date, can_send_messages=can_send_messages, can_send_media_messages=can_send_media_messages, can_send_other_messages=can_send_other_messages, From 57724c55fff0efb9a330719f06dc2ba23a1ab187 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 4 Aug 2019 20:10:57 +0300 Subject: [PATCH 069/128] Add initializer for ChatPermissions object (for IDE) --- aiogram/types/chat_permissions.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/aiogram/types/chat_permissions.py b/aiogram/types/chat_permissions.py index 0d93256f..9d44653e 100644 --- a/aiogram/types/chat_permissions.py +++ b/aiogram/types/chat_permissions.py @@ -16,3 +16,24 @@ class ChatPermissions(base.TelegramObject): can_change_info: base.Boolean = fields.Field() can_invite_users: base.Boolean = fields.Field() can_pin_messages: base.Boolean = fields.Field() + + def __init__(self, + can_send_messages: base.Boolean = None, + can_send_media_messages: base.Boolean = None, + can_send_polls: base.Boolean = None, + can_send_other_messages: base.Boolean = None, + can_add_web_page_previews: base.Boolean = None, + can_change_info: base.Boolean = None, + can_invite_users: base.Boolean = None, + can_pin_messages: base.Boolean = None, + **kwargs): + super(ChatPermissions, self).__init__( + can_send_messages=can_send_messages, + can_send_media_messages=can_send_media_messages, + can_send_polls=can_send_polls, + can_send_other_messages=can_send_other_messages, + can_add_web_page_previews=can_add_web_page_previews, + can_change_info=can_change_info, + can_invite_users=can_invite_users, + can_pin_messages=can_pin_messages, + ) From c081846b0ea520212adfda6d515fe868e8935ac5 Mon Sep 17 00:00:00 2001 From: Gabben Date: Sun, 4 Aug 2019 23:11:07 +0500 Subject: [PATCH 070/128] Update chat_member.py --- aiogram/types/chat_member.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 288b6d76..89506400 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -36,7 +36,7 @@ class ChatMember(base.TelegramObject): return ChatMemberStatus.is_chat_admin(self.status) def is_chat_member(self) -> bool: - return ChatMemberStatus.is_chat_member(self.status, self.is_member) + return ChatMemberStatus.is_chat_member(self.status) def __int__(self) -> int: return self.user.id @@ -60,5 +60,5 @@ class ChatMemberStatus(helper.Helper): return role in [cls.ADMINISTRATOR, cls.CREATOR] @classmethod - def is_chat_member(cls, role: str, is_member: Optional[bool] = None) -> bool: - return (role == cls.RESTRICTED and is_member is True) or role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR] + def is_chat_member(cls, role: str) -> bool: + return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED] From 1be6e1f31d4d2f5bb0676c3659aeab7a9d1109ca Mon Sep 17 00:00:00 2001 From: Gabben Date: Sun, 4 Aug 2019 23:18:57 +0500 Subject: [PATCH 071/128] Update test_chat_member.py --- tests/types/test_chat_member.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/types/test_chat_member.py b/tests/types/test_chat_member.py index 97e365ec..2cea44ce 100644 --- a/tests/types/test_chat_member.py +++ b/tests/types/test_chat_member.py @@ -66,10 +66,8 @@ def test_chat_member_status_filters(): assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.CREATOR) assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.ADMINISTRATOR) assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.MEMBER) - assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED, True) + assert types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED) - assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED) - assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.RESTRICTED, False) assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.LEFT) assert not types.ChatMemberStatus.is_chat_member(types.ChatMemberStatus.KICKED) From 321aa404c3f8350d8718f0eb46dc4aa70438ea69 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 5 Aug 2019 10:02:06 +0300 Subject: [PATCH 072/128] Add docstrings to the Text filter --- aiogram/dispatcher/filters/builtin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 386ed7ef..af328a13 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -213,11 +213,12 @@ class Text(Filter): ignore_case=False): """ Check text for one of pattern. Only one mode can be used in one filter. + In every pattern, a single string is treated as a list with 1 element. - :param equals: - :param contains: - :param startswith: - :param endswith: + :param equals: True if object text in the list + :param contains: True if object text contains all strings from the list + :param startswith: True if object text startswith any of strings from the list + :param endswith: True if object text endswith any of strings from the list :param ignore_case: case insensitive """ # Only one mode can be used. check it. From 6246432a0c6196974244b15bddea27733e1562fe Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 5 Aug 2019 10:12:48 +0300 Subject: [PATCH 073/128] Improve Text filter docs --- aiogram/dispatcher/filters/builtin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index af328a13..df72b9d0 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -215,10 +215,10 @@ class Text(Filter): Check text for one of pattern. Only one mode can be used in one filter. In every pattern, a single string is treated as a list with 1 element. - :param equals: True if object text in the list - :param contains: True if object text contains all strings from the list - :param startswith: True if object text startswith any of strings from the list - :param endswith: True if object text endswith any of strings from the list + :param equals: True if object's text in the list + :param contains: True if object's text contains all strings from the list + :param startswith: True if object's text starts with any of strings from the list + :param endswith: True if object's text ends with any of strings from the list :param ignore_case: case insensitive """ # Only one mode can be used. check it. From 4d00b7c50242612f8289bca4d86d621f428ff741 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 5 Aug 2019 11:32:28 +0300 Subject: [PATCH 074/128] refactor dp.throttled --- aiogram/dispatcher/dispatcher.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 199db1d2..61874642 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -1054,16 +1054,12 @@ class Dispatcher(DataMixin, ContextInstanceMixin): :param chat_id: chat id :return: decorator """ - def decorator(func): - nonlocal on_throttled, key, rate, user_id, chat_id - if key is None: - key = func.__name__ - @functools.wraps(func) async def wrapped(*args, **kwargs): - is_not_throttled = await self.throttle(key, rate=rate, - user_id=user_id, chat_id=chat_id, + is_not_throttled = await self.throttle(key if key is not None else func.__name__, + rate=rate, + user=user_id, chat=chat_id, no_error=True) if is_not_throttled: return await func(*args, **kwargs) @@ -1090,7 +1086,6 @@ class Dispatcher(DataMixin, ContextInstanceMixin): asyncio.get_running_loop().run_in_executor(None, partial_func ) - return wrapped return decorator From 4a52f97a68b20c41a9c367c0e64c371575563136 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 5 Aug 2019 13:17:47 +0300 Subject: [PATCH 075/128] Fix throttled docs --- aiogram/dispatcher/dispatcher.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 61874642..404cc8e1 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -1041,11 +1041,13 @@ class Dispatcher(DataMixin, ContextInstanceMixin): Example: .. code-block:: python3 + async def handler_throttled(message: types.Message, **kwargs): - message.answer("Throttled!") + await message.answer("Throttled!") @dp.throttled(handler_throttled) - async def some_handler(message: types.Message ) + async def some_handler(message: types.Message): + await message.answer("Didn't throttled!") :param on_throttled: the callable object that should be either a function or return a coroutine :param key: key in storage From bd032f066e514e67d578e1cd0fcc91371ff4a3bc Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Thu, 8 Aug 2019 19:12:49 +0500 Subject: [PATCH 076/128] Added AdminFilter and example of its usage --- aiogram/dispatcher/dispatcher.py | 7 ++++- aiogram/dispatcher/filters/__init__.py | 3 +- aiogram/dispatcher/filters/builtin.py | 40 ++++++++++++++++++++++++++ examples/admin_filter_example.py | 28 ++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 examples/admin_filter_example.py diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 0da5f621..6e3aacc5 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -9,7 +9,7 @@ import aiohttp from aiohttp.helpers import sentinel from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \ - RegexpCommandsFilter, StateFilter, Text, IdFilter + RegexpCommandsFilter, StateFilter, Text, IdFilter, AdminFilter from .handler import Handler from .middlewares import MiddlewareManager from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \ @@ -119,6 +119,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.channel_post_handlers, self.edited_channel_post_handlers, self.callback_query_handlers, self.inline_query_handlers ]) + filters_factory.bind(AdminFilter, event_handlers=[ + self.message_handlers, self.edited_message_handlers, + self.channel_post_handlers, self.edited_channel_post_handlers, + self.callback_query_handlers, self.inline_query_handlers + ]) def __del__(self): self.stop_polling() diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index eb4a5a52..b9174ee6 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,5 +1,5 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ - ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text, IdFilter + ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text, IdFilter, AdminFilter from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -24,6 +24,7 @@ __all__ = [ 'StateFilter', 'Text', 'IdFilter', + 'AdminFilter', 'get_filter_spec', 'get_filters_spec', 'execute_filter', diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index df72b9d0..4401f349 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -562,3 +562,43 @@ class IdFilter(Filter): return chat_id in self.chat_id return False + + +class AdminFilter(Filter): + """ + Checks if user is admin in a chat. + If chat_id is not set, the filter will check in the current chat (correct only for messages). + chat_id is required for InlineQuery. + """ + + def __init__(self, admin_chat_id: typing.Optional[int] = None, admin_current_chat: typing.Optional[bool] = False): + self.chat_id = admin_chat_id + self.check_current_chat = admin_current_chat + + @classmethod + def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: + result = {} + + if "admin_chat_id" in full_config: # use prefix 'admin' to not conflict with IdFilter + result["admin_chat_id"] = full_config.pop("admin_chat_id") + if "admin_current_chat" in full_config: # set True if need to check current chat + result["admin_current_chat"] = full_config.pop("admin_current_chat") + + return result + + async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool: + user_id = obj.from_user.id + chat_id = self.chat_id + + if not chat_id or self.check_current_chat: + if isinstance(obj, Message): + chat_id = obj.chat.id + elif isinstance(obj, CallbackQuery): + if obj.message: + chat_id = obj.message.chat.id + else: + raise ValueError("Cannot get current chat in a InlineQuery") + + admins = [member.user.id for member in await obj.bot.get_chat_administrators(chat_id)] + + return user_id in admins diff --git a/examples/admin_filter_example.py b/examples/admin_filter_example.py new file mode 100644 index 00000000..052f8efe --- /dev/null +++ b/examples/admin_filter_example.py @@ -0,0 +1,28 @@ +import logging + +from aiogram import Bot, Dispatcher, types, executor + +API_TOKEN = 'API TOKEN HERE' + +logging.basicConfig(level=logging.DEBUG) + +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot=bot) + +chat_id = -1001241113577 + + +# checks specified chat +@dp.message_handler(admin_chat_id=chat_id) +async def handler(msg: types.Message): + await msg.reply(f"You are an admin of the chat '{chat_id}'", reply=False) + + +# checks current chat +@dp.message_handler(admin_current_chat=True) +async def handler(msg: types.Message): + await msg.reply("You are an admin of the current chat!", reply=False) + + +if __name__ == '__main__': + executor.start_polling(dp) From 9f6b577f081a88ff5917166775b1b53ffcc07fcd Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Thu, 8 Aug 2019 19:14:27 +0500 Subject: [PATCH 077/128] Rename IdFilter to IDFilter Rename IdFilter to IDFilter as it is an abbreviation like URL --- 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 4401f349..65dd595c 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -501,7 +501,7 @@ class ExceptionsFilter(BoundFilter): return False -class IdFilter(Filter): +class IDFilter(Filter): def __init__(self, user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None, From c0e60f87063397bb014841a04306ce24980b76da Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Thu, 8 Aug 2019 20:53:52 +0500 Subject: [PATCH 078/128] Fix issues and rework AdminFilter --- aiogram/dispatcher/dispatcher.py | 4 +-- aiogram/dispatcher/filters/__init__.py | 4 +-- aiogram/dispatcher/filters/builtin.py | 44 ++++++++++++++++++-------- docs/source/dispatcher/filters.rst | 12 +++++-- examples/admin_filter_example.py | 23 ++++++++------ 5 files changed, 58 insertions(+), 29 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 6e3aacc5..150e8019 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -9,7 +9,7 @@ import aiohttp from aiohttp.helpers import sentinel from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \ - RegexpCommandsFilter, StateFilter, Text, IdFilter, AdminFilter + RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter from .handler import Handler from .middlewares import MiddlewareManager from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \ @@ -114,7 +114,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(ExceptionsFilter, event_handlers=[ self.errors_handlers ]) - filters_factory.bind(IdFilter, event_handlers=[ + filters_factory.bind(IDFilter, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, self.callback_query_handlers, self.inline_query_handlers diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index b9174ee6..3f202915 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,5 +1,5 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ - ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text, IdFilter, AdminFilter + ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -23,7 +23,7 @@ __all__ = [ 'Regexp', 'StateFilter', 'Text', - 'IdFilter', + 'IDFilter', 'AdminFilter', 'get_filter_spec', 'get_filters_spec', diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 65dd595c..c84ddca4 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -9,7 +9,7 @@ from babel.support import LazyProxy from aiogram import types from aiogram.dispatcher.filters.filters import BoundFilter, Filter -from aiogram.types import CallbackQuery, Message, InlineQuery, Poll +from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType class Command(Filter): @@ -571,34 +571,50 @@ class AdminFilter(Filter): chat_id is required for InlineQuery. """ - def __init__(self, admin_chat_id: typing.Optional[int] = None, admin_current_chat: typing.Optional[bool] = False): - self.chat_id = admin_chat_id - self.check_current_chat = admin_current_chat + def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None): + self._all_chats = 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._all_chats = is_chat_admin + if isinstance(is_chat_admin, Iterable): + self.chat_ids = list(map(int, is_chat_admin)) + else: + self.chat_ids = [int(is_chat_admin)] + else: + self._all_chats = True @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: result = {} - if "admin_chat_id" in full_config: # use prefix 'admin' to not conflict with IdFilter - result["admin_chat_id"] = full_config.pop("admin_chat_id") - if "admin_current_chat" in full_config: # set True if need to check current chat - result["admin_current_chat"] = full_config.pop("admin_current_chat") + if "is_chat_admin" in full_config: + result["is_chat_admin"] = full_config.pop("is_chat_admin") return result async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool: user_id = obj.from_user.id - chat_id = self.chat_id + chat_ids = None + + if self._all_chats: + if ChatType.is_private(obj): # there is no admin in private chats + return False - if not chat_id or self.check_current_chat: if isinstance(obj, Message): - chat_id = obj.chat.id + chat_ids = [obj.chat.id] elif isinstance(obj, CallbackQuery): if obj.message: - chat_id = obj.message.chat.id + chat_ids = [obj.message.chat.id] else: - raise ValueError("Cannot get current chat in a InlineQuery") + return False + else: + chat_ids = self.chat_ids - admins = [member.user.id for member in await obj.bot.get_chat_administrators(chat_id)] + admins = [member.user.id for chat_id in chat_ids for member in await obj.bot.get_chat_administrators(chat_id)] return user_id in admins diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index af03e163..059a4f06 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -111,10 +111,18 @@ ExceptionsFilter :show-inheritance: -IdFilter +IDFilter ---------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.IdFilter +.. autoclass:: aiogram.dispatcher.filters.builtin.IDFilter + :members: + :show-inheritance: + + +AdminFilter +---------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.AdminFilter :members: :show-inheritance: diff --git a/examples/admin_filter_example.py b/examples/admin_filter_example.py index 052f8efe..ec8746bb 100644 --- a/examples/admin_filter_example.py +++ b/examples/admin_filter_example.py @@ -2,26 +2,31 @@ import logging from aiogram import Bot, Dispatcher, types, executor -API_TOKEN = 'API TOKEN HERE' +API_TOKEN = 'API_TOKEN_HERE' + logging.basicConfig(level=logging.DEBUG) bot = Bot(token=API_TOKEN) dp = Dispatcher(bot=bot) -chat_id = -1001241113577 - # checks specified chat -@dp.message_handler(admin_chat_id=chat_id) -async def handler(msg: types.Message): - await msg.reply(f"You are an admin of the chat '{chat_id}'", reply=False) +@dp.message_handler(is_chat_admin=-1001241113577) +async def handle_specified(msg: types.Message): + await msg.answer("You are an admin of the specified chat!") + + +# checks multiple chats +@dp.message_handler(is_chat_admin=[-1001241113577, -320463906]) +async def handle_multiple(msg: types.Message): + await msg.answer("You are an admin of multiple chats!") # checks current chat -@dp.message_handler(admin_current_chat=True) -async def handler(msg: types.Message): - await msg.reply("You are an admin of the current chat!", reply=False) +@dp.message_handler(is_chat_admin=True) +async def handler3(msg: types.Message): + await msg.answer("You are an admin of the current chat!") if __name__ == '__main__': From c7c27e9e1c81ef8d9c3474dff33f49757fed3011 Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Thu, 8 Aug 2019 21:07:23 +0500 Subject: [PATCH 079/128] Some fixes --- aiogram/dispatcher/filters/builtin.py | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index c84ddca4..9270932c 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -572,21 +572,21 @@ class AdminFilter(Filter): """ def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None): - self._all_chats = False - self.chat_ids = 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._all_chats = is_chat_admin + self._check_current = is_chat_admin if isinstance(is_chat_admin, Iterable): - self.chat_ids = list(map(int, is_chat_admin)) + self._chat_ids = list(map(int, is_chat_admin)) else: - self.chat_ids = [int(is_chat_admin)] + self._chat_ids = [int(is_chat_admin)] else: - self._all_chats = True + self._check_current = True @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: @@ -601,19 +601,19 @@ class AdminFilter(Filter): user_id = obj.from_user.id chat_ids = None - if self._all_chats: - if ChatType.is_private(obj): # there is no admin in private chats - return False - + if self._check_current: if isinstance(obj, Message): + if ChatType.is_private(obj): + return False chat_ids = [obj.chat.id] - elif isinstance(obj, CallbackQuery): - if obj.message: - chat_ids = [obj.message.chat.id] + elif isinstance(obj, CallbackQuery) and obj.message: + if ChatType.is_private(obj.message): # there is no admin in private chats + return False + chat_ids = [obj.message.chat.id] else: return False else: - chat_ids = self.chat_ids + chat_ids = self._chat_ids admins = [member.user.id for chat_id in chat_ids for member in await obj.bot.get_chat_administrators(chat_id)] From de0f8d33f24ce36a494f27028b053344d685c8e3 Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Thu, 8 Aug 2019 21:15:01 +0500 Subject: [PATCH 080/128] Doc fix --- aiogram/dispatcher/filters/builtin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 9270932c..acba3a2c 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -567,8 +567,8 @@ class IDFilter(Filter): class AdminFilter(Filter): """ Checks if user is admin in a chat. - If chat_id is not set, the filter will check in the current chat (correct only for messages). - chat_id is required for InlineQuery. + If is_chat_admin is not set, the filter will check in the current chat (correct only for messages). + is_chat_admin is required for InlineQuery. """ def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None): From bda0bdd0131c6edcfd62dcb79c5a14a07810c0ea Mon Sep 17 00:00:00 2001 From: Arslan 'Ars2014' Sakhapov Date: Fri, 9 Aug 2019 12:57:32 +0500 Subject: [PATCH 081/128] Optimizations from code review --- aiogram/dispatcher/filters/builtin.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index acba3a2c..54490a35 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -582,9 +582,9 @@ class AdminFilter(Filter): if isinstance(is_chat_admin, bool): self._check_current = is_chat_admin if isinstance(is_chat_admin, Iterable): - self._chat_ids = list(map(int, is_chat_admin)) + self._chat_ids = list(is_chat_admin) else: - self._chat_ids = [int(is_chat_admin)] + self._chat_ids = [is_chat_admin] else: self._check_current = True @@ -599,19 +599,17 @@ class AdminFilter(Filter): async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool: user_id = obj.from_user.id - chat_ids = None if self._check_current: if isinstance(obj, Message): - if ChatType.is_private(obj): - return False - chat_ids = [obj.chat.id] + message = obj elif isinstance(obj, CallbackQuery) and obj.message: - if ChatType.is_private(obj.message): # there is no admin in private chats - return False - chat_ids = [obj.message.chat.id] + message = obj.message else: return False + if ChatType.is_private(message): # there is no admin in private chats + return False + chat_ids = [message.chat.id] else: chat_ids = self._chat_ids From 61e1015c1e71ae5b86e830465076168206f81d01 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 20 Jul 2019 22:47:23 +0300 Subject: [PATCH 082/128] Minor refactor callback_data --- aiogram/utils/callback_data.py | 47 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py index 916d08c4..b0162a7e 100644 --- a/aiogram/utils/callback_data.py +++ b/aiogram/utils/callback_data.py @@ -28,13 +28,13 @@ class CallbackData: def __init__(self, prefix, *parts, sep=':'): if not isinstance(prefix, str): - raise TypeError(f"Prefix must be instance of str not {type(prefix).__name__}") - elif not prefix: - raise ValueError('Prefix can\'t be empty') - elif sep in prefix: - raise ValueError(f"Separator '{sep}' can't be used in prefix") - elif not parts: - raise TypeError('Parts is not passed!') + raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}') + if not prefix: + raise ValueError("Prefix can't be empty") + if sep in prefix: + raise ValueError(f"Separator {sep!r} can't be used in prefix") + if not parts: + raise TypeError('Parts were not passed!') self.prefix = prefix self.sep = sep @@ -59,20 +59,20 @@ class CallbackData: if args: value = args.pop(0) else: - raise ValueError(f"Value for '{part}' is not passed!") + raise ValueError(f'Value for {part!r} was not passed!') if value is not None and not isinstance(value, str): value = str(value) if not value: - raise ValueError(f"Value for part {part} can't be empty!'") - elif self.sep in value: - raise ValueError(f"Symbol defined as separator can't be used in values of parts") + raise ValueError(f"Value for part {part!r} can't be empty!'") + if self.sep in value: + raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values") data.append(value) if args or kwargs: - raise TypeError('Too many arguments is passed!') + raise TypeError('Too many arguments were passed!') callback_data = self.sep.join(data) if len(callback_data) > 64: @@ -106,30 +106,31 @@ class CallbackData: """ for key in config.keys(): if key not in self._part_names: - raise ValueError(f"Invalid field name '{key}'") + raise ValueError(f'Invalid field name {key!r}') return CallbackDataFilter(self, config) class CallbackDataFilter(Filter): + def __init__(self, factory: CallbackData, config: typing.Dict[str, str]): self.config = config self.factory = factory @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]): - raise ValueError('That filter can\'t be used in filters factory!') + raise ValueError("That filter can't be used in filters factory!") async def check(self, query: types.CallbackQuery): try: data = self.factory.parse(query.data) except ValueError: return False - else: - for key, value in self.config.items(): - if isinstance(value, (list, tuple, set)): - if data.get(key) not in value: - return False - else: - if value != data.get(key): - return False - return {'callback_data': data} + + for key, value in self.config.items(): + if isinstance(value, (list, tuple, set, frozenset)): + if data.get(key) not in value: + return False + else: + if data.get(key) != value: + return False + return {'callback_data': data} From 846f83f11713a3ba5185e13b238ae9c87123b82f Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 20 Jul 2019 23:08:38 +0300 Subject: [PATCH 083/128] Some callback_data_factory.py refactor --- aiogram/utils/executor.py | 2 +- examples/callback_data_factory.py | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 65594371..cdb3fe91 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -339,7 +339,7 @@ class Executor: async def _skip_updates(self): await self.dispatcher.reset_webhook(True) await self.dispatcher.skip_updates() - log.warning(f"Updates are skipped successfully.") + log.warning(f'Updates were skipped successfully.') async def _welcome(self): user = await self.dispatcher.bot.me diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index 8fd197df..9a8affe9 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -1,4 +1,3 @@ -import asyncio import logging import random import uuid @@ -21,12 +20,12 @@ dp.middleware.setup(LoggingMiddleware()) POSTS = { str(uuid.uuid4()): { - 'title': f"Post {index}", + 'title': f'Post {index}', 'body': 'Lorem ipsum dolor sit amet, ' 'consectetur adipiscing elit, ' 'sed do eiusmod tempor incididunt ut ' 'labore et dolore magna aliqua', - 'votes': random.randint(-2, 5) + 'votes': random.randint(-2, 5), } for index in range(1, 6) } @@ -42,21 +41,24 @@ def get_keyboard() -> types.InlineKeyboardMarkup: markup.add( types.InlineKeyboardButton( post['title'], - callback_data=posts_cb.new(id=post_id, action='view')) + callback_data=posts_cb.new(id=post_id, action='view')), ) return markup def format_post(post_id: str, post: dict) -> (str, types.InlineKeyboardMarkup): - text = f"{md.hbold(post['title'])}\n" \ - f"{md.quote_html(post['body'])}\n" \ - f"\n" \ - f"Votes: {post['votes']}" + text = md.text( + md.hbold(post['title']), + md.quote_html(post['body']), + '', # just new empty line + f"Votes: {post['votes']}", + sep = '\n', + ) markup = types.InlineKeyboardMarkup() markup.row( types.InlineKeyboardButton('👍', callback_data=posts_cb.new(id=post_id, action='like')), - types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='unlike')), + types.InlineKeyboardButton('👎', callback_data=posts_cb.new(id=post_id, action='dislike')), ) markup.add(types.InlineKeyboardButton('<< Back', callback_data=posts_cb.new(id='-', action='list'))) return text, markup @@ -84,7 +86,7 @@ async def query_view(query: types.CallbackQuery, callback_data: dict): await query.message.edit_text(text, reply_markup=markup) -@dp.callback_query_handler(posts_cb.filter(action=['like', 'unlike'])) +@dp.callback_query_handler(posts_cb.filter(action=['like', 'dislike'])) async def query_post_vote(query: types.CallbackQuery, callback_data: dict): try: await dp.throttle('vote', rate=1) @@ -100,10 +102,10 @@ async def query_post_vote(query: types.CallbackQuery, callback_data: dict): if action == 'like': post['votes'] += 1 - elif action == 'unlike': + elif action == 'dislike': post['votes'] -= 1 - await query.answer('Voted.') + await query.answer('Vote accepted') text, markup = format_post(post_id, post) await query.message.edit_text(text, reply_markup=markup) @@ -114,4 +116,4 @@ async def message_not_modified_handler(update, error): if __name__ == '__main__': - executor.start_polling(dp, loop=loop, skip_updates=True) + executor.start_polling(dp, skip_updates=True) From cb7ec3edb83518fafd2f0ddc9aa1f229f51bfb87 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 20 Jul 2019 23:37:52 +0300 Subject: [PATCH 084/128] Refactor examples/callback_data_factory_simple.py --- examples/callback_data_factory_simple.py | 42 +++++++++++------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py index 2c6a8358..5fc9c548 100644 --- a/examples/callback_data_factory_simple.py +++ b/examples/callback_data_factory_simple.py @@ -27,42 +27,40 @@ likes = {} # user_id: amount_of_likes def get_keyboard(): return types.InlineKeyboardMarkup().row( types.InlineKeyboardButton('👍', callback_data=vote_cb.new(action='up')), - types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down'))) + types.InlineKeyboardButton('👎', callback_data=vote_cb.new(action='down')), + ) @dp.message_handler(commands=['start']) async def cmd_start(message: types.Message): amount_of_likes = likes.get(message.from_user.id, 0) # get value if key exists else set to 0 - await message.reply(f'Vote! Now you have {amount_of_likes} votes.', reply_markup=get_keyboard()) + await message.reply(f'Vote! You have {amount_of_likes} votes now.', reply_markup=get_keyboard()) -@dp.callback_query_handler(vote_cb.filter(action='up')) -async def vote_up_cb_handler(query: types.CallbackQuery, callback_data: dict): - logging.info(callback_data) # callback_data contains all info from callback data - likes[query.from_user.id] = likes.get(query.from_user.id, 0) + 1 # update amount of likes in storage - amount_of_likes = likes[query.from_user.id] +@dp.callback_query_handler(vote_cb.filter(action=['up', 'down'])) +async def callback_vote_action(query: types.CallbackQuery, callback_data: dict): + logging.info('Got this callback data: %r', callback_data) # callback_data contains all info from callback data + await query.answer() # don't forget to answer callback query as soon as possible + callback_data_action = callback_data['action'] + likes_count = likes.get(query.from_user.id, 0) - await bot.edit_message_text(f'You voted up! Now you have {amount_of_likes} votes.', - query.from_user.id, - query.message.message_id, - reply_markup=get_keyboard()) + if callback_data_action == 'up': + likes_count += 1 + else: + likes_count -= 1 + likes[query.from_user.id] = likes_count # update amount of likes in storage -@dp.callback_query_handler(vote_cb.filter(action='down')) -async def vote_down_cb_handler(query: types.CallbackQuery, callback_data: dict): - logging.info(callback_data) # callback_data contains all info from callback data - likes[query.from_user.id] = likes.get(query.from_user.id, 0) - 1 # update amount of likes in storage - amount_of_likes = likes[query.from_user.id] - - await bot.edit_message_text(f'You voted down! Now you have {amount_of_likes} votes.', - query.from_user.id, - query.message.message_id, - reply_markup=get_keyboard()) + await bot.edit_message_text( + f'You voted {callback_data_action}! Now you have {likes_count} vote[s].', + query.from_user.id, + query.message.message_id, + reply_markup=get_keyboard(), + ) @dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises async def message_not_modified_handler(update, error): - # pass return True From f4aafb043e7c3e0d82e4070b1351cf22ecb6647c Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 21 Jul 2019 11:19:08 +0300 Subject: [PATCH 085/128] Refactor examples/check_user_language.py --- examples/check_user_language.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/check_user_language.py b/examples/check_user_language.py index f59246cf..98bed8a6 100644 --- a/examples/check_user_language.py +++ b/examples/check_user_language.py @@ -2,7 +2,6 @@ Babel is required. """ -import asyncio import logging from aiogram import Bot, Dispatcher, executor, md, types @@ -22,12 +21,13 @@ async def check_language(message: types.Message): await message.reply(md.text( md.bold('Info about your language:'), - md.text(' 🔸', md.bold('Code:'), md.italic(locale.locale)), - md.text(' 🔸', md.bold('Territory:'), md.italic(locale.territory or 'Unknown')), - md.text(' 🔸', md.bold('Language name:'), md.italic(locale.language_name)), - md.text(' 🔸', md.bold('English language name:'), md.italic(locale.english_name)), - sep='\n')) + md.text('🔸', md.bold('Code:'), md.code(locale.language)), + md.text('🔸', md.bold('Territory:'), md.code(locale.territory or 'Unknown')), + md.text('🔸', md.bold('Language name:'), md.code(locale.language_name)), + md.text('🔸', md.bold('English language name:'), md.code(locale.english_name)), + sep='\n', + )) if __name__ == '__main__': - executor.start_polling(dp, loop=loop, skip_updates=True) + executor.start_polling(dp, skip_updates=True) From 3867b72df6e95ae6bddc3979f62deaf3870cc22f Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 21 Jul 2019 11:30:33 +0300 Subject: [PATCH 086/128] Refactor examples/echo_bot.py --- examples/echo_bot.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/echo_bot.py b/examples/echo_bot.py index 27dc70d9..00046f3a 100644 --- a/examples/echo_bot.py +++ b/examples/echo_bot.py @@ -20,7 +20,7 @@ dp = Dispatcher(bot) @dp.message_handler(commands=['start', 'help']) async def send_welcome(message: types.Message): """ - This handler will be called when client send `/start` or `/help` commands. + This handler will be called when user sends `/start` or `/help` command """ await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") @@ -28,13 +28,25 @@ async def send_welcome(message: types.Message): @dp.message_handler(regexp='(^cat[s]?$|puss)') async def cats(message: types.Message): with open('data/cats.jpg', 'rb') as photo: - await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺', - reply_to_message_id=message.message_id) + ''' + # Old fashioned way: + await bot.send_photo( + message.chat.id, + photo, + caption='Cats are here 😺', + reply_to_message_id=message.message_id, + ) + ''' + + await message.reply_photo(photo, caption='Cats are here 😺') @dp.message_handler() async def echo(message: types.Message): - await bot.send_message(message.chat.id, message.text) + # old style: + # await bot.send_message(message.chat.id, message.text) + + await message.reply(message.text, reply=False) if __name__ == '__main__': From 6cf06bd081678948718970b2c73a5da2ce14d27d Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 21 Jul 2019 23:44:24 +0300 Subject: [PATCH 087/128] Fix test_states_group name --- tests/{states_group.py => test_states_group.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{states_group.py => test_states_group.py} (100%) diff --git a/tests/states_group.py b/tests/test_states_group.py similarity index 100% rename from tests/states_group.py rename to tests/test_states_group.py From cabb10bc06d045f89adc9111ccc60c30fa7643d1 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 21 Jul 2019 23:45:25 +0300 Subject: [PATCH 088/128] Create some tests --- .../test_filters/test_builtin.py | 18 ++++++++++++++++++ .../test_dispatcher/test_filters/test_state.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/test_dispatcher/test_filters/test_builtin.py create mode 100644 tests/test_dispatcher/test_filters/test_state.py diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py new file mode 100644 index 00000000..86344cec --- /dev/null +++ b/tests/test_dispatcher/test_filters/test_builtin.py @@ -0,0 +1,18 @@ +import pytest + +from aiogram.dispatcher.filters.builtin import Text + + +class TestText: + + @pytest.mark.parametrize('param, key', [ + ('text', 'equals'), + ('text_contains', 'contains'), + ('text_startswith', 'startswith'), + ('text_endswith', 'endswith'), + ]) + def test_validate(self, param, key): + value = 'spam and eggs' + config = {param: value} + res = Text.validate(config) + assert res == {key: value} diff --git a/tests/test_dispatcher/test_filters/test_state.py b/tests/test_dispatcher/test_filters/test_state.py new file mode 100644 index 00000000..b7f5a5fd --- /dev/null +++ b/tests/test_dispatcher/test_filters/test_state.py @@ -0,0 +1,18 @@ +from aiogram.dispatcher.filters.state import StatesGroup + +class TestStatesGroup: + + def test_all_childs(self): + + class InnerState1(StatesGroup): + pass + + class InnerState2(InnerState1): + pass + + class Form(StatesGroup): + inner1 = InnerState1 + inner2 = InnerState2 + + form_childs = Form.all_childs + assert form_childs == (InnerState1, InnerState2) From ad7238fda8d51f9a96fb5d32be4f1f2b68b9396a Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Mon, 22 Jul 2019 00:11:06 +0300 Subject: [PATCH 089/128] Some refactor --- aiogram/dispatcher/dispatcher.py | 10 +-- aiogram/dispatcher/filters/builtin.py | 18 ++-- aiogram/dispatcher/filters/state.py | 23 +++--- aiogram/utils/mixins.py | 4 +- dev_requirements.txt | 1 + examples/finite_state_machine_example.py | 43 ++++++---- tests/types/dataset.py | 100 +++++++++++------------ 7 files changed, 106 insertions(+), 93 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 0da5f621..8236118e 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -97,22 +97,22 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(Text, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers + self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers, ]) filters_factory.bind(HashTag, event_handlers=[ self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers + self.channel_post_handlers, self.edited_channel_post_handlers, ]) filters_factory.bind(Regexp, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers + self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers, ]) filters_factory.bind(RegexpCommandsFilter, event_handlers=[ - self.message_handlers, self.edited_message_handlers + self.message_handlers, self.edited_message_handlers, ]) filters_factory.bind(ExceptionsFilter, event_handlers=[ - self.errors_handlers + self.errors_handlers, ]) filters_factory.bind(IdFilter, event_handlers=[ self.message_handlers, self.edited_message_handlers, diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index df72b9d0..9a44de23 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -205,6 +205,13 @@ class Text(Filter): Simple text filter """ + _default_params = ( + ('text', 'equals'), + ('text_contains', 'contains'), + ('text_startswith', 'startswith'), + ('text_endswith', 'endswith'), + ) + def __init__(self, equals: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, contains: Optional[Union[str, LazyProxy, Iterable[Union[str, LazyProxy]]]] = None, @@ -244,14 +251,9 @@ class Text(Filter): @classmethod def validate(cls, full_config: Dict[str, Any]): - if 'text' in full_config: - return {'equals': full_config.pop('text')} - elif 'text_contains' in full_config: - return {'contains': full_config.pop('text_contains')} - elif 'text_startswith' in full_config: - return {'startswith': full_config.pop('text_startswith')} - elif 'text_endswith' in full_config: - return {'endswith': full_config.pop('text_endswith')} + for param, key in cls._default_params: + if param in full_config: + return {key: full_config.pop(param)} async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]): if isinstance(obj, Message): diff --git a/aiogram/dispatcher/filters/state.py b/aiogram/dispatcher/filters/state.py index afe08e64..16937e1c 100644 --- a/aiogram/dispatcher/filters/state.py +++ b/aiogram/dispatcher/filters/state.py @@ -25,17 +25,17 @@ class State: @property def state(self): - if self._state is None: - return None - elif self._state == '*': + if self._state is None or self._state == '*': return self._state - elif self._group_name is None and self._group: + + if self._group_name is None and self._group: group = self._group.__full_group_name__ elif self._group_name: group = self._group_name else: group = '@' - return f"{group}:{self._state}" + + return f'{group}:{self._state}' def set_parent(self, group): if not issubclass(group, StatesGroup): @@ -73,7 +73,6 @@ class StatesGroupMeta(type): elif inspect.isclass(prop) and issubclass(prop, StatesGroup): childs.append(prop) prop._parent = cls - # continue cls._parent = None cls._childs = tuple(childs) @@ -83,13 +82,13 @@ class StatesGroupMeta(type): return cls @property - def __group_name__(cls): + def __group_name__(cls) -> str: return cls._group_name @property - def __full_group_name__(cls): + def __full_group_name__(cls) -> str: if cls._parent: - return cls._parent.__full_group_name__ + '.' + cls._group_name + return '.'.join((cls._parent.__full_group_name__, cls._group_name)) return cls._group_name @property @@ -97,7 +96,7 @@ class StatesGroupMeta(type): return cls._states @property - def childs(cls): + def childs(cls) -> tuple: return cls._childs @property @@ -130,9 +129,9 @@ class StatesGroupMeta(type): def __contains__(cls, item): if isinstance(item, str): return item in cls.all_states_names - elif isinstance(item, State): + if isinstance(item, State): return item in cls.all_states - elif isinstance(item, StatesGroup): + if isinstance(item, StatesGroup): return item in cls.all_childs return False diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index 776479bd..e6857263 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -31,7 +31,7 @@ T = TypeVar('T') class ContextInstanceMixin: def __init_subclass__(cls, **kwargs): - cls.__context_instance = contextvars.ContextVar('instance_' + cls.__name__) + cls.__context_instance = contextvars.ContextVar(f'instance_{cls.__name__}') return cls @classmethod @@ -43,5 +43,5 @@ class ContextInstanceMixin: @classmethod def set_current(cls: Type[T], value: T): if not isinstance(value, cls): - raise TypeError(f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'") + raise TypeError(f'Value should be instance of {cls.__name__!r} not {type(value).__name__!r}') cls.__context_instance.set(value) diff --git a/dev_requirements.txt b/dev_requirements.txt index 79adc949..06bc3e9c 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -15,3 +15,4 @@ sphinx-rtd-theme>=0.4.3 sphinxcontrib-programoutput>=0.14 aiohttp-socks>=0.2.2 rethinkdb>=2.4.1 +coverage==4.5.3 diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py index 66f89fb2..7c0536a7 100644 --- a/examples/finite_state_machine_example.py +++ b/examples/finite_state_machine_example.py @@ -1,16 +1,19 @@ -import asyncio -from typing import Optional +import logging import aiogram.utils.markdown as md from aiogram import Bot, Dispatcher, types from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.dispatcher import FSMContext +from aiogram.dispatcher.filters import Text from aiogram.dispatcher.filters.state import State, StatesGroup from aiogram.types import ParseMode from aiogram.utils import executor +logging.basicConfig(level=logging.INFO) + API_TOKEN = 'BOT TOKEN HERE' + bot = Bot(token=API_TOKEN) # For example use simple MemoryStorage for Dispatcher. @@ -25,7 +28,7 @@ class Form(StatesGroup): gender = State() # Will be represented in storage as 'Form:gender' -@dp.message_handler(commands=['start']) +@dp.message_handler(commands='start') async def cmd_start(message: types.Message): """ Conversation's entry point @@ -37,19 +40,21 @@ async def cmd_start(message: types.Message): # You can use state '*' if you need to handle all states -@dp.message_handler(state='*', commands=['cancel']) -@dp.message_handler(lambda message: message.text.lower() == 'cancel', state='*') -async def cancel_handler(message: types.Message, state: FSMContext, raw_state: Optional[str] = None): +@dp.message_handler(state='*', commands='cancel') +@dp.message_handler(Text(equals='cancel', ignore_case=True), state='*') +async def cancel_handler(message: types.Message, state: FSMContext): """ Allow user to cancel any action """ - if raw_state is None: + current_state = await state.get_state() + if current_state is None: return + logging.info('Cancelling state %r', current_state) # Cancel state and inform user about it await state.finish() # And remove keyboard (just in case) - await message.reply('Canceled.', reply_markup=types.ReplyKeyboardRemove()) + await message.reply('Cancelled.', reply_markup=types.ReplyKeyboardRemove()) @dp.message_handler(state=Form.name) @@ -66,7 +71,7 @@ async def process_name(message: types.Message, state: FSMContext): # Check age. Age gotta be digit @dp.message_handler(lambda message: not message.text.isdigit(), state=Form.age) -async def failed_process_age(message: types.Message): +async def process_age_invalid(message: types.Message): """ If age is invalid """ @@ -88,11 +93,11 @@ async def process_age(message: types.Message, state: FSMContext): @dp.message_handler(lambda message: message.text not in ["Male", "Female", "Other"], state=Form.gender) -async def failed_process_gender(message: types.Message): +async def process_gender_invalid(message: types.Message): """ In this example gender has to be one of: Male, Female, Other. """ - return await message.reply("Bad gender name. Choose you gender from keyboard.") + return await message.reply("Bad gender name. Choose your gender from the keyboard.") @dp.message_handler(state=Form.gender) @@ -104,11 +109,17 @@ async def process_gender(message: types.Message, state: FSMContext): markup = types.ReplyKeyboardRemove() # And send message - await bot.send_message(message.chat.id, md.text( - md.text('Hi! Nice to meet you,', md.bold(data['name'])), - md.text('Age:', data['age']), - md.text('Gender:', data['gender']), - sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN) + await bot.send_message( + message.chat.id, + md.text( + md.text('Hi! Nice to meet you,', md.bold(data['name'])), + md.text('Age:', md.code(data['age'])), + md.text('Gender:', data['gender']), + sep='\n', + ), + reply_markup=markup, + parse_mode=ParseMode.MARKDOWN, + ) # Finish conversation await state.finish() diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 4167eae1..18bcbdad 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -8,7 +8,7 @@ USER = { "first_name": "FirstName", "last_name": "LastName", "username": "username", - "language_code": "ru" + "language_code": "ru", } CHAT = { @@ -16,14 +16,14 @@ CHAT = { "first_name": "FirstName", "last_name": "LastName", "username": "username", - "type": "private" + "type": "private", } PHOTO = { "file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg", "file_size": 1101, "width": 90, - "height": 51 + "height": 51, } AUDIO = { @@ -32,7 +32,7 @@ AUDIO = { "title": "The Best Song", "performer": "The Best Singer", "file_id": "CQADAgADbQEAAsnrIUpNoRRNsH7_hAI", - "file_size": 9507774 + "file_size": 9507774, } CHAT_MEMBER = { @@ -44,7 +44,7 @@ CHAT_MEMBER = { "can_invite_users": True, "can_restrict_members": True, "can_pin_messages": True, - "can_promote_members": False + "can_promote_members": False, } CONTACT = { @@ -57,7 +57,7 @@ DOCUMENT = { "file_name": "test.docx", "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "file_id": "BQADAgADpgADy_JxS66XQTBRHFleAg", - "file_size": 21331 + "file_size": 21331, } ANIMATION = { @@ -65,51 +65,51 @@ ANIMATION = { "mime_type": "video/mp4", "thumb": PHOTO, "file_id": "CgADBAAD4DUAAoceZAe2WiE9y0crrAI", - "file_size": 65837 + "file_size": 65837, } ENTITY_BOLD = { "offset": 5, "length": 2, - "type": "bold" + "type": "bold", } ENTITY_ITALIC = { "offset": 8, "length": 1, - "type": "italic" + "type": "italic", } ENTITY_LINK = { "offset": 10, "length": 6, "type": "text_link", - "url": "http://google.com/" + "url": "http://google.com/", } ENTITY_CODE = { "offset": 17, "length": 7, - "type": "code" + "type": "code", } ENTITY_PRE = { "offset": 30, "length": 4, - "type": "pre" + "type": "pre", } ENTITY_MENTION = { "offset": 47, "length": 9, - "type": "mention" + "type": "mention", } GAME = { "title": "Karate Kido", "description": "No trees were harmed in the making of this game :)", "photo": [PHOTO, PHOTO, PHOTO], - "animation": ANIMATION + "animation": ANIMATION, } INVOICE = { @@ -120,19 +120,19 @@ INVOICE = { "Order our Working Time Machine today!", "start_parameter": "time-machine-example", "currency": "USD", - "total_amount": 6250 + "total_amount": 6250, } LOCATION = { "latitude": 50.693416, - "longitude": 30.624605 + "longitude": 30.624605, } VENUE = { "location": LOCATION, "title": "Venue Name", "address": "Venue Address", - "foursquare_id": "4e6f2cec483bad563d150f98" + "foursquare_id": "4e6f2cec483bad563d150f98", } SHIPPING_ADDRESS = { @@ -141,7 +141,7 @@ SHIPPING_ADDRESS = { "city": "DefaultCity", "street_line1": "Central", "street_line2": "Middle", - "post_code": "424242" + "post_code": "424242", } STICKER = { @@ -156,7 +156,7 @@ STICKER = { "height": 128 }, "file_id": "AAbbCCddEEffGGhh1234567890", - "file_size": 12345 + "file_size": 12345, } SUCCESSFUL_PAYMENT = { @@ -164,7 +164,7 @@ SUCCESSFUL_PAYMENT = { "total_amount": 6250, "invoice_payload": "HAPPY FRIDAYS COUPON", "telegram_payment_charge_id": "_", - "provider_payment_charge_id": "12345678901234_test" + "provider_payment_charge_id": "12345678901234_test", } VIDEO = { @@ -174,7 +174,7 @@ VIDEO = { "mime_type": "video/quicktime", "thumb": PHOTO, "file_id": "BAADAgpAADdawy_JxS72kRvV3cortAg", - "file_size": 10099782 + "file_size": 10099782, } VIDEO_NOTE = { @@ -182,14 +182,14 @@ VIDEO_NOTE = { "length": 240, "thumb": PHOTO, "file_id": "AbCdEfGhIjKlMnOpQrStUvWxYz", - "file_size": 186562 + "file_size": 186562, } VOICE = { "duration": 1, "mime_type": "audio/ogg", "file_id": "AwADawAgADADy_JxS2gopIVIIxlhAg", - "file_size": 4321 + "file_size": 4321, } CALLBACK_QUERY = {} @@ -206,7 +206,7 @@ EDITED_MESSAGE = { "chat": CHAT, "date": 1508825372, "edit_date": 1508825379, - "text": "hi there (edited)" + "text": "hi there (edited)", } FORWARDED_MESSAGE = { @@ -219,7 +219,7 @@ FORWARDED_MESSAGE = { "forward_date": 1522749037, "text": "Forwarded text with entities from public channel ", "entities": [ENTITY_BOLD, ENTITY_CODE, ENTITY_ITALIC, ENTITY_LINK, - ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE] + ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE], } INLINE_QUERY = {} @@ -229,7 +229,7 @@ MESSAGE = { "from": USER, "chat": CHAT, "date": 1508709711, - "text": "Hi, world!" + "text": "Hi, world!", } MESSAGE_WITH_AUDIO = { @@ -238,7 +238,7 @@ MESSAGE_WITH_AUDIO = { "chat": CHAT, "date": 1508739776, "audio": AUDIO, - "caption": "This is my favourite song" + "caption": "This is my favourite song", } MESSAGE_WITH_AUTHOR_SIGNATURE = {} @@ -250,7 +250,7 @@ MESSAGE_WITH_CONTACT = { "from": USER, "chat": CHAT, "date": 1522850298, - "contact": CONTACT + "contact": CONTACT, } MESSAGE_WITH_DELETE_CHAT_PHOTO = {} @@ -261,7 +261,7 @@ MESSAGE_WITH_DOCUMENT = { "chat": CHAT, "date": 1508768012, "document": DOCUMENT, - "caption": "Read my document" + "caption": "Read my document", } MESSAGE_WITH_EDIT_DATE = {} @@ -273,7 +273,7 @@ MESSAGE_WITH_GAME = { "from": USER, "chat": CHAT, "date": 1508824810, - "game": GAME + "game": GAME, } MESSAGE_WITH_GROUP_CHAT_CREATED = {} @@ -283,7 +283,7 @@ MESSAGE_WITH_INVOICE = { "from": USER, "chat": CHAT, "date": 1508761719, - "invoice": INVOICE + "invoice": INVOICE, } MESSAGE_WITH_LEFT_CHAT_MEMBER = {} @@ -293,7 +293,7 @@ MESSAGE_WITH_LOCATION = { "from": USER, "chat": CHAT, "date": 1508755473, - "location": LOCATION + "location": LOCATION, } MESSAGE_WITH_MIGRATE_TO_CHAT_ID = { @@ -301,7 +301,7 @@ MESSAGE_WITH_MIGRATE_TO_CHAT_ID = { "from": USER, "chat": CHAT, "date": 1526943253, - "migrate_to_chat_id": -1234567890987 + "migrate_to_chat_id": -1234567890987, } MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = { @@ -309,7 +309,7 @@ MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = { "from": USER, "chat": CHAT, "date": 1526943253, - "migrate_from_chat_id": -123456789 + "migrate_from_chat_id": -123456789, } MESSAGE_WITH_NEW_CHAT_MEMBERS = {} @@ -324,7 +324,7 @@ MESSAGE_WITH_PHOTO = { "chat": CHAT, "date": 1508825154, "photo": [PHOTO, PHOTO, PHOTO, PHOTO], - "caption": "photo description" + "caption": "photo description", } MESSAGE_WITH_MEDIA_GROUP = { @@ -333,7 +333,7 @@ MESSAGE_WITH_MEDIA_GROUP = { "chat": CHAT, "date": 1522843665, "media_group_id": "12182749320567362", - "photo": [PHOTO, PHOTO, PHOTO, PHOTO] + "photo": [PHOTO, PHOTO, PHOTO, PHOTO], } MESSAGE_WITH_PINNED_MESSAGE = {} @@ -345,7 +345,7 @@ MESSAGE_WITH_STICKER = { "from": USER, "chat": CHAT, "date": 1508771450, - "sticker": STICKER + "sticker": STICKER, } MESSAGE_WITH_SUCCESSFUL_PAYMENT = { @@ -353,7 +353,7 @@ MESSAGE_WITH_SUCCESSFUL_PAYMENT = { "from": USER, "chat": CHAT, "date": 1508761169, - "successful_payment": SUCCESSFUL_PAYMENT + "successful_payment": SUCCESSFUL_PAYMENT, } MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {} @@ -364,7 +364,7 @@ MESSAGE_WITH_VENUE = { "chat": CHAT, "date": 1522849819, "location": LOCATION, - "venue": VENUE + "venue": VENUE, } MESSAGE_WITH_VIDEO = { @@ -373,7 +373,7 @@ MESSAGE_WITH_VIDEO = { "chat": CHAT, "date": 1508756494, "video": VIDEO, - "caption": "description" + "caption": "description", } MESSAGE_WITH_VIDEO_NOTE = { @@ -381,7 +381,7 @@ MESSAGE_WITH_VIDEO_NOTE = { "from": USER, "chat": CHAT, "date": 1522835890, - "video_note": VIDEO_NOTE + "video_note": VIDEO_NOTE, } MESSAGE_WITH_VOICE = { @@ -389,7 +389,7 @@ MESSAGE_WITH_VOICE = { "from": USER, "chat": CHAT, "date": 1508768403, - "voice": VOICE + "voice": VOICE, } PRE_CHECKOUT_QUERY = { @@ -397,7 +397,7 @@ PRE_CHECKOUT_QUERY = { "from": USER, "currency": "USD", "total_amount": 6250, - "invoice_payload": "HAPPY FRIDAYS COUPON" + "invoice_payload": "HAPPY FRIDAYS COUPON", } REPLY_MESSAGE = { @@ -406,37 +406,37 @@ REPLY_MESSAGE = { "chat": CHAT, "date": 1508751866, "reply_to_message": MESSAGE, - "text": "Reply to quoted message" + "text": "Reply to quoted message", } SHIPPING_QUERY = { "id": "262181558684397422", "from": USER, "invoice_payload": "HAPPY FRIDAYS COUPON", - "shipping_address": SHIPPING_ADDRESS + "shipping_address": SHIPPING_ADDRESS, } USER_PROFILE_PHOTOS = { "total_count": 1, "photos": [ - [PHOTO, PHOTO, PHOTO] - ] + [PHOTO, PHOTO, PHOTO], + ], } FILE = { "file_id": "XXXYYYZZZ", "file_size": 5254, - "file_path": "voice\/file_8" + "file_path": "voice/file_8", } INVITE_LINK = 'https://t.me/joinchat/AbCdEfjKILDADwdd123' UPDATE = { "update_id": 123456789, - "message": MESSAGE + "message": MESSAGE, } WEBHOOK_INFO = { "url": "", "has_custom_certificate": False, - "pending_update_count": 0 + "pending_update_count": 0, } From d360538bba17ae8032889ae804307025db90b5e8 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 00:08:05 +0300 Subject: [PATCH 090/128] Some localization changes --- examples/locales/mybot.pot | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/locales/mybot.pot b/examples/locales/mybot.pot index b0736569..e2cf1014 100644 --- a/examples/locales/mybot.pot +++ b/examples/locales/mybot.pot @@ -1,31 +1,27 @@ # Translations template for PROJECT. -# Copyright (C) 2018 ORGANIZATION +# Copyright (C) 2019 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2018. +# FIRST AUTHOR , 2019. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-30 03:50+0300\n" +"POT-Creation-Date: 2019-07-22 20:45+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.6.0\n" +"Generated-By: Babel 2.7.0\n" -#: i18n_example.py:48 +#: i18n_example.py:60 msgid "Hello, {user}!" msgstr "" -#: i18n_example.py:53 +#: i18n_example.py:65 msgid "Your current language: {language}" msgstr "" -msgid "Aiogram has {number} like!" -msgid_plural "Aiogram has {number} likes!" -msgstr[0] "" -msgstr[1] "" From a78b93c5cdeb9918893bb05247b484112ef35037 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 17:55:39 +0300 Subject: [PATCH 091/128] Refactor examples/i18n_example.py --- aiogram/contrib/middlewares/i18n.py | 6 ++--- aiogram/types/user.py | 4 +++- examples/i18n_example.py | 28 ++++++++++++++++++------ examples/locales/mybot.pot | 5 ++--- examples/locales/ru/LC_MESSAGES/mybot.po | 17 +++++++------- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 8373f3d6..0bb10680 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -97,15 +97,13 @@ class I18nMiddleware(BaseMiddleware): if locale not in self.locales: if n is 1: return singular - else: - return plural + return plural translator = self.locales[locale] if plural is None: return translator.gettext(singular) - else: - return translator.ngettext(singular, plural, n) + return translator.ngettext(singular, plural, n) def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy: """ diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 441c275f..27ee27e0 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Optional + import babel from . import base @@ -45,7 +47,7 @@ class User(base.TelegramObject): return self.full_name @property - def locale(self) -> babel.core.Locale or None: + def locale(self) -> Optional[babel.core.Locale]: """ Get user's locale diff --git a/examples/i18n_example.py b/examples/i18n_example.py index bf23c8d1..f69482af 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -54,14 +54,16 @@ dp.middleware.setup(i18n) _ = i18n.gettext -@dp.message_handler(commands=['start']) +@dp.message_handler(commands='start') async def cmd_start(message: types.Message): # Simply use `_('message')` instead of `'message'` and never use f-strings for translatable texts. await message.reply(_('Hello, {user}!').format(user=message.from_user.full_name)) -@dp.message_handler(commands=['lang']) +@dp.message_handler(commands='lang') async def cmd_lang(message: types.Message, locale): + # For setting custom lang you have to modify i18n middleware, like this: + # https://github.com/aiogram/EventsTrackerBot/blob/master/modules/base/middlewares.py await message.reply(_('Your current language: {language}').format(language=locale)) # If you care about pluralization, here's small handler @@ -70,15 +72,27 @@ async def cmd_lang(message: types.Message, locale): # Alias for gettext method, parser will understand double underscore as plural (aka ngettext) __ = i18n.gettext -# Some pseudo numeric value -TOTAL_LIKES = 0 -@dp.message_handler(commands=['like']) +# some likes manager +LIKES_STORAGE = {'count': 0} + + +def get_likes() -> int: + return LIKES_STORAGE['count'] + + +def increase_likes() -> int: + LIKES_STORAGE['count'] += 1 + return get_likes() +# + + +@dp.message_handler(commands='like') async def cmd_like(message: types.Message, locale): - TOTAL_LIKES += 1 + likes = increase_likes() # NOTE: This is comment for a translator - await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', TOTAL_LIKES).format(number=TOTAL_LIKES)) + await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes)) if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) diff --git a/examples/locales/mybot.pot b/examples/locales/mybot.pot index e2cf1014..62b2d425 100644 --- a/examples/locales/mybot.pot +++ b/examples/locales/mybot.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2019-07-22 20:45+0300\n" +"POT-Creation-Date: 2019-08-10 17:51+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -21,7 +21,6 @@ msgstr "" msgid "Hello, {user}!" msgstr "" -#: i18n_example.py:65 +#: i18n_example.py:67 msgid "Your current language: {language}" msgstr "" - diff --git a/examples/locales/ru/LC_MESSAGES/mybot.po b/examples/locales/ru/LC_MESSAGES/mybot.po index 8180af42..9064bc0e 100644 --- a/examples/locales/ru/LC_MESSAGES/mybot.po +++ b/examples/locales/ru/LC_MESSAGES/mybot.po @@ -1,14 +1,14 @@ # Russian translations for PROJECT. -# Copyright (C) 2018 ORGANIZATION +# Copyright (C) 2019 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2018. +# FIRST AUTHOR , 2019. # msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2018-06-30 03:50+0300\n" -"PO-Revision-Date: 2018-06-30 03:43+0300\n" +"POT-Creation-Date: 2019-08-10 17:51+0300\n" +"PO-Revision-Date: 2019-08-10 17:52+0300\n" "Last-Translator: FULL NAME \n" "Language: ru\n" "Language-Team: ru \n" @@ -17,18 +17,19 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.6.0\n" +"Generated-By: Babel 2.7.0\n" -#: i18n_example.py:48 +#: i18n_example.py:60 msgid "Hello, {user}!" msgstr "Привет, {user}!" -#: i18n_example.py:53 +#: i18n_example.py:67 msgid "Your current language: {language}" msgstr "Твой язык: {language}" +#: i18n_example.py:95 msgid "Aiogram has {number} like!" msgid_plural "Aiogram has {number} likes!" msgstr[0] "Aiogram имеет {number} лайк!" msgstr[1] "Aiogram имеет {number} лайка!" -msgstr[2] "Aiogram имеет {number} лайков!" \ No newline at end of file +msgstr[2] "Aiogram имеет {number} лайков!" From 5846d3a6c2aa8ae9b60b86247d4cf200d8c0a01a Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 18:41:29 +0300 Subject: [PATCH 092/128] More minor refactoring --- aiogram/dispatcher/dispatcher.py | 49 +++++++++++++++++--------- aiogram/dispatcher/filters/__init__.py | 2 +- aiogram/dispatcher/filters/builtin.py | 4 +-- aiogram/dispatcher/filters/factory.py | 2 +- aiogram/dispatcher/filters/filters.py | 2 +- aiogram/dispatcher/handler.py | 7 ++-- 6 files changed, 42 insertions(+), 24 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 8236118e..24e13801 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -85,39 +85,56 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(StateFilter, exclude_event_handlers=[ self.errors_handlers, - self.poll_handlers + self.poll_handlers, ]) filters_factory.bind(ContentTypeFilter, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, ]), filters_factory.bind(Command, event_handlers=[ - self.message_handlers, self.edited_message_handlers + self.message_handlers, + self.edited_message_handlers ]) filters_factory.bind(Text, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers, + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + self.callback_query_handlers, + self.poll_handlers, + self.inline_query_handlers, ]) filters_factory.bind(HashTag, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, ]) filters_factory.bind(Regexp, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.poll_handlers, self.inline_query_handlers, + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + self.callback_query_handlers, + self.poll_handlers, + self.inline_query_handlers, ]) filters_factory.bind(RegexpCommandsFilter, event_handlers=[ - self.message_handlers, self.edited_message_handlers, + self.message_handlers, + self.edited_message_handlers, ]) filters_factory.bind(ExceptionsFilter, event_handlers=[ self.errors_handlers, ]) filters_factory.bind(IdFilter, event_handlers=[ - self.message_handlers, self.edited_message_handlers, - self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers, self.inline_query_handlers + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + self.callback_query_handlers, + self.inline_query_handlers, ]) def __del__(self): diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index eb4a5a52..374bcf2a 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -27,5 +27,5 @@ __all__ = [ 'get_filter_spec', 'get_filters_spec', 'execute_filter', - 'check_filters' + 'check_filters', ] diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 9a44de23..b0324290 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -558,9 +558,9 @@ class IdFilter(Filter): if self.user_id and self.chat_id: return user_id in self.user_id and chat_id in self.chat_id - elif self.user_id: + if self.user_id: return user_id in self.user_id - elif self.chat_id: + if self.chat_id: return chat_id in self.chat_id return False diff --git a/aiogram/dispatcher/filters/factory.py b/aiogram/dispatcher/filters/factory.py index 89e3e792..13b188ff 100644 --- a/aiogram/dispatcher/filters/factory.py +++ b/aiogram/dispatcher/filters/factory.py @@ -70,4 +70,4 @@ class FiltersFactory: yield filter_ if full_config: - raise NameError('Invalid filter name(s): \'' + '\', '.join(full_config.keys()) + '\'') + raise NameError("Invalid filter name(s): '" + "', ".join(full_config.keys()) + "'") diff --git a/aiogram/dispatcher/filters/filters.py b/aiogram/dispatcher/filters/filters.py index 4806c55a..220ef96c 100644 --- a/aiogram/dispatcher/filters/filters.py +++ b/aiogram/dispatcher/filters/filters.py @@ -82,7 +82,7 @@ class FilterRecord: Filters record for factory """ - def __init__(self, callback: typing.Callable, + def __init__(self, callback: typing.Union[typing.Callable, 'AbstractFilter'], validator: typing.Optional[typing.Callable] = None, event_handlers: typing.Optional[typing.Iterable[Handler]] = None, exclude_event_handlers: typing.Optional[typing.Iterable[Handler]] = None): diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 889dc8d6..cd5e9b50 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -1,7 +1,7 @@ import inspect from contextvars import ContextVar from dataclasses import dataclass -from typing import Optional, Iterable +from typing import Optional, Iterable, List ctx_data = ContextVar('ctx_handler_data') current_handler = ContextVar('current_handler') @@ -41,11 +41,10 @@ class Handler: self.dispatcher = dispatcher self.once = once - self.handlers = [] + self.handlers: List[Handler.HandlerObj] = [] self.middleware_key = middleware_key def register(self, handler, filters=None, index=None): - from .filters import get_filters_spec """ Register callback @@ -55,6 +54,8 @@ class Handler: :param filters: list of filters :param index: you can reorder handlers """ + from .filters import get_filters_spec + spec = _get_spec(handler) if filters and not isinstance(filters, (list, tuple, set)): From a4f8dc907a9fc9e626627e142051551d0829dd1b Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 18:41:50 +0300 Subject: [PATCH 093/128] Refactor examples/id_filter_example.py --- examples/id_filter_example.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/examples/id_filter_example.py b/examples/id_filter_example.py index 64dc3b3f..343253e3 100644 --- a/examples/id_filter_example.py +++ b/examples/id_filter_example.py @@ -1,37 +1,35 @@ from aiogram import Bot, Dispatcher, executor, types from aiogram.dispatcher.handler import SkipHandler -API_TOKEN = 'API_TOKE_HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) -user_id_to_test = None # todo: Set id here -chat_id_to_test = user_id_to_test +user_id_required = None # TODO: Set id here +chat_id_required = user_id_required # Change for use in groups (user_id == chat_id in pm) -@dp.message_handler(user_id=user_id_to_test) +@dp.message_handler(user_id=user_id_required) async def handler1(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello, checking with user_id=") - raise SkipHandler + await bot.send_message(msg.chat.id, "Hello, checking with user_id=") + raise SkipHandler # just for demo -@dp.message_handler(chat_id=chat_id_to_test) +@dp.message_handler(chat_id=chat_id_required) async def handler2(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello, checking with chat_id=") - raise SkipHandler + await bot.send_message(msg.chat.id, "Hello, checking with chat_id=") + raise SkipHandler # just for demo -@dp.message_handler(user_id=user_id_to_test, chat_id=chat_id_to_test) +@dp.message_handler(user_id=user_id_required, chat_id=chat_id_required) async def handler3(msg: types.Message): - await bot.send_message(msg.chat.id, - "Hello from user= & chat_id=") + await msg.reply("Hello from user= & chat_id=", reply=False) -@dp.message_handler(user_id=[user_id_to_test, 123]) # todo: add second id here +@dp.message_handler(user_id=[user_id_required, 42]) # TODO: You can add any number of ids here async def handler4(msg: types.Message): - print("Checked user_id with list!") + await msg.reply("Checked user_id with list!", reply=False) if __name__ == '__main__': From c6871f8071906dcc21f6a3bf55ad2f3b894e2b59 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 20:07:15 +0300 Subject: [PATCH 094/128] Refactor examples/inline_bot.py --- examples/inline_bot.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/examples/inline_bot.py b/examples/inline_bot.py index f1a81bb4..28f83e43 100644 --- a/examples/inline_bot.py +++ b/examples/inline_bot.py @@ -1,9 +1,11 @@ -import asyncio +import hashlib import logging -from aiogram import Bot, types, Dispatcher, executor +from aiogram import Bot, Dispatcher, executor +from aiogram.types import InlineQuery, \ + InputTextMessageContent, InlineQueryResultArticle -API_TOKEN = 'BOT TOKEN HERE' +API_TOKEN = 'BOT_TOKEN_HERE' logging.basicConfig(level=logging.DEBUG) @@ -12,10 +14,22 @@ dp = Dispatcher(bot) @dp.inline_handler() -async def inline_echo(inline_query: types.InlineQuery): - input_content = types.InputTextMessageContent(inline_query.query or 'echo') - item = types.InlineQueryResultArticle(id='1', title='echo', - input_message_content=input_content) +async def inline_echo(inline_query: InlineQuery): + # id affects both preview and content, + # so it has to be unique for each result + # (Unique identifier for this result, 1-64 Bytes) + # you can set your unique id's + # but for example i'll generate it based on text because I know, that + # only text will be passed in this example + text = inline_query.query or 'echo' + input_content = InputTextMessageContent(text) + result_id: str = hashlib.md5(text.encode()).hexdigest() + item = InlineQueryResultArticle( + id=result_id, + title=f'Result {text!r}', + input_message_content=input_content, + ) + # don't forget to set cache_time=1 for testing (default is 300s or 5m) await bot.answer_inline_query(inline_query.id, results=[item], cache_time=1) From 7a155794e2322f4a24a8c6a2745fa3dbbd6b93e2 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 21:55:52 +0300 Subject: [PATCH 095/128] Refactor examples/inline_keyboard_example.py --- examples/i18n_example.py | 2 +- examples/inline_keyboard_example.py | 44 ++++++++++++++++------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/examples/i18n_example.py b/examples/i18n_example.py index f69482af..3bb624bd 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -37,7 +37,7 @@ from pathlib import Path from aiogram import Bot, Dispatcher, executor, types from aiogram.contrib.middlewares.i18n import I18nMiddleware -TOKEN = 'BOT TOKEN HERE' +TOKEN = 'BOT_TOKEN_HERE' I18N_DOMAIN = 'mybot' BASE_DIR = Path(__file__).parent diff --git a/examples/inline_keyboard_example.py b/examples/inline_keyboard_example.py index 2478b9e0..8b950f98 100644 --- a/examples/inline_keyboard_example.py +++ b/examples/inline_keyboard_example.py @@ -6,50 +6,56 @@ import logging from aiogram import Bot, Dispatcher, executor, types + API_TOKEN = 'BOT_TOKEN_HERE' # Configure logging logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) # Initialize bot and dispatcher bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) -@dp.message_handler(commands=['start']) +@dp.message_handler(commands='start') async def start_cmd_handler(message: types.Message): keyboard_markup = types.InlineKeyboardMarkup(row_width=3) # default row_width is 3, so here we can omit it actually # kept for clearness - keyboard_markup.row(types.InlineKeyboardButton("Yes!", callback_data='yes'), - # in real life for the callback_data the callback data factory should be used - # here the raw string is used for the simplicity - types.InlineKeyboardButton("No!", callback_data='no')) + text_and_data = ( + ('Yes!', 'yes'), + ('No!', 'no'), + ) + # in real life for the callback_data the callback data factory should be used + # here the raw string is used for the simplicity + row_btns = (types.InlineKeyboardButton(text, callback_data=data) for text, data in text_and_data) - keyboard_markup.add(types.InlineKeyboardButton("aiogram link", - url='https://github.com/aiogram/aiogram')) - # url buttons has no callback data + keyboard_markup.row(*row_btns) + keyboard_markup.add( + # url buttons have no callback data + types.InlineKeyboardButton('aiogram source', url='https://github.com/aiogram/aiogram'), + ) await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) -@dp.callback_query_handler(lambda cb: cb.data in ['yes', 'no']) # if cb.data is either 'yes' or 'no' -# @dp.callback_query_handler(text='yes') # if cb.data == 'yes' +# Use multiple registrators. Handler will execute when one of the filters is OK +@dp.callback_query_handler(text='no') # if cb.data == 'no' +@dp.callback_query_handler(text='yes') # if cb.data == 'yes' async def inline_kb_answer_callback_handler(query: types.CallbackQuery): - await query.answer() # send answer to close the rounding circle - answer_data = query.data - logger.debug(f"answer_data={answer_data}") - # here we can work with query.data + # always answer callback queries, even if you have nothing to say + await query.answer(f'You answered with {answer_data!r}') + if answer_data == 'yes': - await bot.send_message(query.from_user.id, "That's great!") + text = 'Great, me too!' elif answer_data == 'no': - await bot.send_message(query.from_user.id, "Oh no...Why so?") + text = 'Oh no...Why so?' else: - await bot.send_message(query.from_user.id, "Invalid callback data!") + text = f'Unexpected callback data {answer_data!r}!' + + await bot.send_message(query.from_user.id, text) if __name__ == '__main__': From 95fcaaeed798e34b974245667cf5fc14ef05efbe Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sat, 10 Aug 2019 22:09:29 +0300 Subject: [PATCH 096/128] Micro refactor --- aiogram/dispatcher/filters/builtin.py | 10 +++++----- examples/media_group.py | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index b0324290..2e561bcd 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -149,7 +149,7 @@ class CommandStart(Command): :param deep_link: string or compiled regular expression (by ``re.compile(...)``). """ - super(CommandStart, self).__init__(['start']) + super().__init__(['start']) self.deep_link = deep_link async def check(self, message: types.Message): @@ -159,7 +159,7 @@ class CommandStart(Command): :param message: :return: """ - check = await super(CommandStart, self).check(message) + check = await super().check(message) if check and self.deep_link is not None: if not isinstance(self.deep_link, re.Pattern): @@ -179,7 +179,7 @@ class CommandHelp(Command): """ def __init__(self): - super(CommandHelp, self).__init__(['help']) + super().__init__(['help']) class CommandSettings(Command): @@ -188,7 +188,7 @@ class CommandSettings(Command): """ def __init__(self): - super(CommandSettings, self).__init__(['settings']) + super().__init__(['settings']) class CommandPrivacy(Command): @@ -197,7 +197,7 @@ class CommandPrivacy(Command): """ def __init__(self): - super(CommandPrivacy, self).__init__(['privacy']) + super().__init__(['privacy']) class Text(Filter): diff --git a/examples/media_group.py b/examples/media_group.py index eafbac6a..3d488364 100644 --- a/examples/media_group.py +++ b/examples/media_group.py @@ -2,7 +2,8 @@ import asyncio from aiogram import Bot, Dispatcher, executor, filters, types -API_TOKEN = 'BOT TOKEN HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) @@ -13,10 +14,10 @@ async def send_welcome(message: types.Message): # So... At first I want to send something like this: await message.reply("Do you want to see many pussies? Are you ready?") - # And wait few seconds... + # Wait a little... await asyncio.sleep(1) - # Good bots should send chat actions. Or not. + # Good bots should send chat actions... await types.ChatActions.upload_photo() # Create media group From 189753cf6747be7303d547e41dc05e3518c43bd3 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 10 Aug 2019 23:42:49 +0300 Subject: [PATCH 097/128] Add renamed_argument decorator for convenient warnings about deprecated arguments --- aiogram/utils/deprecated.py | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 1ea2561d..792a4d17 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -5,6 +5,7 @@ Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-sta import functools import inspect import warnings +import asyncio def deprecated(reason): @@ -73,3 +74,57 @@ def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): warnings.simplefilter('always', warning) warnings.warn(message, category=warning, stacklevel=stacklevel) warnings.simplefilter('default', warning) + + +def renamed_argument(old_name: str, new_name: str, until_version: str): + """ + A meta-decorator to mark some arguments in function as deprecated. + + .. code-block:: python3 + + @renamed_argument("user", "user_id", "3.0") + def some_function(user_id): + print(f"user_id={user_id}") + + some_function(user=123) # prints 'user_id=123' with warning + some_function(123) # prints 'user_id=123' without warning + some_function(user_id=123) # prints 'user_id=123' without warning + + + :param old_name: + :param new_name: + :param until_version: the version in which the argument is scheduled to be removed + :return: decorator + """ + + def decorator(func): + if asyncio.iscoroutinefunction(func): + @functools.wraps(func) + async def wrapped(*args, **kwargs): + if old_name in kwargs: + warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' is renamed to '{new_name}' " + f"and will be removed in aiogram {until_version}", stacklevel=3) + kwargs.update( + { + new_name: kwargs[old_name], + } + ) + kwargs.pop(old_name) + await func(*args, **kwargs) + else: + @functools.wraps(func) + def wrapped(*args, **kwargs): + if old_name in kwargs: + warn_deprecated(f"In function '{func.__name__}' argument '{old_name}' is renamed to '{new_name}' " + f"and will be removed in aiogram {until_version}", stacklevel=3) + kwargs.update( + { + new_name: kwargs[old_name], + } + ) + kwargs.pop(old_name) + func(*args, **kwargs) + + return wrapped + + return decorator From 05bfb37e18d4afd3082c0356063e29bc5e505f30 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 10 Aug 2019 23:44:45 +0300 Subject: [PATCH 098/128] Add docs for deprecated --- docs/source/utils/deprecated.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/utils/deprecated.rst b/docs/source/utils/deprecated.rst index 0a7b4089..ce80bb0e 100644 --- a/docs/source/utils/deprecated.rst +++ b/docs/source/utils/deprecated.rst @@ -1,4 +1,8 @@ ========== Deprecated ========== -Coming soon... +.. literalinclude:: ../../../aiogram/utils/deprecated.py + :caption: deprecated.py + :language: python3 + :linenos: + From cc601a7e0d1152ce0de543f45c663df785856937 Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 11 Aug 2019 00:07:30 +0300 Subject: [PATCH 099/128] add support of stacklevel parameter in renamed_argument decorator --- aiogram/utils/deprecated.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 792a4d17..4b954c56 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -76,24 +76,27 @@ def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): warnings.simplefilter('default', warning) -def renamed_argument(old_name: str, new_name: str, until_version: str): +def renamed_argument(old_name: str, new_name: str, until_version: str, stacklevel: int = 3): """ A meta-decorator to mark some arguments in function as deprecated. .. code-block:: python3 - @renamed_argument("user", "user_id", "3.0") - def some_function(user_id): - print(f"user_id={user_id}") + @renamed_argument("chat", "chat_id", "3.0") # stacklevel=3 by default + @renamed_argument("user", "user_id", "3.0", stacklevel=4) + def some_function(user_id, chat_id=None): + print(f"user_id={user_id}, chat_id={chat_id}") - some_function(user=123) # prints 'user_id=123' with warning - some_function(123) # prints 'user_id=123' without warning - some_function(user_id=123) # prints 'user_id=123' without warning + some_function(user=123) # prints 'user_id=123, chat_id=None' with warning + some_function(123) # prints 'user_id=123, chat_id=None' without warning + some_function(user_id=123) # prints 'user_id=123, chat_id=None' without warning :param old_name: :param new_name: :param until_version: the version in which the argument is scheduled to be removed + :param stacklevel: leave it to default if it's the first decorator used. + Increment with any new decorator used. :return: decorator """ @@ -102,8 +105,10 @@ def renamed_argument(old_name: str, new_name: str, until_version: str): @functools.wraps(func) async def wrapped(*args, **kwargs): if old_name in kwargs: - warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' is renamed to '{new_name}' " - f"and will be removed in aiogram {until_version}", stacklevel=3) + warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' " + f"is renamed to '{new_name}' " + f"and will be removed in aiogram {until_version}", + stacklevel=stacklevel) kwargs.update( { new_name: kwargs[old_name], @@ -115,8 +120,10 @@ def renamed_argument(old_name: str, new_name: str, until_version: str): @functools.wraps(func) def wrapped(*args, **kwargs): if old_name in kwargs: - warn_deprecated(f"In function '{func.__name__}' argument '{old_name}' is renamed to '{new_name}' " - f"and will be removed in aiogram {until_version}", stacklevel=3) + warn_deprecated(f"In function '{func.__name__}' argument '{old_name}' " + f"is renamed to '{new_name}' " + f"and will be removed in aiogram {until_version}", + stacklevel=stacklevel) kwargs.update( { new_name: kwargs[old_name], From f750ea13f5660889747a1f887e2849878739304b Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 11 Aug 2019 00:09:44 +0300 Subject: [PATCH 100/128] fix typo --- aiogram/utils/deprecated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 4b954c56..3917c72c 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -78,7 +78,7 @@ def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): def renamed_argument(old_name: str, new_name: str, until_version: str, stacklevel: int = 3): """ - A meta-decorator to mark some arguments in function as deprecated. + A meta-decorator to mark an argument as deprecated. .. code-block:: python3 From e1cd68d4d3a4ab286398df2a2450073f27593920 Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 11 Aug 2019 01:15:03 +0300 Subject: [PATCH 101/128] fix quotes --- aiogram/utils/deprecated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 3917c72c..89081c8b 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -120,8 +120,8 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve @functools.wraps(func) def wrapped(*args, **kwargs): if old_name in kwargs: - warn_deprecated(f"In function '{func.__name__}' argument '{old_name}' " - f"is renamed to '{new_name}' " + warn_deprecated(f"In function `{func.__name__}` argument `{old_name}` " + f"is renamed to `{new_name}` " f"and will be removed in aiogram {until_version}", stacklevel=stacklevel) kwargs.update( From 68aa7dbf1a72dce7df87b22c227224efb558c7ad Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 15:30:01 +0300 Subject: [PATCH 102/128] Refactor examples/payments.py --- examples/middleware_and_antiflood.py | 2 +- examples/payments.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/middleware_and_antiflood.py b/examples/middleware_and_antiflood.py index 4a0cc491..72fabf55 100644 --- a/examples/middleware_and_antiflood.py +++ b/examples/middleware_and_antiflood.py @@ -7,7 +7,7 @@ from aiogram.dispatcher.handler import CancelHandler, current_handler from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram.utils.exceptions import Throttled -TOKEN = 'BOT TOKEN HERE' +TOKEN = 'BOT_TOKEN_HERE' # In this example Redis storage is used storage = RedisStorage2(db=5) diff --git a/examples/payments.py b/examples/payments.py index a01fbaf3..42162578 100644 --- a/examples/payments.py +++ b/examples/payments.py @@ -1,13 +1,12 @@ -import asyncio - from aiogram import Bot from aiogram import types from aiogram.dispatcher import Dispatcher from aiogram.types.message import ContentTypes from aiogram.utils import executor -BOT_TOKEN = 'BOT TOKEN HERE' -PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1234567890abcdef1234567890abcdef' + +BOT_TOKEN = 'BOT_TOKEN_HERE' +PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1422' bot = Bot(BOT_TOKEN) dp = Dispatcher(bot) @@ -15,13 +14,13 @@ dp = Dispatcher(bot) # Setup prices prices = [ types.LabeledPrice(label='Working Time Machine', amount=5750), - types.LabeledPrice(label='Gift wrapping', amount=500) + types.LabeledPrice(label='Gift wrapping', amount=500), ] # Setup shipping options shipping_options = [ types.ShippingOption(id='instant', title='WorldWide Teleporter').add(types.LabeledPrice('Teleporter', 1000)), - types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300)) + types.ShippingOption(id='pickup', title='Local pickup').add(types.LabeledPrice('Pickup', 300)), ] @@ -59,7 +58,7 @@ async def cmd_buy(message: types.Message): ' Order our Working Time Machine today!', provider_token=PAYMENTS_PROVIDER_TOKEN, currency='usd', - photo_url='https://images.fineartamerica.com/images-medium-large/2-the-time-machine-dmitriy-khristenko.jpg', + photo_url='https://telegra.ph/file/d08ff863531f10bf2ea4b.jpg', photo_height=512, # !=0/None or picture won't be shown photo_width=512, photo_size=512, @@ -69,14 +68,14 @@ async def cmd_buy(message: types.Message): payload='HAPPY FRIDAYS COUPON') -@dp.shipping_query_handler(func=lambda query: True) +@dp.shipping_query_handler(lambda query: True) async def shipping(shipping_query: types.ShippingQuery): await bot.answer_shipping_query(shipping_query.id, ok=True, shipping_options=shipping_options, error_message='Oh, seems like our Dog couriers are having a lunch right now.' ' Try again later!') -@dp.pre_checkout_query_handler(func=lambda query: True) +@dp.pre_checkout_query_handler(lambda query: True) async def checkout(pre_checkout_query: types.PreCheckoutQuery): await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True, error_message="Aliens tried to steal your card's CVV," @@ -95,4 +94,4 @@ async def got_payment(message: types.Message): if __name__ == '__main__': - executor.start_polling(dp) + executor.start_polling(dp, skip_updates=True) From a93fb463820fc6924bc22ffeb1c1d2a579a0f3f0 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 15:49:27 +0300 Subject: [PATCH 103/128] Refactor examples/proxy_and_emojize.py --- examples/proxy_and_emojize.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py index 17e33872..5ef40608 100644 --- a/examples/proxy_and_emojize.py +++ b/examples/proxy_and_emojize.py @@ -1,4 +1,3 @@ -import asyncio import logging import aiohttp @@ -11,13 +10,13 @@ from aiogram.utils.executor import start_polling from aiogram.utils.markdown import bold, code, italic, text # Configure bot here -API_TOKEN = 'BOT TOKEN HERE' -PROXY_URL = 'http://PROXY_URL' # Or 'socks5://...' +API_TOKEN = 'BOT_TOKEN_HERE' +PROXY_URL = 'http://PROXY_URL' # Or 'socks5://host:port' -# If authentication is required in your proxy then uncomment next line and change login/password for it +# NOTE: If authentication is required in your proxy then uncomment next line and change login/password for it # PROXY_AUTH = aiohttp.BasicAuth(login='login', password='password') -# And add `proxy_auth=PROXY_AUTH` argument in line 25, like this: -# >>> bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL, proxy_auth=PROXY_AUTH) +# And add `proxy_auth=PROXY_AUTH` argument in line 30, like this: +# >>> bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH) # Also you can use Socks5 proxy but you need manually install aiohttp_socks package. # Get my ip URL @@ -26,26 +25,32 @@ GET_IP_URL = 'http://bot.whatismyipaddress.com/' logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN, proxy=PROXY_URL) + +# If auth is required: +# bot = Bot(token=API_TOKEN, proxy=PROXY_URL, proxy_auth=PROXY_AUTH) dp = Dispatcher(bot) -async def fetch(url, proxy=None, proxy_auth=None): - async with aiohttp.ClientSession() as session: - async with session.get(url, proxy=proxy, proxy_auth=proxy_auth) as response: - return await response.text() +async def fetch(url, session): + async with session.get(url) as response: + return await response.text() @dp.message_handler(commands=['start']) async def cmd_start(message: types.Message): + # fetching urls will take some time, so notify user that everything is OK + await types.ChatActions.typing() + content = [] # Make request (without proxy) - ip = await fetch(GET_IP_URL) + async with aiohttp.ClientSession() as session: + ip = await fetch(GET_IP_URL, session) content.append(text(':globe_showing_Americas:', bold('IP:'), code(ip))) # This line is formatted to '🌎 *IP:* `YOUR IP`' - # Make request through proxy - ip = await fetch(GET_IP_URL, bot.proxy, bot.proxy_auth) + # Make request through bot's proxy + ip = await fetch(GET_IP_URL, bot.session) content.append(text(':locked_with_key:', bold('IP:'), code(ip), italic('via proxy'))) # This line is formatted to '🔐 *IP:* `YOUR IP` _via proxy_' From 747c87631ed28dbf6da49ff48b86cdcd3bde0457 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 16:26:29 +0300 Subject: [PATCH 104/128] Refactor examples/regexp_commands_filter_example.py --- examples/regexp_commands_filter_example.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/regexp_commands_filter_example.py b/examples/regexp_commands_filter_example.py index 86ccba55..05de9dd8 100644 --- a/examples/regexp_commands_filter_example.py +++ b/examples/regexp_commands_filter_example.py @@ -2,14 +2,28 @@ from aiogram import Bot, types from aiogram.dispatcher import Dispatcher, filters from aiogram.utils import executor -bot = Bot(token='TOKEN') + +bot = Bot(token='BOT_TOKEN_HERE', parse_mode=types.ParseMode.HTML) dp = Dispatcher(bot) @dp.message_handler(filters.RegexpCommandsFilter(regexp_commands=['item_([0-9]*)'])) async def send_welcome(message: types.Message, regexp_command): - await message.reply("You have requested an item with number: {}".format(regexp_command.group(1))) + await message.reply(f"You have requested an item with id {regexp_command.group(1)}") + + +@dp.message_handler(commands='start') +async def create_deeplink(message: types.Message): + bot_user = await bot.me + bot_username = bot_user.username + deeplink = f'https://t.me/{bot_username}?start=item_12345' + text = ( + f'Either send a command /item_1234 or follow this link {deeplink} and then click start\n' + 'It also can be hidden in a inline button\n\n' + 'Or just send /start item_123' + ) + await message.reply(text, disable_web_page_preview=True) if __name__ == '__main__': - executor.start_polling(dp) + executor.start_polling(dp, skip_updates=True) From 8538e6af573429dc374271d6e746c370bd206e7b Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 21:52:58 +0300 Subject: [PATCH 105/128] Refactor examples/regular_keyboard_example.py --- examples/regular_keyboard_example.py | 38 ++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/examples/regular_keyboard_example.py b/examples/regular_keyboard_example.py index 350e007e..d111053c 100644 --- a/examples/regular_keyboard_example.py +++ b/examples/regular_keyboard_example.py @@ -6,6 +6,7 @@ import logging from aiogram import Bot, Dispatcher, executor, types + API_TOKEN = 'BOT_TOKEN_HERE' # Configure logging @@ -18,24 +19,27 @@ bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) -@dp.message_handler(commands=['start']) +@dp.message_handler(commands='start') async def start_cmd_handler(message: types.Message): keyboard_markup = types.ReplyKeyboardMarkup(row_width=3) # default row_width is 3, so here we can omit it actually # kept for clearness - keyboard_markup.row(types.KeyboardButton("Yes!"), - types.KeyboardButton("No!")) + btns_text = ('Yes!', 'No!') + keyboard_markup.row(*(types.KeyboardButton(text) for text in btns_text)) # adds buttons as a new row to the existing keyboard # the behaviour doesn't depend on row_width attribute - keyboard_markup.add(types.KeyboardButton("I don't know"), - types.KeyboardButton("Who am i?"), - types.KeyboardButton("Where am i?"), - types.KeyboardButton("Who is there?")) - # adds buttons. New rows is formed according to row_width parameter + more_btns_text = ( + "I don't know", + "Who am i?", + "Where am i?", + "Who is there?", + ) + keyboard_markup.add(*(types.KeyboardButton(text) for text in more_btns_text)) + # adds buttons. New rows are formed according to row_width parameter - await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) + await message.reply("Hi!\nDo you like aiogram?", reply_markup=keyboard_markup) @dp.message_handler() @@ -45,15 +49,17 @@ async def all_msg_handler(message: types.Message): # in real bot, it's better to define message_handler(text="...") for each button # but here for the simplicity only one handler is defined - text_of_button = message.text - logger.debug(text_of_button) # print the text we got + button_text = message.text + logger.debug('The answer is %r', button_text) # print the text we've got - if text_of_button == 'Yes!': - await message.reply("That's great", reply_markup=types.ReplyKeyboardRemove()) - elif text_of_button == 'No!': - await message.reply("Oh no! Why?", reply_markup=types.ReplyKeyboardRemove()) + if button_text == 'Yes!': + reply_text = "That's great" + elif button_text == 'No!': + reply_text = "Oh no! Why?" else: - await message.reply("Keep calm...Everything is fine", reply_markup=types.ReplyKeyboardRemove()) + reply_text = "Keep calm...Everything is fine" + + await message.reply(reply_text, reply_markup=types.ReplyKeyboardRemove()) # with message, we send types.ReplyKeyboardRemove() to hide the keyboard From 94811c57e101e2589d1dd6e26def555ab16dabe0 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 21:59:56 +0300 Subject: [PATCH 106/128] Minor refactor examples/text_filter_example.py and examples/throtling_example.py --- examples/text_filter_example.py | 7 +++++-- examples/throtling_example.py | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/text_filter_example.py b/examples/text_filter_example.py index a47a5c92..60d631e3 100644 --- a/examples/text_filter_example.py +++ b/examples/text_filter_example.py @@ -7,7 +7,8 @@ import logging from aiogram import Bot, Dispatcher, executor, types -API_TOKEN = 'API_TOKEN_HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' # Configure logging logging.basicConfig(level=logging.INFO) @@ -16,10 +17,12 @@ logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) + # if the text from user in the list @dp.message_handler(text=['text1', 'text2']) async def text_in_handler(message: types.Message): - await message.answer("The message text is in the list!") + await message.answer("The message text equals to one of in the list!") + # if the text contains any string @dp.message_handler(text_contains='example1') diff --git a/examples/throtling_example.py b/examples/throtling_example.py index 2641b44b..18563472 100644 --- a/examples/throtling_example.py +++ b/examples/throtling_example.py @@ -4,7 +4,6 @@ Example for throttling manager. You can use that for flood controlling. """ -import asyncio import logging from aiogram import Bot, types @@ -13,14 +12,15 @@ from aiogram.dispatcher import Dispatcher from aiogram.utils.exceptions import Throttled from aiogram.utils.executor import start_polling -API_TOKEN = 'BOT TOKEN HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN) # Throttling manager does not work without Leaky Bucket. -# Then need to use storages. For example use simple in-memory storage. +# You need to use a storage. For example use simple in-memory storage. storage = MemoryStorage() dp = Dispatcher(bot, storage=storage) From fdf3c448608950433d7cb1e9e73ebdeb5c4d4e48 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 22:50:58 +0300 Subject: [PATCH 107/128] Rename old webhook example --- examples/{webhook_example.py => webhook_example_old.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{webhook_example.py => webhook_example_old.py} (100%) diff --git a/examples/webhook_example.py b/examples/webhook_example_old.py similarity index 100% rename from examples/webhook_example.py rename to examples/webhook_example_old.py From 251760a0325647978207feec4065bdc7f6dfdab8 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 22:51:50 +0300 Subject: [PATCH 108/128] Rename webhook example --- examples/{webhook_example_2.py => webhook_example.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{webhook_example_2.py => webhook_example.py} (100%) diff --git a/examples/webhook_example_2.py b/examples/webhook_example.py similarity index 100% rename from examples/webhook_example_2.py rename to examples/webhook_example.py From 58a5da925ab5f89010ff5ac9f43cf8356c5db1b1 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 22:52:54 +0300 Subject: [PATCH 109/128] Refactor examples/webhook_example.py --- examples/webhook_example.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/examples/webhook_example.py b/examples/webhook_example.py index e2d9225c..6efa8767 100644 --- a/examples/webhook_example.py +++ b/examples/webhook_example.py @@ -1,11 +1,13 @@ -import asyncio import logging from aiogram import Bot, types +from aiogram.contrib.middlewares.logging import LoggingMiddleware from aiogram.dispatcher import Dispatcher +from aiogram.dispatcher.webhook import SendMessage from aiogram.utils.executor import start_webhook -API_TOKEN = 'BOT TOKEN HERE' + +API_TOKEN = 'BOT_TOKEN_HERE' # webhook settings WEBHOOK_HOST = 'https://your.domain' @@ -20,11 +22,16 @@ logging.basicConfig(level=logging.INFO) bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) +dp.middleware.setup(LoggingMiddleware()) @dp.message_handler() async def echo(message: types.Message): - await bot.send_message(message.chat.id, message.text) + # Regular request + # await bot.send_message(message.chat.id, message.text) + + # or reply INTO webhook + return SendMessage(message.chat.id, message.text) async def on_startup(dp): @@ -33,10 +40,27 @@ async def on_startup(dp): async def on_shutdown(dp): + logging.warning('Shutting down..') + # insert code here to run it before shutdown - pass + + # Remove webhook (not acceptable in some cases) + await bot.delete_webhook() + + # Close DB connection (if used) + await dp.storage.close() + await dp.storage.wait_closed() + + logging.warning('Bye!') if __name__ == '__main__': - start_webhook(dispatcher=dp, webhook_path=WEBHOOK_PATH, on_startup=on_startup, on_shutdown=on_shutdown, - skip_updates=True, host=WEBAPP_HOST, port=WEBAPP_PORT) + start_webhook( + dispatcher=dp, + webhook_path=WEBHOOK_PATH, + on_startup=on_startup, + on_shutdown=on_shutdown, + skip_updates=True, + host=WEBAPP_HOST, + port=WEBAPP_PORT, + ) From f4339d10b043a691aad662b80e9e7752af76213a Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 22:55:44 +0300 Subject: [PATCH 110/128] Super minor refactor webhook and executor --- aiogram/dispatcher/webhook.py | 32 ++++++++++++++++---------------- aiogram/utils/executor.py | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index bee635ae..135fe21e 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -523,7 +523,7 @@ class SendMessage(BaseResponse, ReplyToMixin, ParseModeMixin, DisableNotificatio 'disable_web_page_preview': self.disable_web_page_preview, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } def write(self, *text, sep=' '): @@ -642,7 +642,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'caption': self.caption, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -704,7 +704,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'title': self.title, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -817,7 +817,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'caption': self.caption, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -871,7 +871,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'duration': self.duration, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -924,7 +924,7 @@ class SendVideoNote(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'length': self.length, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1050,7 +1050,7 @@ class SendLocation(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'longitude': self.longitude, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1109,7 +1109,7 @@ class SendVenue(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'foursquare_id': self.foursquare_id, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1160,7 +1160,7 @@ class SendContact(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'last_name': self.last_name, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1220,7 +1220,7 @@ class KickChatMember(BaseResponse): return { 'chat_id': self.chat_id, 'user_id': self.user_id, - 'until_date': prepare_arg(self.until_date) + 'until_date': prepare_arg(self.until_date), } @@ -1608,7 +1608,7 @@ class EditMessageText(BaseResponse, ParseModeMixin, DisableWebPagePreviewMixin): 'text': self.text, 'parse_mode': self.parse_mode, 'disable_web_page_preview': self.disable_web_page_preview, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1649,7 +1649,7 @@ class EditMessageCaption(BaseResponse): 'message_id': self.message_id, 'inline_message_id': self.inline_message_id, 'caption': self.caption, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1685,7 +1685,7 @@ class EditMessageReplyMarkup(BaseResponse): 'chat_id': self.chat_id, 'message_id': self.message_id, 'inline_message_id': self.inline_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1756,7 +1756,7 @@ class SendSticker(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'sticker': self.sticker, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } @@ -1848,7 +1848,7 @@ class AddStickerToSet(BaseResponse): 'name': self.name, 'png_sticker': self.png_sticker, 'emojis': self.emojis, - 'mask_position': prepare_arg(self.mask_position) + 'mask_position': prepare_arg(self.mask_position), } @@ -2177,5 +2177,5 @@ class SendGame(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'game_short_name': self.game_short_name, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, - 'reply_markup': prepare_arg(self.reply_markup) + 'reply_markup': prepare_arg(self.reply_markup), } diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index cdb3fe91..854facae 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -15,7 +15,7 @@ from ..dispatcher.webhook import BOT_DISPATCHER_KEY, DEFAULT_ROUTE_NAME, Webhook APP_EXECUTOR_KEY = 'APP_EXECUTOR' -def _setup_callbacks(executor, on_startup=None, on_shutdown=None): +def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None): if on_startup is not None: executor.on_startup(on_startup) if on_shutdown is not None: From 277eb8b701504917853d644a7f986f37ec885507 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 23:32:41 +0300 Subject: [PATCH 111/128] Refactor TextFilter and its tests --- aiogram/dispatcher/filters/builtin.py | 33 ++- tests/test_filters.py | 347 +++++++++----------------- 2 files changed, 133 insertions(+), 247 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 2e561bcd..5b50c23c 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -84,9 +84,9 @@ class Command(Filter): if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower(): return False - elif prefix not in prefixes: + if prefix not in prefixes: return False - elif (command.lower() if ignore_case else command) not in commands: + if (command.lower() if ignore_case else command) not in commands: return False return {'command': Command.CommandObj(command=command, prefix=prefix, mention=mention)} @@ -271,19 +271,26 @@ class Text(Filter): if self.ignore_case: text = text.lower() + _pre_process_func = lambda s: str(s).lower() + else: + _pre_process_func = str + # now check if self.equals is not None: - self.equals = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.equals)) - return text in self.equals - elif self.contains is not None: - self.contains = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.contains)) - return all(map(text.__contains__, self.contains)) - elif self.startswith is not None: - self.startswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.startswith)) - return any(map(text.startswith, self.startswith)) - elif self.endswith is not None: - self.endswith = list(map(lambda s: str(s).lower() if self.ignore_case else str(s), self.endswith)) - return any(map(text.endswith, self.endswith)) + equals = list(map(_pre_process_func, self.equals)) + return text in equals + + if self.contains is not None: + contains = list(map(_pre_process_func, self.contains)) + return all(map(text.__contains__, contains)) + + if self.startswith is not None: + startswith = list(map(_pre_process_func, self.startswith)) + return any(map(text.startswith, startswith)) + + if self.endswith is not None: + endswith = list(map(_pre_process_func, self.endswith)) + return any(map(text.endswith, endswith)) return False diff --git a/tests/test_filters.py b/tests/test_filters.py index 288aa0a4..609db736 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -4,38 +4,35 @@ from aiogram.dispatcher.filters import Text from aiogram.types import Message, CallbackQuery, InlineQuery, Poll +def data_sample_1(): + return [ + ('', ''), + ('', 'exAmple_string'), + + ('example_string', 'example_string'), + ('example_string', 'exAmple_string'), + ('exAmple_string', 'example_string'), + + ('example_string', 'example_string_dsf'), + ('example_string', 'example_striNG_dsf'), + ('example_striNG', 'example_string_dsf'), + + ('example_string', 'not_example_string'), + ('example_string', 'not_eXample_string'), + ('EXample_string', 'not_example_string'), + ] + class TestTextFilter: + + async def _run_check(self, check, test_text): + assert await check(Message(text=test_text)) + assert await check(CallbackQuery(data=test_text)) + assert await check(InlineQuery(query=test_text)) + assert await check(Poll(question=test_text)) + @pytest.mark.asyncio - @pytest.mark.parametrize("test_prefix, test_text, ignore_case", - [('', '', True), - ('', 'exAmple_string', True), - ('', '', False), - ('', 'exAmple_string', False), - - ('example_string', 'example_string', True), - ('example_string', 'exAmple_string', True), - ('exAmple_string', 'example_string', True), - - ('example_string', 'example_string', False), - ('example_string', 'exAmple_string', False), - ('exAmple_string', 'example_string', False), - - ('example_string', 'example_string_dsf', True), - ('example_string', 'example_striNG_dsf', True), - ('example_striNG', 'example_string_dsf', True), - - ('example_string', 'example_string_dsf', False), - ('example_string', 'example_striNG_dsf', False), - ('example_striNG', 'example_string_dsf', False), - - ('example_string', 'not_example_string', True), - ('example_string', 'not_eXample_string', True), - ('EXample_string', 'not_example_string', True), - - ('example_string', 'not_example_string', False), - ('example_string', 'not_eXample_string', False), - ('EXample_string', 'not_example_string', False), - ]) + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_prefix, test_text", data_sample_1()) async def test_startswith(self, test_prefix, test_text, ignore_case): test_filter = Text(startswith=test_prefix, ignore_case=ignore_case) @@ -50,42 +47,26 @@ class TestTextFilter: return result is _test_text.startswith(_test_prefix) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_prefix_list, test_text, ignore_case", - [(['not_example', ''], '', True), - (['', 'not_example'], 'exAmple_string', True), - (['not_example', ''], '', False), - (['', 'not_example'], 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_prefix_list, test_text", [ + (['not_example', ''], ''), + (['', 'not_example'], 'exAmple_string'), - (['example_string', 'not_example'], 'example_string', True), - (['not_example', 'example_string'], 'exAmple_string', True), - (['exAmple_string', 'not_example'], 'example_string', True), + (['not_example', 'example_string'], 'example_string'), + (['example_string', 'not_example'], 'exAmple_string'), + (['not_example', 'exAmple_string'], 'example_string'), - (['not_example', 'example_string'], 'example_string', False), - (['example_string', 'not_example'], 'exAmple_string', False), - (['not_example', 'exAmple_string'], 'example_string', False), + (['not_example', 'example_string'], 'example_string_dsf'), + (['example_string', 'not_example'], 'example_striNG_dsf'), + (['not_example', 'example_striNG'], 'example_string_dsf'), - (['example_string', 'not_example'], 'example_string_dsf', True), - (['not_example', 'example_string'], 'example_striNG_dsf', True), - (['example_striNG', 'not_example'], 'example_string_dsf', True), - - (['not_example', 'example_string'], 'example_string_dsf', False), - (['example_string', 'not_example'], 'example_striNG_dsf', False), - (['not_example', 'example_striNG'], 'example_string_dsf', False), - - (['example_string', 'not_example'], 'not_example_string', True), - (['not_example', 'example_string'], 'not_eXample_string', True), - (['EXample_string', 'not_example'], 'not_example_string', True), - - (['not_example', 'example_string'], 'not_example_string', False), - (['example_string', 'not_example'], 'not_eXample_string', False), - (['not_example', 'EXample_string'], 'not_example_string', False), - ]) + (['not_example', 'example_string'], 'not_example_string'), + (['example_string', 'not_example'], 'not_eXample_string'), + (['not_example', 'EXample_string'], 'not_example_string'), + ]) async def test_startswith_list(self, test_prefix_list, test_text, ignore_case): test_filter = Text(startswith=test_prefix_list, ignore_case=ignore_case) @@ -100,42 +81,11 @@ class TestTextFilter: return result is any(map(_test_text.startswith, _test_prefix_list)) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_postfix, test_text, ignore_case", - [('', '', True), - ('', 'exAmple_string', True), - ('', '', False), - ('', 'exAmple_string', False), - - ('example_string', 'example_string', True), - ('example_string', 'exAmple_string', True), - ('exAmple_string', 'example_string', True), - - ('example_string', 'example_string', False), - ('example_string', 'exAmple_string', False), - ('exAmple_string', 'example_string', False), - - ('example_string', 'example_string_dsf', True), - ('example_string', 'example_striNG_dsf', True), - ('example_striNG', 'example_string_dsf', True), - - ('example_string', 'example_string_dsf', False), - ('example_string', 'example_striNG_dsf', False), - ('example_striNG', 'example_string_dsf', False), - - ('example_string', 'not_example_string', True), - ('example_string', 'not_eXample_string', True), - ('EXample_string', 'not_eXample_string', True), - - ('example_string', 'not_example_string', False), - ('example_string', 'not_eXample_string', False), - ('EXample_string', 'not_example_string', False), - ]) + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_postfix, test_text", data_sample_1()) async def test_endswith(self, test_postfix, test_text, ignore_case): test_filter = Text(endswith=test_postfix, ignore_case=ignore_case) @@ -150,42 +100,26 @@ class TestTextFilter: return result is _test_text.endswith(_test_postfix) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_postfix_list, test_text, ignore_case", - [(['', 'not_example'], '', True), - (['not_example', ''], 'exAmple_string', True), - (['', 'not_example'], '', False), - (['not_example', ''], 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_postfix_list, test_text", [ + (['', 'not_example'], ''), + (['not_example', ''], 'exAmple_string'), - (['example_string', 'not_example'], 'example_string', True), - (['not_example', 'example_string'], 'exAmple_string', True), - (['exAmple_string', 'not_example'], 'example_string', True), + (['example_string', 'not_example'], 'example_string'), + (['not_example', 'example_string'], 'exAmple_string'), + (['exAmple_string', 'not_example'], 'example_string'), - (['example_string', 'not_example'], 'example_string', False), - (['not_example', 'example_string'], 'exAmple_string', False), - (['exAmple_string', 'not_example'], 'example_string', False), + (['not_example', 'example_string'], 'example_string_dsf'), + (['example_string', 'not_example'], 'example_striNG_dsf'), + (['not_example', 'example_striNG'], 'example_string_dsf'), - (['example_string', 'not_example'], 'example_string_dsf', True), - (['not_example', 'example_string'], 'example_striNG_dsf', True), - (['example_striNG', 'not_example'], 'example_string_dsf', True), - - (['not_example', 'example_string'], 'example_string_dsf', False), - (['example_string', 'not_example'], 'example_striNG_dsf', False), - (['not_example', 'example_striNG'], 'example_string_dsf', False), - - (['not_example', 'example_string'], 'not_example_string', True), - (['example_string', 'not_example'], 'not_eXample_string', True), - (['not_example', 'EXample_string'], 'not_eXample_string', True), - - (['not_example', 'example_string'], 'not_example_string', False), - (['example_string', 'not_example'], 'not_eXample_string', False), - (['not_example', 'EXample_string'], 'not_example_string', False), - ]) + (['not_example', 'example_string'], 'not_example_string'), + (['example_string', 'not_example'], 'not_eXample_string'), + (['not_example', 'EXample_string'], 'not_example_string'), + ]) async def test_endswith_list(self, test_postfix_list, test_text, ignore_case): test_filter = Text(endswith=test_postfix_list, ignore_case=ignore_case) @@ -199,42 +133,26 @@ class TestTextFilter: _test_text = test_text return result is any(map(_test_text.endswith, _test_postfix_list)) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_string, test_text, ignore_case", - [('', '', True), - ('', 'exAmple_string', True), - ('', '', False), - ('', 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_string, test_text", [ + ('', ''), + ('', 'exAmple_string'), - ('example_string', 'example_string', True), - ('example_string', 'exAmple_string', True), - ('exAmple_string', 'example_string', True), + ('example_string', 'example_string'), + ('example_string', 'exAmple_string'), + ('exAmple_string', 'example_string'), - ('example_string', 'example_string', False), - ('example_string', 'exAmple_string', False), - ('exAmple_string', 'example_string', False), + ('example_string', 'example_string_dsf'), + ('example_string', 'example_striNG_dsf'), + ('example_striNG', 'example_string_dsf'), - ('example_string', 'example_string_dsf', True), - ('example_string', 'example_striNG_dsf', True), - ('example_striNG', 'example_string_dsf', True), - - ('example_string', 'example_string_dsf', False), - ('example_string', 'example_striNG_dsf', False), - ('example_striNG', 'example_string_dsf', False), - - ('example_string', 'not_example_strin', True), - ('example_string', 'not_eXample_strin', True), - ('EXample_string', 'not_eXample_strin', True), - - ('example_string', 'not_example_strin', False), - ('example_string', 'not_eXample_strin', False), - ('EXample_string', 'not_example_strin', False), - ]) + ('example_string', 'not_example_strin'), + ('example_string', 'not_eXample_strin'), + ('EXample_string', 'not_example_strin'), + ]) async def test_contains(self, test_string, test_text, ignore_case): test_filter = Text(contains=test_string, ignore_case=ignore_case) @@ -249,23 +167,16 @@ class TestTextFilter: return result is (_test_string in _test_text) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_filter_list, test_text, ignore_case", - [(['a', 'ab', 'abc'], 'A', True), - (['a', 'ab', 'abc'], 'ab', True), - (['a', 'ab', 'abc'], 'aBc', True), - (['a', 'ab', 'abc'], 'd', True), - - (['a', 'ab', 'abc'], 'A', False), - (['a', 'ab', 'abc'], 'ab', False), - (['a', 'ab', 'abc'], 'aBc', False), - (['a', 'ab', 'abc'], 'd', False), - ]) + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_filter_list, test_text", [ + (['a', 'ab', 'abc'], 'A'), + (['a', 'ab', 'abc'], 'ab'), + (['a', 'ab', 'abc'], 'aBc'), + (['a', 'ab', 'abc'], 'd'), + ]) async def test_contains_list(self, test_filter_list, test_text, ignore_case): test_filter = Text(contains=test_filter_list, ignore_case=ignore_case) @@ -280,34 +191,22 @@ class TestTextFilter: return result is all(map(_test_text.__contains__, _test_filter_list)) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_filter_text, test_text, ignore_case", - [('', '', True), - ('', 'exAmple_string', True), - ('', '', False), - ('', 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_filter_text, test_text", [ + ('', ''), + ('', 'exAmple_string'), - ('example_string', 'example_string', True), - ('example_string', 'exAmple_string', True), - ('exAmple_string', 'example_string', True), + ('example_string', 'example_string'), + ('example_string', 'exAmple_string'), + ('exAmple_string', 'example_string'), - ('example_string', 'example_string', False), - ('example_string', 'exAmple_string', False), - ('exAmple_string', 'example_string', False), - - ('example_string', 'not_example_string', True), - ('example_string', 'not_eXample_string', True), - ('EXample_string', 'not_eXample_string', True), - - ('example_string', 'not_example_string', False), - ('example_string', 'not_eXample_string', False), - ('EXample_string', 'not_example_string', False), - ]) + ('example_string', 'not_example_string'), + ('example_string', 'not_eXample_string'), + ('EXample_string', 'not_example_string'), + ]) async def test_equals_string(self, test_filter_text, test_text, ignore_case): test_filter = Text(equals=test_filter_text, ignore_case=ignore_case) @@ -321,50 +220,30 @@ class TestTextFilter: _test_text = test_text return result is (_test_text == _test_filter_text) - assert await check(Message(text=test_text)) - assert await check(CallbackQuery(data=test_text)) - assert await check(InlineQuery(query=test_text)) - assert await check(Poll(question=test_text)) + await self._run_check(check, test_text) @pytest.mark.asyncio - @pytest.mark.parametrize("test_filter_list, test_text, ignore_case", - [(['', 'new_string'], '', True), - (['new_string', ''], 'exAmple_string', True), - (['new_string', ''], '', False), - (['', 'new_string'], 'exAmple_string', False), + @pytest.mark.parametrize('ignore_case', (True, False)) + @pytest.mark.parametrize("test_filter_list, test_text", [ + (['new_string', ''], ''), + (['', 'new_string'], 'exAmple_string'), - (['example_string'], 'example_string', True), - (['example_string'], 'exAmple_string', True), - (['exAmple_string'], 'example_string', True), + (['example_string'], 'example_string'), + (['example_string'], 'exAmple_string'), + (['exAmple_string'], 'example_string'), - (['example_string'], 'example_string', False), - (['example_string'], 'exAmple_string', False), - (['exAmple_string'], 'example_string', False), + (['example_string'], 'not_example_string'), + (['example_string'], 'not_eXample_string'), + (['EXample_string'], 'not_example_string'), - (['example_string'], 'not_example_string', True), - (['example_string'], 'not_eXample_string', True), - (['EXample_string'], 'not_eXample_string', True), + (['example_string', 'new_string'], 'example_string'), + (['new_string', 'example_string'], 'exAmple_string'), + (['exAmple_string', 'new_string'], 'example_string'), - (['example_string'], 'not_example_string', False), - (['example_string'], 'not_eXample_string', False), - (['EXample_string'], 'not_example_string', False), - - (['example_string', 'new_string'], 'example_string', True), - (['new_string', 'example_string'], 'exAmple_string', True), - (['exAmple_string', 'new_string'], 'example_string', True), - - (['example_string', 'new_string'], 'example_string', False), - (['new_string', 'example_string'], 'exAmple_string', False), - (['exAmple_string', 'new_string'], 'example_string', False), - - (['example_string', 'new_string'], 'not_example_string', True), - (['new_string', 'example_string'], 'not_eXample_string', True), - (['EXample_string', 'new_string'], 'not_eXample_string', True), - - (['example_string', 'new_string'], 'not_example_string', False), - (['new_string', 'example_string'], 'not_eXample_string', False), - (['EXample_string', 'new_string'], 'not_example_string', False), - ]) + (['example_string', 'new_string'], 'not_example_string'), + (['new_string', 'example_string'], 'not_eXample_string'), + (['EXample_string', 'new_string'], 'not_example_string'), + ]) async def test_equals_list(self, test_filter_list, test_text, ignore_case): test_filter = Text(equals=test_filter_list, ignore_case=ignore_case) From be622ca5590156749feba4783a566fb846468210 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 23:42:18 +0300 Subject: [PATCH 112/128] Refactor some redundant elifs --- aiogram/types/message.py | 56 ++++++++++++++++----------------- aiogram/types/message_entity.py | 12 +++---- aiogram/utils/helper.py | 10 +++--- aiogram/utils/payload.py | 8 ++--- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 67fd07fa..0097fe58 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -94,60 +94,60 @@ class Message(base.TelegramObject): def content_type(self): if self.text: return ContentType.TEXT - elif self.audio: + if self.audio: return ContentType.AUDIO - elif self.animation: + if self.animation: return ContentType.ANIMATION - elif self.document: + if self.document: return ContentType.DOCUMENT - elif self.game: + if self.game: return ContentType.GAME - elif self.photo: + if self.photo: return ContentType.PHOTO - elif self.sticker: + if self.sticker: return ContentType.STICKER - elif self.video: + if self.video: return ContentType.VIDEO - elif self.video_note: + if self.video_note: return ContentType.VIDEO_NOTE - elif self.voice: + if self.voice: return ContentType.VOICE - elif self.contact: + if self.contact: return ContentType.CONTACT - elif self.venue: + if self.venue: return ContentType.VENUE - elif self.location: + if self.location: return ContentType.LOCATION - elif self.new_chat_members: + if self.new_chat_members: return ContentType.NEW_CHAT_MEMBERS - elif self.left_chat_member: + if self.left_chat_member: return ContentType.LEFT_CHAT_MEMBER - elif self.invoice: + if self.invoice: return ContentType.INVOICE - elif self.successful_payment: + if self.successful_payment: return ContentType.SUCCESSFUL_PAYMENT - elif self.connected_website: + if self.connected_website: return ContentType.CONNECTED_WEBSITE - elif self.migrate_from_chat_id: + if self.migrate_from_chat_id: return ContentType.MIGRATE_FROM_CHAT_ID - elif self.migrate_to_chat_id: + if self.migrate_to_chat_id: return ContentType.MIGRATE_TO_CHAT_ID - elif self.pinned_message: + if self.pinned_message: return ContentType.PINNED_MESSAGE - elif self.new_chat_title: + if self.new_chat_title: return ContentType.NEW_CHAT_TITLE - elif self.new_chat_photo: + if self.new_chat_photo: return ContentType.NEW_CHAT_PHOTO - elif self.delete_chat_photo: + if self.delete_chat_photo: return ContentType.DELETE_CHAT_PHOTO - elif self.group_chat_created: + if self.group_chat_created: return ContentType.GROUP_CHAT_CREATED - elif self.passport_data: + if self.passport_data: return ContentType.PASSPORT_DATA - elif self.poll: + if self.poll: return ContentType.POLL - else: - return ContentType.UNKNOWN + + return ContentType.UNKNOWN def is_command(self): """ diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index abb4f060..8b2e62d4 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -52,27 +52,27 @@ class MessageEntity(base.TelegramObject): if as_html: return markdown.hbold(entity_text) return markdown.bold(entity_text) - elif self.type == MessageEntityType.ITALIC: + if self.type == MessageEntityType.ITALIC: if as_html: return markdown.hitalic(entity_text) return markdown.italic(entity_text) - elif self.type == MessageEntityType.PRE: + if self.type == MessageEntityType.PRE: if as_html: return markdown.hpre(entity_text) return markdown.pre(entity_text) - elif self.type == MessageEntityType.CODE: + if self.type == MessageEntityType.CODE: if as_html: return markdown.hcode(entity_text) return markdown.code(entity_text) - elif self.type == MessageEntityType.URL: + if self.type == MessageEntityType.URL: if as_html: return markdown.hlink(entity_text, entity_text) return markdown.link(entity_text, entity_text) - elif self.type == MessageEntityType.TEXT_LINK: + if self.type == MessageEntityType.TEXT_LINK: if as_html: return markdown.hlink(entity_text, self.url) return markdown.link(entity_text, self.url) - elif self.type == MessageEntityType.TEXT_MENTION and self.user: + if self.type == MessageEntityType.TEXT_MENTION and self.user: return self.user.get_mention(entity_text) return entity_text diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index eeabca7c..443a2ffe 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -120,15 +120,15 @@ class HelperMode(Helper): """ if mode == cls.SCREAMING_SNAKE_CASE: return cls._screaming_snake_case(text) - elif mode == cls.snake_case: + if mode == cls.snake_case: return cls._snake_case(text) - elif mode == cls.lowercase: + if mode == cls.lowercase: return cls._snake_case(text).replace('_', '') - elif mode == cls.lowerCamelCase: + if mode == cls.lowerCamelCase: return cls._camel_case(text) - elif mode == cls.CamelCase: + if mode == cls.CamelCase: return cls._camel_case(text, True) - elif callable(mode): + if callable(mode): return mode(text) return text diff --git a/aiogram/utils/payload.py b/aiogram/utils/payload.py index 45643553..0c5e8ae9 100644 --- a/aiogram/utils/payload.py +++ b/aiogram/utils/payload.py @@ -52,14 +52,14 @@ def prepare_arg(value): """ if value is None: return value - elif isinstance(value, (list, dict)) or hasattr(value, 'to_python'): + if isinstance(value, (list, dict)) or hasattr(value, 'to_python'): return json.dumps(_normalize(value)) - elif isinstance(value, datetime.timedelta): + if isinstance(value, datetime.timedelta): now = datetime.datetime.now() return int((now + value).timestamp()) - elif isinstance(value, datetime.datetime): + if isinstance(value, datetime.datetime): return round(value.timestamp()) - elif isinstance(value, LazyProxy): + if isinstance(value, LazyProxy): return str(value) return value From dfc334ef2016b06974ad39b9fcd11b14b438fcfe Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Sun, 11 Aug 2019 23:45:31 +0300 Subject: [PATCH 113/128] Minor refactor MessageEntity#parse --- aiogram/types/message_entity.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 8b2e62d4..b9a9103e 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -49,29 +49,23 @@ class MessageEntity(base.TelegramObject): entity_text = self.get_text(text) if self.type == MessageEntityType.BOLD: - if as_html: - return markdown.hbold(entity_text) - return markdown.bold(entity_text) + method = markdown.hbold if as_html else markdown.bold + return method(entity_text) if self.type == MessageEntityType.ITALIC: - if as_html: - return markdown.hitalic(entity_text) - return markdown.italic(entity_text) + method = markdown.hitalic if as_html else markdown.italic + return method(entity_text) if self.type == MessageEntityType.PRE: - if as_html: - return markdown.hpre(entity_text) - return markdown.pre(entity_text) + method = markdown.hpre if as_html else markdown.pre + return method(entity_text) if self.type == MessageEntityType.CODE: - if as_html: - return markdown.hcode(entity_text) - return markdown.code(entity_text) + method = markdown.hcode if as_html else markdown.code + return method(entity_text) if self.type == MessageEntityType.URL: - if as_html: - return markdown.hlink(entity_text, entity_text) - return markdown.link(entity_text, entity_text) + method = markdown.hlink if as_html else markdown.link + return method(entity_text, entity_text) if self.type == MessageEntityType.TEXT_LINK: - if as_html: - return markdown.hlink(entity_text, self.url) - return markdown.link(entity_text, self.url) + method = markdown.hlink if as_html else markdown.link + return method(entity_text, self.url) if self.type == MessageEntityType.TEXT_MENTION and self.user: return self.user.get_mention(entity_text) return entity_text From 9ea22a29fc3a14beedeafd22d7f250a1aa45368a Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 12 Aug 2019 14:16:05 +0300 Subject: [PATCH 114/128] fix docs --- aiogram/utils/deprecated.py | 6 ++---- docs/source/utils/deprecated.rst | 7 ++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 89081c8b..ecb66e6a 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -1,7 +1,3 @@ -""" -Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically -""" - import functools import inspect import warnings @@ -13,6 +9,8 @@ def deprecated(reason): This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used. + + Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically """ if isinstance(reason, str): diff --git a/docs/source/utils/deprecated.rst b/docs/source/utils/deprecated.rst index ce80bb0e..7f2f07cc 100644 --- a/docs/source/utils/deprecated.rst +++ b/docs/source/utils/deprecated.rst @@ -1,8 +1,5 @@ ========== Deprecated ========== -.. literalinclude:: ../../../aiogram/utils/deprecated.py - :caption: deprecated.py - :language: python3 - :linenos: - +.. automodule:: aiogram.utils.deprecated + :members: From 026416a66882ed445f71918df1b12872deb15e25 Mon Sep 17 00:00:00 2001 From: birdi Date: Mon, 12 Aug 2019 14:16:38 +0300 Subject: [PATCH 115/128] refactoring --- aiogram/utils/deprecated.py | 5 ++--- examples/webhook_example.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index ecb66e6a..8a527420 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -40,7 +40,7 @@ def deprecated(reason): return decorator - elif inspect.isclass(reason) or inspect.isfunction(reason): + if inspect.isclass(reason) or inspect.isfunction(reason): # The @deprecated is used without any 'reason'. # @@ -64,8 +64,7 @@ def deprecated(reason): return wrapper1 - else: - raise TypeError(repr(type(reason))) + raise TypeError(repr(type(reason))) def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): diff --git a/examples/webhook_example.py b/examples/webhook_example.py index 0f6ae3cd..ca8028fd 100644 --- a/examples/webhook_example.py +++ b/examples/webhook_example.py @@ -62,7 +62,7 @@ async def cmd_about(message: types.Message): async def cancel(message: types.Message): # Get current state context - state = dp.current_state(chat=message.chat.id, user=message.from_user.id) + state = dp.current_state(chat_id=message.chat.id, user_id=message.from_user.id) # If current user in any state - cancel it. if await state.get_state() is not None: From 5a29eb09600f5e6b7b2f9b869178d3c3781184d0 Mon Sep 17 00:00:00 2001 From: birdi Date: Thu, 15 Aug 2019 01:37:55 +0300 Subject: [PATCH 116/128] Add relax argument in executor.start_polling --- aiogram/utils/executor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 854facae..33f80684 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -23,7 +23,7 @@ def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None): def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True, - on_startup=None, on_shutdown=None, timeout=20, fast=True): + on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True): """ Start bot in long-polling mode @@ -38,7 +38,7 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) _setup_callbacks(executor, on_startup, on_shutdown) - executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast) + executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast) def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -291,7 +291,7 @@ class Executor: self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name) self.run_app(**kwargs) - def start_polling(self, reset_webhook=None, timeout=20, fast=True): + def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True): """ Start bot in long-polling mode @@ -303,7 +303,8 @@ class Executor: try: loop.run_until_complete(self._startup_polling()) - loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast)) + loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout, + relax=relax, fast=fast)) loop.run_forever() except (KeyboardInterrupt, SystemExit): # loop.stop() From 7863f052d9a768e6039e43190983e5e0acadd9e7 Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Fri, 16 Aug 2019 22:33:19 +0300 Subject: [PATCH 117/128] Refactor aiogram/utils/auth_widget.py + fix check auth widget token in BaseBot, fix tests --- aiogram/bot/base.py | 4 +-- aiogram/utils/auth_widget.py | 33 ++++++++++++++++++++ aiogram/utils/deprecated.py | 7 +++-- tests/test_bot/test_api.py | 18 +++++++++++ tests/test_token.py | 41 ------------------------- tests/test_utils/test_auth_widget.py | 46 ++++++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 46 deletions(-) create mode 100644 tests/test_bot/test_api.py delete mode 100644 tests/test_token.py create mode 100644 tests/test_utils/test_auth_widget.py diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 85773e30..06bd9467 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -13,7 +13,7 @@ from aiohttp.helpers import sentinel from . import api from ..types import ParseMode, base from ..utils import json -from ..utils.auth_widget import check_token +from ..utils.auth_widget import check_integrity class BaseBot: @@ -266,4 +266,4 @@ class BaseBot: self.parse_mode = None def check_auth_widget(self, data): - return check_token(data, self.__token) + return check_integrity(self.__token, data) diff --git a/aiogram/utils/auth_widget.py b/aiogram/utils/auth_widget.py index b9084eb1..c5b03aa3 100644 --- a/aiogram/utils/auth_widget.py +++ b/aiogram/utils/auth_widget.py @@ -8,7 +8,10 @@ import collections import hashlib import hmac +from aiogram.utils.deprecated import deprecated + +@deprecated('`generate_hash` is outdated, please use `check_signature` or `check_integrity`') def generate_hash(data: dict, token: str) -> str: """ Generate secret hash @@ -24,6 +27,7 @@ def generate_hash(data: dict, token: str) -> str: return hmac.new(secret.digest(), msg.encode('utf-8'), digestmod=hashlib.sha256).hexdigest() +@deprecated('`check_token` helper was renamed to `check_integrity`') def check_token(data: dict, token: str) -> bool: """ Validate auth token @@ -34,3 +38,32 @@ def check_token(data: dict, token: str) -> bool: """ param_hash = data.get('hash', '') or '' return param_hash == generate_hash(data, token) + + +def check_signature(token: str, hash: str, **kwargs) -> bool: + """ + Generate hexadecimal representation + of the HMAC-SHA-256 signature of the data-check-string + with the SHA256 hash of the bot's token used as a secret key + + :param token: + :param hash: + :param kwargs: all params received on auth + :return: + """ + secret = hashlib.sha256(token.encode('utf-8')) + check_string = '\n'.join(map(lambda k: f'{k}={kwargs[k]}', sorted(kwargs))) + hmac_string = hmac.new(secret.digest(), check_string.encode('utf-8'), digestmod=hashlib.sha256).hexdigest() + return hmac_string == hash + + +def check_integrity(token: str, data: dict) -> bool: + """ + Verify the authentication and the integrity + of the data received on user's auth + + :param token: Bot's token + :param data: all data that came on auth + :return: + """ + return check_signature(token, **data) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 8a527420..b88efcd7 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -1,10 +1,11 @@ -import functools +import asyncio import inspect import warnings -import asyncio +import functools +from typing import Callable -def deprecated(reason): +def deprecated(reason) -> Callable: """ This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted diff --git a/tests/test_bot/test_api.py b/tests/test_bot/test_api.py new file mode 100644 index 00000000..c5193bcc --- /dev/null +++ b/tests/test_bot/test_api.py @@ -0,0 +1,18 @@ +import pytest +from aiogram.bot.api import check_token + +from aiogram.utils.exceptions import ValidationError + + +VALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' +INVALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff 123456789' # Space in token and wrong length + + +class Test_check_token: + + def test_valid(self): + assert check_token(VALID_TOKEN) is True + + def test_invalid_token(self): + with pytest.raises(ValidationError): + check_token(INVALID_TOKEN) diff --git a/tests/test_token.py b/tests/test_token.py deleted file mode 100644 index b8a6087f..00000000 --- a/tests/test_token.py +++ /dev/null @@ -1,41 +0,0 @@ -import pytest - -from aiogram.bot import api -from aiogram.utils import auth_widget, exceptions - -VALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' -INVALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff 123456789' # Space in token and wrong length - -VALID_DATA = { - 'date': 1525385236, - 'first_name': 'Test', - 'last_name': 'User', - 'id': 123456789, - 'username': 'username', - 'hash': '69a9871558fbbe4cd0dbaba52fa1cc4f38315d3245b7504381a64139fb024b5b' -} -INVALID_DATA = { - 'date': 1525385237, - 'first_name': 'Test', - 'last_name': 'User', - 'id': 123456789, - 'username': 'username', - 'hash': '69a9871558fbbe4cd0dbaba52fa1cc4f38315d3245b7504381a64139fb024b5b' -} - - -def test_valid_token(): - assert api.check_token(VALID_TOKEN) - - -def test_invalid_token(): - with pytest.raises(exceptions.ValidationError): - api.check_token(INVALID_TOKEN) - - -def test_widget(): - assert auth_widget.check_token(VALID_DATA, VALID_TOKEN) - - -def test_invalid_widget_data(): - assert not auth_widget.check_token(INVALID_DATA, VALID_TOKEN) diff --git a/tests/test_utils/test_auth_widget.py b/tests/test_utils/test_auth_widget.py new file mode 100644 index 00000000..8c6f5941 --- /dev/null +++ b/tests/test_utils/test_auth_widget.py @@ -0,0 +1,46 @@ +import pytest + +from aiogram.utils.auth_widget import check_integrity, \ + generate_hash, check_token + +TOKEN = '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11' + + +@pytest.fixture +def data(): + return { + 'id': '42', + 'first_name': 'John', + 'last_name': 'Smith', + 'username': 'username', + 'photo_url': 'https://t.me/i/userpic/320/picname.jpg', + 'auth_date': '1565810688', + 'hash': 'c303db2b5a06fe41d23a9b14f7c545cfc11dcc7473c07c9c5034ae60062461ce', + } + + +def test_generate_hash(data): + res = generate_hash(data, TOKEN) + assert res == data['hash'] + + +class Test_check_token: + """ + This case gonna be deleted + """ + def test_ok(self, data): + assert check_token(data, TOKEN) is True + + def test_fail(self, data): + data.pop('username') + assert check_token(data, TOKEN) is False + + +class Test_check_integrity: + + def test_ok(self, data): + assert check_integrity(TOKEN, data) is True + + def test_fail(self, data): + data.pop('username') + assert check_integrity(TOKEN, data) is False From 19fc1b8d8053d071bdd838433dd9c3a069ab1fa9 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 16 Aug 2019 22:57:13 +0300 Subject: [PATCH 118/128] Bump deprecated util and fix warning from tests related with new permissions object. --- aiogram/utils/auth_widget.py | 4 ++-- aiogram/utils/deprecated.py | 6 +++--- tests/test_bot.py | 12 +++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/aiogram/utils/auth_widget.py b/aiogram/utils/auth_widget.py index c5b03aa3..a39a0eed 100644 --- a/aiogram/utils/auth_widget.py +++ b/aiogram/utils/auth_widget.py @@ -11,7 +11,7 @@ import hmac from aiogram.utils.deprecated import deprecated -@deprecated('`generate_hash` is outdated, please use `check_signature` or `check_integrity`') +@deprecated('`generate_hash` is outdated, please use `check_signature` or `check_integrity`', stacklevel=3) def generate_hash(data: dict, token: str) -> str: """ Generate secret hash @@ -27,7 +27,7 @@ def generate_hash(data: dict, token: str) -> str: return hmac.new(secret.digest(), msg.encode('utf-8'), digestmod=hashlib.sha256).hexdigest() -@deprecated('`check_token` helper was renamed to `check_integrity`') +@deprecated('`check_token` helper was renamed to `check_integrity`', stacklevel=3) def check_token(data: dict, token: str) -> bool: """ Validate auth token diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index b88efcd7..ebdb14a3 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -5,7 +5,7 @@ import functools from typing import Callable -def deprecated(reason) -> Callable: +def deprecated(reason, stacklevel=2) -> Callable: """ This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted @@ -33,7 +33,7 @@ def deprecated(reason) -> Callable: @functools.wraps(func) def wrapper(*args, **kwargs): - warn_deprecated(msg.format(name=func.__name__, reason=reason)) + warn_deprecated(msg.format(name=func.__name__, reason=reason), stacklevel=stacklevel) warnings.simplefilter('default', DeprecationWarning) return func(*args, **kwargs) @@ -60,7 +60,7 @@ def deprecated(reason) -> Callable: @functools.wraps(func1) def wrapper1(*args, **kwargs): - warn_deprecated(msg1.format(name=func1.__name__)) + warn_deprecated(msg1.format(name=func1.__name__), stacklevel=stacklevel) return func1(*args, **kwargs) return wrapper1 diff --git a/tests/test_bot.py b/tests/test_bot.py index 448f8dda..3e48ea57 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -333,9 +333,15 @@ async def test_restrict_chat_member(bot: Bot, event_loop): chat = types.Chat(**CHAT) async with FakeTelegram(message_dict=True, loop=event_loop): - result = await bot.restrict_chat_member(chat_id=chat.id, user_id=user.id, can_add_web_page_previews=False, - can_send_media_messages=False, can_send_messages=False, - can_send_other_messages=False, until_date=123) + result = await bot.restrict_chat_member( + chat_id=chat.id, + user_id=user.id, + permissions=types.ChatPermissions( + can_add_web_page_previews=False, + can_send_media_messages=False, + can_send_messages=False, + can_send_other_messages=False + ), until_date=123) assert isinstance(result, bool) assert result is True From 9a30285d3bbdf8a3b2b3c60beeed631b011a9a3f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 17 Aug 2019 00:08:04 +0300 Subject: [PATCH 119/128] Update docs. --- aiogram/utils/exceptions.py | 168 +++++++++++++++--------------- docs/source/utils/auth_widget.rst | 4 +- docs/source/utils/context.rst | 4 - docs/source/utils/deprecated.rst | 1 + docs/source/utils/emoji.rst | 4 +- docs/source/utils/exceptions.rst | 4 +- docs/source/utils/executor.rst | 5 +- docs/source/utils/helper.rst | 4 +- docs/source/utils/index.rst | 5 +- docs/source/utils/json.rst | 4 +- docs/source/utils/markdown.rst | 4 +- docs/source/utils/parts.rst | 4 +- docs/source/utils/payload.rst | 4 +- 13 files changed, 114 insertions(+), 101 deletions(-) delete mode 100644 docs/source/utils/context.rst diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index a6612547..f77fe257 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -1,94 +1,92 @@ """ -TelegramAPIError - ValidationError - Throttled - BadRequest - MessageError - MessageNotModified - MessageToForwardNotFound - MessageToDeleteNotFound - MessageIdentifierNotSpecified - MessageTextIsEmpty - MessageCantBeEdited - MessageCantBeDeleted - MessageToEditNotFound - MessageToReplyNotFound - ToMuchMessages - PollError - PollCantBeStopped - PollHasAlreadyClosed - PollsCantBeSentToPrivateChats - PollSizeError - PollMustHaveMoreOptions - PollCantHaveMoreOptions - PollsOptionsLengthTooLong - PollOptionsMustBeNonEmpty - PollQuestionMustBeNonEmpty - MessageWithPollNotFound (with MessageError) - MessageIsNotAPoll (with MessageError) - ObjectExpectedAsReplyMarkup - InlineKeyboardExpected - ChatNotFound - ChatDescriptionIsNotModified - InvalidQueryID - InvalidPeerID - InvalidHTTPUrlContent - ButtonURLInvalid - URLHostIsEmpty - StartParamInvalid - ButtonDataInvalid - WrongFileIdentifier - GroupDeactivated - BadWebhook - WebhookRequireHTTPS - BadWebhookPort - BadWebhookAddrInfo - BadWebhookNoAddressAssociatedWithHostname - NotFound - MethodNotKnown - PhotoAsInputFileRequired - InvalidStickersSet - NoStickerInRequest - ChatAdminRequired - NeedAdministratorRightsInTheChannel - MethodNotAvailableInPrivateChats - CantDemoteChatCreator - CantRestrictSelf - NotEnoughRightsToRestrict - PhotoDimensions - UnavailableMembers - TypeOfFileMismatch - WrongRemoteFileIdSpecified - PaymentProviderInvalid - CurrencyTotalAmountInvalid - CantParseUrl - UnsupportedUrlProtocol - CantParseEntities - ResultIdDuplicate - ConflictError - TerminatedByOtherGetUpdates - CantGetUpdates - Unauthorized - BotKicked - BotBlocked - UserDeactivated - CantInitiateConversation - CantTalkWithBots - NetworkError - RetryAfter - MigrateToChat - RestartingTelegram +- TelegramAPIError + - ValidationError + - Throttled + - BadRequest + - MessageError + - MessageNotModified + - MessageToForwardNotFound + - MessageToDeleteNotFound + - MessageIdentifierNotSpecified + - MessageTextIsEmpty + - MessageCantBeEdited + - MessageCantBeDeleted + - MessageToEditNotFound + - MessageToReplyNotFound + - ToMuchMessages + - PollError + - PollCantBeStopped + - PollHasAlreadyClosed + - PollsCantBeSentToPrivateChats + - PollSizeError + - PollMustHaveMoreOptions + - PollCantHaveMoreOptions + - PollsOptionsLengthTooLong + - PollOptionsMustBeNonEmpty + - PollQuestionMustBeNonEmpty + - MessageWithPollNotFound (with MessageError) + - MessageIsNotAPoll (with MessageError) + - ObjectExpectedAsReplyMarkup + - InlineKeyboardExpected + - ChatNotFound + - ChatDescriptionIsNotModified + - InvalidQueryID + - InvalidPeerID + - InvalidHTTPUrlContent + - ButtonURLInvalid + - URLHostIsEmpty + - StartParamInvalid + - ButtonDataInvalid + - WrongFileIdentifier + - GroupDeactivated + - BadWebhook + - WebhookRequireHTTPS + - BadWebhookPort + - BadWebhookAddrInfo + - BadWebhookNoAddressAssociatedWithHostname + - NotFound + - MethodNotKnown + - PhotoAsInputFileRequired + - InvalidStickersSet + - NoStickerInRequest + - ChatAdminRequired + - NeedAdministratorRightsInTheChannel + - MethodNotAvailableInPrivateChats + - CantDemoteChatCreator + - CantRestrictSelf + - NotEnoughRightsToRestrict + - PhotoDimensions + - UnavailableMembers + - TypeOfFileMismatch + - WrongRemoteFileIdSpecified + - PaymentProviderInvalid + - CurrencyTotalAmountInvalid + - CantParseUrl + - UnsupportedUrlProtocol + - CantParseEntities + - ResultIdDuplicate + - ConflictError + - TerminatedByOtherGetUpdates + - CantGetUpdates + - Unauthorized + - BotKicked + - BotBlocked + - UserDeactivated + - CantInitiateConversation + - CantTalkWithBots + - NetworkError + - RetryAfter + - MigrateToChat + - RestartingTelegram - -TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0 -TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout - -AIOGramWarning - TimeoutWarning +- AIOGramWarning + - TimeoutWarning """ import time # TODO: Use exceptions detector from `aiograph`. +# TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0 +# TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout _PREFIXES = ['error: ', '[error]: ', 'bad request: ', 'conflict: ', 'not found: '] diff --git a/docs/source/utils/auth_widget.rst b/docs/source/utils/auth_widget.rst index e3a90ef6..95cb3913 100644 --- a/docs/source/utils/auth_widget.rst +++ b/docs/source/utils/auth_widget.rst @@ -1,4 +1,6 @@ =========== Auth Widget =========== -Coming soon... + +.. automodule:: aiogram.utils.auth_widget + :members: diff --git a/docs/source/utils/context.rst b/docs/source/utils/context.rst deleted file mode 100644 index 7a930a7e..00000000 --- a/docs/source/utils/context.rst +++ /dev/null @@ -1,4 +0,0 @@ -======= -Context -======= -Coming soon... diff --git a/docs/source/utils/deprecated.rst b/docs/source/utils/deprecated.rst index 7f2f07cc..619224f8 100644 --- a/docs/source/utils/deprecated.rst +++ b/docs/source/utils/deprecated.rst @@ -1,5 +1,6 @@ ========== Deprecated ========== + .. automodule:: aiogram.utils.deprecated :members: diff --git a/docs/source/utils/emoji.rst b/docs/source/utils/emoji.rst index 27382dd6..1be210e3 100644 --- a/docs/source/utils/emoji.rst +++ b/docs/source/utils/emoji.rst @@ -1,4 +1,6 @@ ===== Emoji ===== -Coming soon... + +.. automodule:: aiogram.utils.emoji + :members: diff --git a/docs/source/utils/exceptions.rst b/docs/source/utils/exceptions.rst index 199e67aa..b296afd3 100644 --- a/docs/source/utils/exceptions.rst +++ b/docs/source/utils/exceptions.rst @@ -1,4 +1,6 @@ ========== Exceptions ========== -Coming soon... + +.. automodule:: aiogram.utils.exceptions + :members: diff --git a/docs/source/utils/executor.rst b/docs/source/utils/executor.rst index 2cb8eaa1..f88dd8c5 100644 --- a/docs/source/utils/executor.rst +++ b/docs/source/utils/executor.rst @@ -1,4 +1,7 @@ ======== Executor ======== -Coming soon... + +.. automodule:: aiogram.utils.executor + :members: + diff --git a/docs/source/utils/helper.rst b/docs/source/utils/helper.rst index 4ffc74ab..ba8bf016 100644 --- a/docs/source/utils/helper.rst +++ b/docs/source/utils/helper.rst @@ -1,4 +1,6 @@ ====== Helper ====== -Coming soon... + +.. automodule:: aiogram.utils.helper + :members: diff --git a/docs/source/utils/index.rst b/docs/source/utils/index.rst index bc4a52ed..1ac3777c 100644 --- a/docs/source/utils/index.rst +++ b/docs/source/utils/index.rst @@ -3,14 +3,13 @@ Utils .. toctree:: + auth_widget executor exceptions - context markdown helper - auth_widget + deprecated payload parts json emoji - deprecated diff --git a/docs/source/utils/json.rst b/docs/source/utils/json.rst index 84833031..68577ff4 100644 --- a/docs/source/utils/json.rst +++ b/docs/source/utils/json.rst @@ -1,4 +1,6 @@ ==== JSON ==== -Coming soon... + +.. automodule:: aiogram.utils.json + :members: diff --git a/docs/source/utils/markdown.rst b/docs/source/utils/markdown.rst index ee32dfd4..bcbe0497 100644 --- a/docs/source/utils/markdown.rst +++ b/docs/source/utils/markdown.rst @@ -1,4 +1,6 @@ ======== Markdown ======== -Coming soon... + +.. automodule:: aiogram.utils.markdown + :members: diff --git a/docs/source/utils/parts.rst b/docs/source/utils/parts.rst index 845d017e..fd2e91de 100644 --- a/docs/source/utils/parts.rst +++ b/docs/source/utils/parts.rst @@ -1,4 +1,6 @@ ===== Parts ===== -Coming soon... + +.. automodule:: aiogram.utils.parts + :members: diff --git a/docs/source/utils/payload.rst b/docs/source/utils/payload.rst index b3427906..e3e0331a 100644 --- a/docs/source/utils/payload.rst +++ b/docs/source/utils/payload.rst @@ -1,4 +1,6 @@ ======= Payload ======= -Coming soon... + +.. automodule:: aiogram.utils.payload + :members: From 0c1fc4b1dddc8958575e9ee9932221d15977d72e Mon Sep 17 00:00:00 2001 From: Evgen Date: Mon, 19 Aug 2019 00:11:05 +0500 Subject: [PATCH 120/128] Add aiogram AUR package installation instructions --- docs/source/install.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/install.rst b/docs/source/install.rst index de199af6..cd89dc54 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -7,6 +7,10 @@ Using PIP $ pip install -U aiogram +Using AUR +--------- +*aiogram* is also available in Arch User Repository, so you can install this library on any Arch-based distribution like ArchLinux, Antergos, Manjaro, etc. To do this, use your favorite AUR-helper and install `python-aiogram `_ package. + From sources ------------ .. code-block:: bash From 3d4bdcc49829e8defcf76f970f1d9ce137f3b7f0 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 23 Aug 2019 23:29:59 +0300 Subject: [PATCH 121/128] Fix `Dispatcher.throttle(...)` and rename user & chat arguments to user_id & chat_id --- aiogram/dispatcher/dispatcher.py | 59 ++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index a5bf5b9f..6891f8be 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -8,6 +8,7 @@ import typing import aiohttp 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 from .handler import Handler @@ -914,15 +915,17 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return FSMContext(storage=self.storage, chat=chat, user=user) - async def throttle(self, key, *, rate=None, user=None, chat=None, no_error=None) -> bool: + @renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=4) + @renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4) + async def throttle(self, key, *, rate=None, user_id=None, chat_id=None, no_error=None) -> bool: """ Execute throttling manager. Returns True if limit has not exceeded otherwise raises ThrottleError or returns False :param key: key in storage :param rate: limit (by default is equal to default rate limit) - :param user: user id - :param chat: chat id + :param user_id: user id + :param chat_id: chat id :param no_error: return boolean value instead of raising error :return: bool """ @@ -933,14 +936,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin): no_error = self.no_throttle_error if rate is None: rate = self.throttling_rate_limit - if user is None and chat is None: - user = types.User.get_current() - chat = types.Chat.get_current() + if user_id is None and chat_id is None: + user_id = types.User.get_current().id + chat_id = types.Chat.get_current().id # Detect current time now = time.time() - bucket = await self.storage.get_bucket(chat=chat, user=user) + bucket = await self.storage.get_bucket(chat=chat_id, user=user_id) # Fix bucket if bucket is None: @@ -964,53 +967,57 @@ class Dispatcher(DataMixin, ContextInstanceMixin): else: data[EXCEEDED_COUNT] = 1 bucket[key].update(data) - await self.storage.set_bucket(chat=chat, user=user, bucket=bucket) + await self.storage.set_bucket(chat=chat_id, user=user_id, bucket=bucket) if not result and not no_error: # Raise if it is allowed - raise Throttled(key=key, chat=chat, user=user, **data) + raise Throttled(key=key, chat=chat_id, user=user_id, **data) return result - async def check_key(self, key, chat=None, user=None): + @renamed_argument('user', 'user_id', '3.0') + @renamed_argument('chat', 'chat_id', '3.0') + async def check_key(self, key, chat_id=None, user_id=None): """ Get information about key in bucket :param key: - :param chat: - :param user: + :param chat_id: + :param user_id: :return: """ if not self.storage.has_bucket(): raise RuntimeError('This storage does not provide Leaky Bucket') - if user is None and chat is None: - user = types.User.get_current() - chat = types.Chat.get_current() + if user_id is None and chat_id is None: + user_id = types.User.get_current() + chat_id = types.Chat.get_current() - bucket = await self.storage.get_bucket(chat=chat, user=user) + bucket = await self.storage.get_bucket(chat=chat_id, user=user_id) data = bucket.get(key, {}) - return Throttled(key=key, chat=chat, user=user, **data) + return Throttled(key=key, chat=chat_id, user=user_id, **data) - async def release_key(self, key, chat=None, user=None): + @renamed_argument('user', 'user_id', '3.0') + @renamed_argument('chat', 'chat_id', '3.0') + async def release_key(self, key, chat_id=None, user_id=None): """ Release blocked key :param key: - :param chat: - :param user: + :param chat_id: + :param user_id: :return: """ if not self.storage.has_bucket(): raise RuntimeError('This storage does not provide Leaky Bucket') - if user is None and chat is None: - user = types.User.get_current() - chat = types.Chat.get_current() + if user_id is None and chat_id is None: + user_id = types.User.get_current() + chat_id = types.Chat.get_current() - bucket = await self.storage.get_bucket(chat=chat, user=user) + bucket = await self.storage.get_bucket(chat=chat_id, user=user_id) if bucket and key in bucket: del bucket['key'] - await self.storage.set_bucket(chat=chat, user=user, bucket=bucket) + await self.storage.set_bucket(chat=chat_id, user=user_id, bucket=bucket) return True return False @@ -1086,7 +1093,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): async def wrapped(*args, **kwargs): is_not_throttled = await self.throttle(key if key is not None else func.__name__, rate=rate, - user=user_id, chat=chat_id, + user_id=user_id, chat_id=chat_id, no_error=True) if is_not_throttled: return await func(*args, **kwargs) From 6de53dd476e0c0f76e470f702c0a3d7fb90e6fd3 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 24 Aug 2019 16:14:49 +0300 Subject: [PATCH 122/128] Fix stacklevel arguments in renamed arguments in the dispatcher --- aiogram/dispatcher/dispatcher.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 6891f8be..c12ece20 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -915,7 +915,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return FSMContext(storage=self.storage, chat=chat, user=user) - @renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=4) + @renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3) @renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4) async def throttle(self, key, *, rate=None, user_id=None, chat_id=None, no_error=None) -> bool: """ @@ -975,7 +975,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return result @renamed_argument('user', 'user_id', '3.0') - @renamed_argument('chat', 'chat_id', '3.0') + @renamed_argument('chat', 'chat_id', '3.0', stacklevel=4) async def check_key(self, key, chat_id=None, user_id=None): """ Get information about key in bucket @@ -997,7 +997,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return Throttled(key=key, chat=chat_id, user=user_id, **data) @renamed_argument('user', 'user_id', '3.0') - @renamed_argument('chat', 'chat_id', '3.0') + @renamed_argument('chat', 'chat_id', '3.0', stacklevel=4) async def release_key(self, key, chat_id=None, user_id=None): """ Release blocked key From 21127c3a7b2bf37ab5280140670f92803a0d1765 Mon Sep 17 00:00:00 2001 From: birdi Date: Sat, 24 Aug 2019 16:23:17 +0300 Subject: [PATCH 123/128] Replace positional args with kwargs --- aiogram/dispatcher/dispatcher.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index c12ece20..6d16a005 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -974,8 +974,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): raise Throttled(key=key, chat=chat_id, user=user_id, **data) return result - @renamed_argument('user', 'user_id', '3.0') - @renamed_argument('chat', 'chat_id', '3.0', stacklevel=4) + @renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3) + @renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4) async def check_key(self, key, chat_id=None, user_id=None): """ Get information about key in bucket @@ -996,8 +996,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): data = bucket.get(key, {}) return Throttled(key=key, chat=chat_id, user=user_id, **data) - @renamed_argument('user', 'user_id', '3.0') - @renamed_argument('chat', 'chat_id', '3.0', stacklevel=4) + @renamed_argument(old_name='user', new_name='user_id', until_version='3.0', stacklevel=3) + @renamed_argument(old_name='chat', new_name='chat_id', until_version='3.0', stacklevel=4) async def release_key(self, key, chat_id=None, user_id=None): """ Release blocked key From 1e75f59f57ba3b71421f9a799f9f2972482da379 Mon Sep 17 00:00:00 2001 From: Evgen Date: Tue, 27 Aug 2019 13:28:11 +0500 Subject: [PATCH 124/128] Fix error with set_chat_permissions I think it should work (I haven't tested it) --- aiogram/bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 53e49997..b30e5309 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1132,7 +1132,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): permissions = prepare_arg(permissions) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_PERMISSIONS) + result = await self.request(api.Methods.SET_CHAT_PERMISSIONS, payload) return result async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String: From dbda878114720d36eaffba1f8a511512bbf45188 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 28 Aug 2019 00:34:32 +0300 Subject: [PATCH 125/128] Fix `renamed_argument` decorator. Return results. --- aiogram/utils/deprecated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index ebdb14a3..cb22c506 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -113,7 +113,7 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve } ) kwargs.pop(old_name) - await func(*args, **kwargs) + return await func(*args, **kwargs) else: @functools.wraps(func) def wrapped(*args, **kwargs): @@ -128,7 +128,7 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve } ) kwargs.pop(old_name) - func(*args, **kwargs) + return func(*args, **kwargs) return wrapped From 6a089fd19f4ba4d153073372a14713e9e10ccd2b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 1 Sep 2019 14:42:09 +0300 Subject: [PATCH 126/128] Add message.send_copy method --- aiogram/types/message.py | 101 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 0097fe58..ef295714 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -32,7 +32,6 @@ from .video_note import VideoNote from .voice import Voice from ..utils import helper from ..utils import markdown as md -from ..utils.deprecated import warn_deprecated class Message(base.TelegramObject): @@ -959,7 +958,6 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def reply_animation(self, animation: typing.Union[base.InputFile, base.String], duration: typing.Union[base.Integer, None] = None, width: typing.Union[base.Integer, None] = None, @@ -1566,6 +1564,105 @@ class Message(base.TelegramObject): """ return await self.chat.pin_message(self.message_id, disable_notification) + async def send_copy( + self: Message, + chat_id: typing.Union[str, int], + with_markup: bool = False, + disable_notification: typing.Optional[bool] = None, + reply_to_message_id: typing.Optional[int] = None, + ) -> Message: + """ + Send copy of current message + + :param chat_id: + :param with_markup: + :param disable_notification: + :param reply_to_message_id: + :return: + """ + bot_instance = self.bot + + kwargs = {"chat_id": chat_id, "parse_mode": ParseMode.HTML} + + if disable_notification is not None: + kwargs["disable_notification"] = disable_notification + if reply_to_message_id is not None: + kwargs["reply_to_message_id"] = reply_to_message_id + if with_markup and self.reply_markup: + kwargs["reply_markup"] = self.reply_markup + + text = self.html_text if (self.text or self.caption) else None + + if self.text: + return await bot_instance.send_message(text=text, **kwargs) + elif self.audio: + return await bot_instance.send_audio( + audio=self.audio.file_id, + caption=text, + title=self.audio.title, + performer=self.audio.performer, + duration=self.audio.duration, + **kwargs + ) + elif self.animation: + return await bot_instance.send_animation( + animation=self.animation.file_id, caption=text, **kwargs + ) + elif self.document: + return await bot_instance.send_document( + document=self.document.file_id, caption=text, **kwargs + ) + elif self.photo: + return await bot_instance.send_photo( + photo=self.photo[-1].file_id, caption=text, **kwargs + ) + elif self.sticker: + kwargs.pop("parse_mode") + return await bot_instance.send_sticker(sticker=self.sticker.file_id, **kwargs) + elif self.video: + return await bot_instance.send_video( + video=self.video.file_id, caption=text, **kwargs + ) + elif self.video_note: + kwargs.pop("parse_mode") + return await bot_instance.send_video_note( + video_note=self.video_note.file_id, **kwargs + ) + elif self.voice: + return await bot_instance.send_voice(voice=self.voice.file_id, **kwargs) + elif self.contact: + kwargs.pop("parse_mode") + return await bot_instance.send_contact( + phone_number=self.contact.phone_number, + first_name=self.contact.first_name, + last_name=self.contact.last_name, + vcard=self.contact.vcard, + **kwargs + ) + elif self.venue: + kwargs.pop("parse_mode") + return await bot_instance.send_venue( + latitude=self.venue.location.latitude, + longitude=self.venue.location.longitude, + title=self.venue.title, + address=self.venue.address, + foursquare_id=self.venue.foursquare_id, + foursquare_type=self.venue.foursquare_type, + **kwargs + ) + elif self.location: + kwargs.pop("parse_mode") + return await bot_instance.send_location( + latitude=self.location.latitude, longitude=self.location.longitude, **kwargs + ) + elif self.poll: + kwargs.pop("parse_mode") + return await bot_instance.send_poll( + question=self.poll.question, options=self.poll.options, **kwargs + ) + else: + raise TypeError("This type of message can't be copied.") + def __int__(self): return self.message_id From e56be672a3ae0211183ef63aa225b15de66334f7 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 1 Sep 2019 14:46:51 +0300 Subject: [PATCH 127/128] Safe close aiohttp session when delete bot instance --- aiogram/bot/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 06bd9467..608abd06 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -99,6 +99,12 @@ class BaseBot: self.parse_mode = parse_mode + def __del__(self): + if self.loop.is_running(): + self.loop.create_task(self.close()) + else: + self.loop.run_until_complete(self.close()) + @staticmethod def _prepare_timeout( value: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] From b8f1b57004effd96dd1d7497258cc7e0b186742b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 1 Sep 2019 19:52:34 +0300 Subject: [PATCH 128/128] Use self.bot instead of bot_instance = self.bot --- aiogram/types/message.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index ef295714..5347027c 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1580,8 +1580,6 @@ class Message(base.TelegramObject): :param reply_to_message_id: :return: """ - bot_instance = self.bot - kwargs = {"chat_id": chat_id, "parse_mode": ParseMode.HTML} if disable_notification is not None: @@ -1594,9 +1592,9 @@ class Message(base.TelegramObject): text = self.html_text if (self.text or self.caption) else None if self.text: - return await bot_instance.send_message(text=text, **kwargs) + return await self.bot.send_message(text=text, **kwargs) elif self.audio: - return await bot_instance.send_audio( + return await self.bot.send_audio( audio=self.audio.file_id, caption=text, title=self.audio.title, @@ -1605,34 +1603,34 @@ class Message(base.TelegramObject): **kwargs ) elif self.animation: - return await bot_instance.send_animation( + return await self.bot.send_animation( animation=self.animation.file_id, caption=text, **kwargs ) elif self.document: - return await bot_instance.send_document( + return await self.bot.send_document( document=self.document.file_id, caption=text, **kwargs ) elif self.photo: - return await bot_instance.send_photo( + return await self.bot.send_photo( photo=self.photo[-1].file_id, caption=text, **kwargs ) elif self.sticker: kwargs.pop("parse_mode") - return await bot_instance.send_sticker(sticker=self.sticker.file_id, **kwargs) + return await self.bot.send_sticker(sticker=self.sticker.file_id, **kwargs) elif self.video: - return await bot_instance.send_video( + return await self.bot.send_video( video=self.video.file_id, caption=text, **kwargs ) elif self.video_note: kwargs.pop("parse_mode") - return await bot_instance.send_video_note( + return await self.bot.send_video_note( video_note=self.video_note.file_id, **kwargs ) elif self.voice: - return await bot_instance.send_voice(voice=self.voice.file_id, **kwargs) + return await self.bot.send_voice(voice=self.voice.file_id, **kwargs) elif self.contact: kwargs.pop("parse_mode") - return await bot_instance.send_contact( + return await self.bot.send_contact( phone_number=self.contact.phone_number, first_name=self.contact.first_name, last_name=self.contact.last_name, @@ -1641,7 +1639,7 @@ class Message(base.TelegramObject): ) elif self.venue: kwargs.pop("parse_mode") - return await bot_instance.send_venue( + return await self.bot.send_venue( latitude=self.venue.location.latitude, longitude=self.venue.location.longitude, title=self.venue.title, @@ -1652,12 +1650,12 @@ class Message(base.TelegramObject): ) elif self.location: kwargs.pop("parse_mode") - return await bot_instance.send_location( + return await self.bot.send_location( latitude=self.location.latitude, longitude=self.location.longitude, **kwargs ) elif self.poll: kwargs.pop("parse_mode") - return await bot_instance.send_poll( + return await self.bot.send_poll( question=self.poll.question, options=self.poll.options, **kwargs ) else: