From e9bf01653ff144802daab5bc4f99b8e70a8f75ba Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 16 Aug 2019 23:00:02 +0300 Subject: [PATCH 001/100] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index b81ceedb..3117ff13 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.3.dev1' +__version__ = '2.3' __api_version__ = '4.4' From a4f684f6bf6a1166fc98552e4a134f400b0995fd Mon Sep 17 00:00:00 2001 From: Anton Patrushev Date: Wed, 28 Aug 2019 23:39:31 +0200 Subject: [PATCH 002/100] process cancellation in dispatch polling --- aiogram/dispatcher/dispatcher.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 6d16a005..d8226f31 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -284,6 +284,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): try: with self.bot.request_timeout(request_timeout): updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout) + except asyncio.CancelledError: + break except: log.exception('Cause exception while getting updates.') await asyncio.sleep(error_sleep) From 65c87e72f73c9eb1898eb6e1d4c8a35443c9ff21 Mon Sep 17 00:00:00 2001 From: Anton Patrushev Date: Wed, 28 Aug 2019 23:40:24 +0200 Subject: [PATCH 003/100] fix set_result of close_waiter future on polling end --- aiogram/dispatcher/dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 6d16a005..4cd38ce3 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -299,7 +299,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): await asyncio.sleep(relax) finally: - self._close_waiter._set_result(None) + self._close_waiter.set_result(None) log.warning('Polling is stopped.') async def _process_polling_updates(self, updates, fast: typing.Optional[bool] = True): From 546b852747fcba9392a9ee23d809cf6dd873f0c6 Mon Sep 17 00:00:00 2001 From: birdi Date: Sun, 1 Sep 2019 00:17:16 +0300 Subject: [PATCH 004/100] Lazy_gettext in i18nMiddleware have enable_cache=False by default --- aiogram/contrib/middlewares/i18n.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 0bb10680..67ab8cca 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -105,7 +105,7 @@ class I18nMiddleware(BaseMiddleware): return translator.gettext(singular) return translator.ngettext(singular, plural, n) - def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=True) -> LazyProxy: + def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=False) -> LazyProxy: """ Lazy get text From b8f1b57004effd96dd1d7497258cc7e0b186742b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 1 Sep 2019 19:52:34 +0300 Subject: [PATCH 005/100] 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: From 1af668f1e0437397458d37b5f798a6917d2c8af2 Mon Sep 17 00:00:00 2001 From: Arseny Boykov <36469655+MrMrRobat@users.noreply.github.com> Date: Tue, 3 Sep 2019 02:26:23 +0300 Subject: [PATCH 006/100] Fix html user mention entity parse --- aiogram/types/message_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index abb4f060..ae2f174f 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -73,7 +73,7 @@ class MessageEntity(base.TelegramObject): return markdown.hlink(entity_text, self.url) return markdown.link(entity_text, self.url) elif self.type == MessageEntityType.TEXT_MENTION and self.user: - return self.user.get_mention(entity_text) + return self.user.get_mention(entity_text, as_html=as_html) return entity_text From a74efa311e2442d436ed60c32af4afb3afd178b1 Mon Sep 17 00:00:00 2001 From: Rossouw Minnaar Date: Sun, 8 Sep 2019 20:16:19 +0200 Subject: [PATCH 007/100] Fix spelling --- aiogram/__init__.py | 2 +- docs/source/examples/index.rst | 2 +- ...dwanced_executor_example.py => advanced_executor_example.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename examples/{adwanced_executor_example.py => advanced_executor_example.py} (100%) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 3117ff13..b81ceedb 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.3' +__version__ = '2.3.dev1' __api_version__ = '4.4' diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index ab417bf4..2bedaa52 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -6,7 +6,7 @@ Examples echo_bot inline_bot - adwanced_executor_example + advanced_executor_example proxy_and_emojize finite_state_machine_example throtling_example diff --git a/examples/adwanced_executor_example.py b/examples/advanced_executor_example.py similarity index 100% rename from examples/adwanced_executor_example.py rename to examples/advanced_executor_example.py From 1cdfa3e3c2f11d8b7e0e7c137be415d2ee5bfd3c Mon Sep 17 00:00:00 2001 From: JR Minnaar Date: Thu, 12 Sep 2019 10:33:24 +0200 Subject: [PATCH 008/100] Fix all occurrences of adwanced in text --- docs/source/examples/adwanced_executor_example.rst | 10 +++++----- examples/advanced_executor_example.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/examples/adwanced_executor_example.rst b/docs/source/examples/adwanced_executor_example.rst index 88011e23..9eb5d950 100644 --- a/docs/source/examples/adwanced_executor_example.rst +++ b/docs/source/examples/adwanced_executor_example.rst @@ -1,28 +1,28 @@ .. Autogenerated file at 2018-10-28 19:31:48.335963 ========================= -Adwanced executor example +Advanced executor example ========================= !/usr/bin/env python3 **This example is outdated** In this example used ArgumentParser for configuring Your bot. Provided to start bot with webhook: -python adwanced_executor_example.py \ +python advanced_executor_example.py \ --token TOKEN_HERE \ --host 0.0.0.0 \ --port 8084 \ --host-name example.com \ --webhook-port 443 Or long polling: -python adwanced_executor_example.py --token TOKEN_HERE +python advanced_executor_example.py --token TOKEN_HERE So... In this example found small trouble: can't get bot instance in handlers. If you want to automatic change getting updates method use executor utils (from aiogram.utils.executor) TODO: Move token to environment variables. -.. literalinclude:: ../../../examples/adwanced_executor_example.py - :caption: adwanced_executor_example.py +.. literalinclude:: ../../../examples/advanced_executor_example.py + :caption: advanced_executor_example.py :language: python :linenos: :lines: 25- diff --git a/examples/advanced_executor_example.py b/examples/advanced_executor_example.py index 521783f1..bfccabbf 100644 --- a/examples/advanced_executor_example.py +++ b/examples/advanced_executor_example.py @@ -4,7 +4,7 @@ In this example used ArgumentParser for configuring Your bot. Provided to start bot with webhook: - python adwanced_executor_example.py \ + python advanced_executor_example.py \ --token TOKEN_HERE \ --host 0.0.0.0 \ --port 8084 \ @@ -12,7 +12,7 @@ Provided to start bot with webhook: --webhook-port 443 Or long polling: - python adwanced_executor_example.py --token TOKEN_HERE + python advanced_executor_example.py --token TOKEN_HERE So... In this example found small trouble: can't get bot instance in handlers. From 384b5e5b9fa7903be0b2bcfe07e537ca83baa451 Mon Sep 17 00:00:00 2001 From: JR Minnaar Date: Thu, 12 Sep 2019 10:42:46 +0200 Subject: [PATCH 009/100] Rename file to match example --- ...dwanced_executor_example.rst => advanced_executor_example.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/source/examples/{adwanced_executor_example.rst => advanced_executor_example.rst} (100%) diff --git a/docs/source/examples/adwanced_executor_example.rst b/docs/source/examples/advanced_executor_example.rst similarity index 100% rename from docs/source/examples/adwanced_executor_example.rst rename to docs/source/examples/advanced_executor_example.rst From f7fdaa115399f59b6c353e005481efd622b3fded Mon Sep 17 00:00:00 2001 From: Arwichok Date: Mon, 16 Sep 2019 14:50:23 +0300 Subject: [PATCH 010/100] add IsReplyFilter --- aiogram/dispatcher/dispatcher.py | 8 +++++++- aiogram/dispatcher/filters/builtin.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index dd764a76..600e25ba 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -10,7 +10,7 @@ from aiohttp.helpers import sentinel from aiogram.utils.deprecated import renamed_argument from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \ - RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter + RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter from .handler import Handler from .middlewares import MiddlewareManager from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \ @@ -145,6 +145,12 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.callback_query_handlers, self.inline_query_handlers, ]) + filters_factory.bind(IsReplyFilter, event_handlers=[ + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + ]) def __del__(self): self.stop_polling() diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index e15b98de..4d5af40a 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -625,3 +625,17 @@ class AdminFilter(Filter): 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 + + +class IsReplyFilter(BoundFilter): + """ + Check if message is replied and send reply message to handler + """ + key = 'is_reply' + + def __init__(self, is_reply): + self.is_reply = is_reply + + async def check(self, msg: ats.Message): + if msg.reply_to_message: + return {'reply': msg.reply_to_message} From 0365173450b7067832bb191a775de1c0b2c77fb8 Mon Sep 17 00:00:00 2001 From: Arwichok Date: Mon, 16 Sep 2019 15:15:43 +0300 Subject: [PATCH 011/100] fix IsReplyFilter typing --- 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 4d5af40a..cec9ff1b 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -636,6 +636,6 @@ class IsReplyFilter(BoundFilter): def __init__(self, is_reply): self.is_reply = is_reply - async def check(self, msg: ats.Message): + async def check(self, msg: Message): if msg.reply_to_message: return {'reply': msg.reply_to_message} From 615b5af00f69ac8acf16fad010eef9352dc8a271 Mon Sep 17 00:00:00 2001 From: Arwichok Date: Mon, 16 Sep 2019 15:47:31 +0300 Subject: [PATCH 012/100] reply True if msg == None and is_reply == False --- aiogram/dispatcher/filters/builtin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index cec9ff1b..a95ecf52 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -637,5 +637,7 @@ class IsReplyFilter(BoundFilter): self.is_reply = is_reply async def check(self, msg: Message): - if msg.reply_to_message: + if msg.reply_to_message and self.is_reply: return {'reply': msg.reply_to_message} + elif msg.reply_to_message is None and self.is_reply is False: + return True From 000cd9aad8179212c775d56ee15a0483449635a3 Mon Sep 17 00:00:00 2001 From: Arwichok Date: Mon, 23 Sep 2019 16:24:33 +0300 Subject: [PATCH 013/100] Update aiogram/dispatcher/filters/builtin.py Co-Authored-By: Alex Root Junior --- 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 a95ecf52..d11a7e35 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -639,5 +639,5 @@ class IsReplyFilter(BoundFilter): async def check(self, msg: Message): if msg.reply_to_message and self.is_reply: return {'reply': msg.reply_to_message} - elif msg.reply_to_message is None and self.is_reply is False: + elif not msg.reply_to_message and not self.is_reply: return True From 496560ab0c81172e4de7fada374fe07ac40a3249 Mon Sep 17 00:00:00 2001 From: Arwichok Date: Mon, 23 Sep 2019 16:32:18 +0300 Subject: [PATCH 014/100] Add IsReplyFilter to docs/../filters.rst --- docs/source/dispatcher/filters.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index 059a4f06..98c1e8fb 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -172,3 +172,10 @@ BoundFilter dp.filters_factory.bind(ChatIdFilter, event_handlers=[dp.message_handlers]) + + +IsReplyFilter +------------- +.. autoclass:: aiogram.dispatcher.filters.filters.IsReplyFilter + :members: + :show-inheritance: \ No newline at end of file From 1cd78648ccd5cd414e535d0141eab83670420ad3 Mon Sep 17 00:00:00 2001 From: Arwichok Date: Mon, 23 Sep 2019 16:48:23 +0300 Subject: [PATCH 015/100] Update docs/../filters.rst --- docs/source/dispatcher/filters.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index 98c1e8fb..b174f1ef 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -127,6 +127,14 @@ AdminFilter :show-inheritance: +IsReplyFilter +------------- + +.. autoclass:: aiogram.dispatcher.filters.filters.IsReplyFilter + :members: + :show-inheritance: + + Making own filters (Custom filters) =================================== @@ -173,9 +181,3 @@ BoundFilter dp.filters_factory.bind(ChatIdFilter, event_handlers=[dp.message_handlers]) - -IsReplyFilter -------------- -.. autoclass:: aiogram.dispatcher.filters.filters.IsReplyFilter - :members: - :show-inheritance: \ No newline at end of file From f13a14c5ef6e3db50e4859ece03b8cdf3e860bcf Mon Sep 17 00:00:00 2001 From: Vasiliy <45375305+eboshare@users.noreply.github.com> Date: Sat, 5 Oct 2019 20:23:54 +0400 Subject: [PATCH 016/100] Fixed send_poll required params --- aiogram/bot/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index b30e5309..5933f0db 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -862,8 +862,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_poll(self, chat_id: typing.Union[base.Integer, base.String], question: base.String, options: typing.List[base.String], - disable_notification: typing.Optional[base.Boolean], - reply_to_message_id: typing.Union[base.Integer, None], + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, From fb4eab734618eb4d817e7fc9d1d51e9768901334 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 6 Oct 2019 23:17:49 +0300 Subject: [PATCH 017/100] Prevent to trigger command filter with non-text messages --- aiogram/dispatcher/filters/builtin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index e15b98de..3a47e6fc 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -79,6 +79,9 @@ class Command(Filter): @staticmethod async def check_command(message: types.Message, commands, prefixes, ignore_case=True, ignore_mention=False): + if not message.text: # Prevent to use with non-text content types + return False + full_command = message.text.split()[0] prefix, (command, _, mention) = full_command[0], full_command[1:].partition('@') From 7efa0fc27d563137e27f71eac52c9408ea4407fd Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 6 Oct 2019 23:30:24 +0300 Subject: [PATCH 018/100] Fix docs status shield --- README.md | 2 +- README.rst | 2 +- docs/source/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 155b2848..21a19977 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![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.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) +[![Documentation Status](https://img.shields.io/readthedocs/aiogram?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 e4768f68..294c9ee8 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ AIOGramBot :target: https://core.telegram.org/bots/api :alt: Telegram Bot API -.. image:: https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square +.. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square :target: http://aiogram.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status diff --git a/docs/source/index.rst b/docs/source/index.rst index 543c793a..4fdf7a20 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -26,7 +26,7 @@ Welcome to aiogram's documentation! :target: https://core.telegram.org/bots/api :alt: Telegram Bot API - .. image:: https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square + .. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square :target: http://aiogram.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status From 7ed44533bb2869a2ab42b8bdac2ebe9dcffb5a02 Mon Sep 17 00:00:00 2001 From: skhortiuk Date: Mon, 7 Oct 2019 12:51:42 +0300 Subject: [PATCH 019/100] Fixed missed `IsReplyFilter` import --- aiogram/dispatcher/filters/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 277db03a..67c13872 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,5 +1,6 @@ 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, IsReplyFilter from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -24,6 +25,7 @@ __all__ = [ 'StateFilter', 'Text', 'IDFilter', + 'IsReplyFilter', 'AdminFilter', 'get_filter_spec', 'get_filters_spec', From 68a9df92227b76b754fe3723679af37eb4e324cb Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Wed, 9 Oct 2019 20:03:48 +0300 Subject: [PATCH 020/100] Create OrderedHelper --- aiogram/utils/helper.py | 36 +++++++++++++++++++++++++++++++++ tests/test_utils/test_helper.py | 22 ++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 tests/test_utils/test_helper.py diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index 443a2ffe..735afe5d 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -13,6 +13,9 @@ Example: >>> print(MyHelper.all()) <<< ['barItem', 'bazItem', 'fooItem', 'lorem'] """ +from typing import List + +PROPS_KEYS_ATTR_NAME = '_props_keys' class Helper: @@ -191,3 +194,36 @@ class ItemsList(list): return self __iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add + + +class OrderedHelperMeta(type): + + def __new__(mcs, name, bases, namespace, **kwargs): + cls = super().__new__(mcs, name, bases, namespace) + + props_keys = [] + + for prop_name in (name for name, prop in namespace.items() if isinstance(prop, (Item, ListItem))): + props_keys.append(prop_name) + + setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys) + + return cls + + +class OrderedHelper(metaclass=OrderedHelperMeta): + mode = '' + + @classmethod + def all(cls) -> List[str]: + """ + Get all Items values + """ + result = [] + for name in getattr(cls, PROPS_KEYS_ATTR_NAME, []): + value = getattr(cls, name) + if isinstance(value, ItemsList): + result.append(value[0]) + else: + result.append(value) + return result diff --git a/tests/test_utils/test_helper.py b/tests/test_utils/test_helper.py new file mode 100644 index 00000000..d202d289 --- /dev/null +++ b/tests/test_utils/test_helper.py @@ -0,0 +1,22 @@ +from aiogram.utils.helper import OrderedHelper, Item, ListItem + + +class TestOrderedHelper: + + def test_items_are_ordered(self): + class Helper(OrderedHelper): + A = Item() + D = Item() + C = Item() + B = Item() + + assert Helper.all() == ['A', 'D', 'C', 'B'] + + def test_list_items_are_ordered(self): + class Helper(OrderedHelper): + A = ListItem() + D = ListItem() + C = ListItem() + B = ListItem() + + assert Helper.all() == ['A', 'D', 'C', 'B'] From 2f5415c1c94e13d969ba164fb69b612d65ddba19 Mon Sep 17 00:00:00 2001 From: Gabben <43146729+gabbhack@users.noreply.github.com> Date: Thu, 10 Oct 2019 19:20:49 +0500 Subject: [PATCH 021/100] Fix incorrect completion order. --- aiogram/utils/executor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 33f80684..fe3483f6 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -361,11 +361,11 @@ class Executor: await callback(self.dispatcher) async def _shutdown_polling(self, wait_closed=False): - await self._shutdown() - for callback in self._on_shutdown_polling: await callback(self.dispatcher) + await self._shutdown() + if wait_closed: await self.dispatcher.wait_closed() From e32a45f4f886137ce1927f366de45ab8f4028234 Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 17:52:27 +0300 Subject: [PATCH 022/100] Update message.py Updated send_copy: Added the ability to specify reply_markup, parse_mode --- aiogram/types/message.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 5347027c..55b2b420 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1567,30 +1567,37 @@ class Message(base.TelegramObject): 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, + reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = self.reply_markup, + parse_mode: typing.Union[base.String, None] = None, ) -> Message: """ Send copy of current message :param chat_id: - :param with_markup: :param disable_notification: :param reply_to_message_id: + :param reply_markup: + :param parse_mode: :return: """ - kwargs = {"chat_id": chat_id, "parse_mode": ParseMode.HTML} + kwargs = {"chat_id": chat_id} + text = self.text or self.caption 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: + if parse_mode is not None: + kwargs["parse_mode"] = parse_mode + if parse_mode == 'html': + text = self.html_text if (self.text or self.caption) else None + if parse_mode == 'markdown': + text = self.md_text if (self.text or self.caption) else None + if 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 self.bot.send_message(text=text, **kwargs) elif self.audio: From 1f177360c4fc56150117cbcd1da22620490d623f Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 17:59:32 +0300 Subject: [PATCH 023/100] Update message.py --- 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 55b2b420..3eda6328 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1596,7 +1596,7 @@ class Message(base.TelegramObject): if parse_mode == 'markdown': text = self.md_text if (self.text or self.caption) else None if reply_markup: - kwargs["reply_markup"] = self.reply_markup + kwargs["reply_markup"] = reply_markup if self.text: return await self.bot.send_message(text=text, **kwargs) From e57c761c408f832fac2394f896f4f012302a37ad Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 18:03:31 +0300 Subject: [PATCH 024/100] Update message.py Second attempt to fix reply_markup --- aiogram/types/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 3eda6328..819199ca 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1569,7 +1569,7 @@ class Message(base.TelegramObject): chat_id: typing.Union[str, int], disable_notification: typing.Optional[bool] = None, reply_to_message_id: typing.Optional[int] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = self.reply_markup, + reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None, parse_mode: typing.Union[base.String, None] = None, ) -> Message: """ @@ -1582,7 +1582,7 @@ class Message(base.TelegramObject): :param parse_mode: :return: """ - kwargs = {"chat_id": chat_id} + kwargs = {"chat_id": chat_id, "reply_markup": self.reply_markup} text = self.text or self.caption if disable_notification is not None: From b172faf89f1d523a92173951a9be0f608cc68817 Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 18:22:44 +0300 Subject: [PATCH 025/100] Update message.py --- aiogram/types/message.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 819199ca..6612b22a 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1582,7 +1582,7 @@ class Message(base.TelegramObject): :param parse_mode: :return: """ - kwargs = {"chat_id": chat_id, "reply_markup": self.reply_markup} + kwargs = {"chat_id": chat_id, "reply_markup": reply_markup or self.reply_markup} text = self.text or self.caption if disable_notification is not None: @@ -1595,8 +1595,6 @@ class Message(base.TelegramObject): text = self.html_text if (self.text or self.caption) else None if parse_mode == 'markdown': text = self.md_text if (self.text or self.caption) else None - if reply_markup: - kwargs["reply_markup"] = reply_markup if self.text: return await self.bot.send_message(text=text, **kwargs) From 1cd4712eb412af97c203245e3c6804b41d7b261f Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 18:37:06 +0300 Subject: [PATCH 026/100] Update message.py Some fixes --- aiogram/types/message.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 6612b22a..4b8baa02 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1582,19 +1582,15 @@ class Message(base.TelegramObject): :param parse_mode: :return: """ - kwargs = {"chat_id": chat_id, "reply_markup": reply_markup or self.reply_markup} + kwargs = {"chat_id": chat_id, "reply_markup": reply_markup or self.reply_markup, "parse_mode": parse_mode or ParseMode.HTML} text = self.text or self.caption 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 parse_mode is not None: - kwargs["parse_mode"] = parse_mode - if parse_mode == 'html': - text = self.html_text if (self.text or self.caption) else None - if parse_mode == 'markdown': - text = self.md_text if (self.text or self.caption) else None + if not kwargs.get("reply_markup"): + kwargs.pop("reply_markup") if self.text: return await self.bot.send_message(text=text, **kwargs) From 68ce9687ec4542a72bd427148decacb30e391273 Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 18:38:13 +0300 Subject: [PATCH 027/100] Update message.py --- 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 4b8baa02..5a67b180 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1583,7 +1583,7 @@ class Message(base.TelegramObject): :return: """ kwargs = {"chat_id": chat_id, "reply_markup": reply_markup or self.reply_markup, "parse_mode": parse_mode or ParseMode.HTML} - text = self.text or self.caption + text = self.html_text if (self.text or self.caption) else None if disable_notification is not None: kwargs["disable_notification"] = disable_notification From bbfc994073e7c5f34091489f1a0133032d845a2e Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 18:42:24 +0300 Subject: [PATCH 028/100] Update message.py --- aiogram/types/message.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 5a67b180..441e176c 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1582,15 +1582,17 @@ class Message(base.TelegramObject): :param parse_mode: :return: """ - kwargs = {"chat_id": chat_id, "reply_markup": reply_markup or self.reply_markup, "parse_mode": parse_mode or ParseMode.HTML} + kwargs = { + "chat_id": chat_id, + "reply_markup": reply_markup or self.reply_markup, + "parse_mode": parse_mode or ParseMode.HTML + } text = self.html_text if (self.text or self.caption) else None 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 not kwargs.get("reply_markup"): - kwargs.pop("reply_markup") if self.text: return await self.bot.send_message(text=text, **kwargs) From 37e6428b7bc30067726a6663659da2a3af581607 Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 22:56:48 +0300 Subject: [PATCH 029/100] Update message.py deleted parse_mode arg from send_copy args --- aiogram/types/message.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 441e176c..45235722 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1570,7 +1570,6 @@ class Message(base.TelegramObject): disable_notification: typing.Optional[bool] = None, reply_to_message_id: typing.Optional[int] = None, reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None, - parse_mode: typing.Union[base.String, None] = None, ) -> Message: """ Send copy of current message @@ -1585,7 +1584,7 @@ class Message(base.TelegramObject): kwargs = { "chat_id": chat_id, "reply_markup": reply_markup or self.reply_markup, - "parse_mode": parse_mode or ParseMode.HTML + "parse_mode": ParseMode.HTML } text = self.html_text if (self.text or self.caption) else None From a6c8e4c2494b10a9d1ab9cc20e62b8da5cf8fbd0 Mon Sep 17 00:00:00 2001 From: Bunk100 <37146584+Bunk100@users.noreply.github.com> Date: Sat, 12 Oct 2019 23:06:41 +0300 Subject: [PATCH 030/100] Update message.py --- aiogram/types/message.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 45235722..49626060 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1578,7 +1578,6 @@ class Message(base.TelegramObject): :param disable_notification: :param reply_to_message_id: :param reply_markup: - :param parse_mode: :return: """ kwargs = { From bd90c726b5e5fc06735ab98fa1d42da56a9f7cd7 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 14 Oct 2019 14:53:12 +0300 Subject: [PATCH 031/100] Fix Bot.__del__ for cases when event loop is closed --- aiogram/bot/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 608abd06..8cc64e33 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -100,10 +100,13 @@ class BaseBot: self.parse_mode = parse_mode def __del__(self): + if not hasattr(self, 'loop'): + return if self.loop.is_running(): self.loop.create_task(self.close()) - else: - self.loop.run_until_complete(self.close()) + return + loop = asyncio.new_event_loop() + loop.run_until_complete(self.close()) @staticmethod def _prepare_timeout( From 238d1d97618f56c239e833ab1338064dacfc6540 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 14 Oct 2019 15:01:45 +0300 Subject: [PATCH 032/100] Pin aiohttp version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 37d328b3..7f7dc1ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -aiohttp>=3.5.4 +aiohttp>=3.5.4,<4.0.0 Babel>=2.6.0 certifi>=2019.3.9 From b88ae7a43561eda00e71d4fc7b38f3bc267798cb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 14 Oct 2019 16:06:35 +0300 Subject: [PATCH 033/100] library -> framework --- README.md | 2 +- README.rst | 2 +- docs/source/index.rst | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 21a19977..02a9374f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![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) -**aiogram** is a pretty simple and fully asynchronous library for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler. +**aiogram** is a pretty simple and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler. You can [read the docs here](http://aiogram.readthedocs.io/en/latest/). diff --git a/README.rst b/README.rst index 294c9ee8..f7c3e951 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ AIOGramBot :alt: MIT License -**aiogram** is a pretty simple and fully asynchronous library for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler. +**aiogram** is a pretty simple and fully asynchronous framework for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler. You can `read the docs here `_. diff --git a/docs/source/index.rst b/docs/source/index.rst index 4fdf7a20..7b6fd231 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,7 +39,7 @@ Welcome to aiogram's documentation! :alt: MIT License -**aiogram** is a pretty simple and fully asynchronous library for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler. +**aiogram** is a pretty simple and fully asynchronous framework for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler. Official aiogram resources diff --git a/setup.py b/setup.py index b5c9e61c..a7876f96 100755 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( author='Alex Root Junior', requires_python='>=3.7', author_email='jroot.junior@gmail.com', - description='Is a pretty simple and fully asynchronous library for Telegram Bot API', + description='Is a pretty simple and fully asynchronous framework for Telegram Bot API', long_description=get_description(), classifiers=[ 'Development Status :: 5 - Production/Stable', From 9610c698bea3d073ff17b91f6aa3d959166aabe1 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 14 Oct 2019 16:11:13 +0300 Subject: [PATCH 034/100] Update installation docs page --- docs/source/install.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index cd89dc54..d8d591c1 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -7,25 +7,34 @@ Using PIP $ pip install -U aiogram +Using Pipenv +------------ + .. code-block:: bash + + $ pipenv install 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 ------------ + + Development versions: + .. code-block:: bash $ git clone https://github.com/aiogram/aiogram.git $ cd aiogram $ python setup.py install - or if you want to install development version (maybe unstable): + Or if you want to install stable version (The same with version form PyPi): .. code-block:: bash $ git clone https://github.com/aiogram/aiogram.git $ cd aiogram - $ git checkout dev-2.x + $ git checkout master $ python setup.py install From cf7786a4673bd7e873cb93f1cf94a2b7de4a890d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 14 Oct 2019 23:52:19 +0300 Subject: [PATCH 035/100] Optimize Message.send_copy --- aiogram/types/message.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 49626060..8fdece2b 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1581,17 +1581,14 @@ class Message(base.TelegramObject): :return: """ kwargs = { - "chat_id": chat_id, - "reply_markup": reply_markup or self.reply_markup, - "parse_mode": ParseMode.HTML - } + "chat_id": chat_id, + "reply_markup": reply_markup or self.reply_markup, + "parse_mode": ParseMode.HTML, + "disable_notification": disable_notification, + "reply_to_message_id": reply_to_message_id, + } text = self.html_text if (self.text or self.caption) else None - 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 self.text: return await self.bot.send_message(text=text, **kwargs) elif self.audio: From 2ed98e566eda4fca57d592ef719424eb2f6bb0aa Mon Sep 17 00:00:00 2001 From: eboshare Date: Tue, 15 Oct 2019 18:00:37 +0400 Subject: [PATCH 036/100] Add aiohttp speedups Add aiohttp[speedups] in instalation recomendations Includes cchardet and aiodns. --- docs/source/install.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/source/install.rst b/docs/source/install.rst index d8d591c1..8d59f2c8 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -62,4 +62,36 @@ You can speedup your bots by following next instructions: $ pip install ujson +- Use aiohttp speedups + + - Use `cchardet `_ instead of chardet module. + + *cChardet* is high speed universal character encoding detector. + + **Installation:** + + .. code-block:: bash + + $ pip install cchardet + + - Use `aiodns `_ for speeding up DNS resolving. + + *aiodns* provides a simple way for doing asynchronous DNS resolutions. + + **Installation:** + + .. code-block:: bash + + $ pip install aiodns + + - Installing speedups altogether. + + The following will get you ``aiohttp`` along with ``cchardet,``, ``aiodns`` and ``brotlipy`` in one bundle. + + **Installation:** + + .. code-block:: bash + + $ pip install aiohttp[speedups] + In addition, you don't need do nothing, *aiogram* is automatically starts using that if is found in your environment. From 9a3c6f5ece14e0a9b91f2cc06ae07466991ab507 Mon Sep 17 00:00:00 2001 From: eboshare Date: Tue, 15 Oct 2019 18:04:14 +0400 Subject: [PATCH 037/100] Remove typo --- docs/source/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index 8d59f2c8..b2fd8e38 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -86,7 +86,7 @@ You can speedup your bots by following next instructions: - Installing speedups altogether. - The following will get you ``aiohttp`` along with ``cchardet,``, ``aiodns`` and ``brotlipy`` in one bundle. + The following will get you ``aiohttp`` along with ``cchardet``, ``aiodns`` and ``brotlipy`` in one bundle. **Installation:** From 675def5013ccfcea1a87d792818fc17fee0d8ac8 Mon Sep 17 00:00:00 2001 From: dark0ghost Date: Tue, 29 Oct 2019 01:37:14 +0300 Subject: [PATCH 038/100] add typing --- aiogram/types/base.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 97f67b16..2fd9129d 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -9,6 +9,8 @@ from babel.support import LazyProxy from .fields import BaseField from ..utils import json from ..utils.mixins import ContextInstanceMixin +if typing.TYPE_CHECKING: + from ..bot.bot import Bot __all__ = ('MetaTelegramObject', 'TelegramObject', 'InputFile', 'String', 'Integer', 'Float', 'Boolean') @@ -22,6 +24,7 @@ String = TypeVar('String', bound=str) Integer = TypeVar('Integer', bound=int) Float = TypeVar('Float', bound=float) Boolean = TypeVar('Boolean', bound=bool) +T = TypeVar('T') class MetaTelegramObject(type): @@ -30,7 +33,7 @@ class MetaTelegramObject(type): """ _objects = {} - def __new__(mcs, name, bases, namespace, **kwargs): + def __new__(mcs: typing.Type[T], name: str, bases: typing.Tuple[typing.Type], namespace: typing.Dict[str, typing.Any], **kwargs: typing.Any) -> T: cls = super(MetaTelegramObject, mcs).__new__(mcs, name, bases, namespace) props = {} @@ -71,7 +74,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): Abstract class for telegram objects """ - def __init__(self, conf=None, **kwargs): + def __init__(self, conf: typing.Dict[str, typing.Any]=None, **kwargs: typing.Any) -> None: """ Deserialize object @@ -117,7 +120,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return getattr(self, ALIASES_ATTR_NAME, {}) @property - def values(self): + def values(self) -> typing.Tuple[str]: """ Get values @@ -128,11 +131,11 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return getattr(self, VALUES_ATTR_NAME) @property - def telegram_types(self): + def telegram_types(self) -> typing.List[TelegramObject]: return type(self).telegram_types @classmethod - def to_object(cls, data): + def to_object(cls: typing.Type[T], data: typing.Dict[str, typing.Any]) -> T: """ Deserialize object @@ -142,7 +145,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return cls(**data) @property - def bot(self): + def bot(self) -> Bot: from ..bot.bot import Bot bot = Bot.get_current() @@ -152,7 +155,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): "'Bot.set_current(bot_instance)'") return bot - def to_python(self) -> typing.Dict: + def to_python(self) -> typing.Dict[str, typing.Any]: """ Get object as JSON serializable @@ -170,7 +173,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): result[self.props_aliases.get(name, name)] = value return result - def clean(self): + def clean(self) -> None: """ Remove empty values """ @@ -188,7 +191,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return json.dumps(self.to_python()) @classmethod - def create(cls, *args, **kwargs): + def create(cls: Type[T], *args: typing.Any, **kwargs: typing.Any) -> T: raise NotImplemented def __str__(self) -> str: @@ -199,7 +202,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): """ return self.as_json() - def __getitem__(self, item): + def __getitem__(self, item: typing.Union[str, int]) -> typing.Any: """ Item getter (by key) @@ -210,7 +213,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return self.props[item].get_value(self) raise KeyError(item) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: typing.Any) -> None: """ Item setter (by key) @@ -222,7 +225,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return self.props[key].set_value(self, value, self.conf.get('parent', None)) raise KeyError(key) - def __contains__(self, item): + def __contains__(self, item: typing.Dict[str, typing.Any]) -> bool: """ Check key contains in that object @@ -232,7 +235,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): self.clean() return item in self.values - def __iter__(self): + def __iter__(self) -> typing.Iterator[str]: """ Iterate over items @@ -241,7 +244,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): for item in self.to_python().items(): yield item - def iter_keys(self): + def iter_keys(self) -> typing.Generator[typing.Any, None, None]: """ Iterate over keys @@ -250,7 +253,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): for key, _ in self: yield key - def iter_values(self): + def iter_values(self) -> typing.Generator[typing.Any, None, None]: """ Iterate over values @@ -259,9 +262,9 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): for _, value in self: yield value - def __hash__(self): - def _hash(obj): - buf = 0 + def __hash__(self) -> int: + def _hash(obj)-> int: + buf: int = 0 if isinstance(obj, list): for item in obj: buf += _hash(item) @@ -281,5 +284,5 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return result - def __eq__(self, other): + def __eq__(self, other: TelegramObject) -> bool: return isinstance(other, self.__class__) and hash(other) == hash(self) From cb4f459597b85954eb03f34366086c1250a8a1bf Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 29 Oct 2019 21:19:55 +0200 Subject: [PATCH 039/100] Fix typing for until_date argument (can be datetime or timedelta) --- aiogram/bot/bot.py | 7 +++++-- aiogram/types/chat.py | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 5933f0db..5e8f05d9 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime import typing import warnings @@ -963,7 +964,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return types.File(**result) async def kick_chat_member(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer, - until_date: typing.Union[base.Integer, None] = None) -> base.Boolean: + until_date: typing.Union[ + base.Integer, datetime.datetime, datetime.timedelta, None] = None) -> base.Boolean: """ Use this method to kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group @@ -1018,7 +1020,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): 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, + until_date: typing.Union[ + base.Integer, datetime.datetime, datetime.timedelta, None] = None, can_send_messages: typing.Union[base.Boolean, None] = None, can_send_media_messages: typing.Union[base.Boolean, None] = None, can_send_other_messages: typing.Union[base.Boolean, None] = None, diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index f5c521a5..66b8fe4d 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import datetime import typing from . import base @@ -164,7 +165,7 @@ class Chat(base.TelegramObject): return await self.bot.delete_chat_description(self.id, description) async def kick(self, user_id: base.Integer, - until_date: typing.Union[base.Integer, None] = None): + until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None): """ Use this method to kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group @@ -205,7 +206,7 @@ class Chat(base.TelegramObject): async def restrict(self, user_id: base.Integer, permissions: typing.Optional[ChatPermissions] = None, - until_date: typing.Union[base.Integer, None] = None, + until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None, can_send_messages: typing.Union[base.Boolean, None] = None, can_send_media_messages: typing.Union[base.Boolean, None] = None, can_send_other_messages: typing.Union[base.Boolean, None] = None, From f8d255b3534ec5e99bce2bbed870318808d59e92 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 29 Oct 2019 22:36:43 +0200 Subject: [PATCH 040/100] Prevent to serialize text as date when rapidjson is used --- aiogram/utils/json.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aiogram/utils/json.py b/aiogram/utils/json.py index b2305b88..56f122e4 100644 --- a/aiogram/utils/json.py +++ b/aiogram/utils/json.py @@ -21,13 +21,11 @@ for json_lib in (RAPIDJSON, UJSON): if mode == RAPIDJSON: def dumps(data): - return json.dumps(data, ensure_ascii=False, number_mode=json.NM_NATIVE, - datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC) + return json.dumps(data, ensure_ascii=False) def loads(data): - return json.loads(data, number_mode=json.NM_NATIVE, - datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC) + return json.loads(data, number_mode=json.NM_NATIVE) elif mode == UJSON: def loads(data): From d5f5cea6653ad39c88499bc695137f26baf0b73f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 29 Oct 2019 22:42:31 +0200 Subject: [PATCH 041/100] Bump version to 2.4 --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index b81ceedb..edea1806 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.3.dev1' +__version__ = '2.4' __api_version__ = '4.4' From 9571669ca4b06165031d8f9830130f3c638b60d8 Mon Sep 17 00:00:00 2001 From: Victor Usachev Date: Wed, 30 Oct 2019 02:42:24 +0300 Subject: [PATCH 042/100] refactor(utils): reduce code duplication --- aiogram/utils/deprecated.py | 38 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index cb22c506..5232e8a3 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -99,35 +99,27 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve """ def decorator(func): - if asyncio.iscoroutinefunction(func): + is_coroutine = asyncio.iscoroutinefunction(func) + + def _handling(kwargs): + routine_type = 'coroutine' if is_coroutine else 'function' + if old_name in kwargs: + warn_deprecated(f"In {routine_type} '{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.pop(old_name)}) + return kwargs + + if is_coroutine: @functools.wraps(func) async def wrapped(*args, **kwargs): - if old_name in kwargs: - 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], - } - ) - kwargs.pop(old_name) + kwargs = _handling(kwargs) return 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}` " - f"is renamed to `{new_name}` " - f"and will be removed in aiogram {until_version}", - stacklevel=stacklevel) - kwargs.update( - { - new_name: kwargs[old_name], - } - ) - kwargs.pop(old_name) + kwargs = _handling(kwargs) return func(*args, **kwargs) return wrapped From 386a1586e068211d3b7df672aea6bea12123c6e7 Mon Sep 17 00:00:00 2001 From: uburuntu Date: Sun, 10 Nov 2019 00:46:29 +0300 Subject: [PATCH 043/100] enh: unify default as_html argument --- aiogram/types/chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 66b8fe4d..d56ee0fa 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -63,7 +63,7 @@ class Chat(base.TelegramObject): return f"tg://user?id={self.id}" - def get_mention(self, name=None, as_html=False): + def get_mention(self, name=None, as_html=True): if name is None: name = self.mention if as_html: From 8117b0a77c0764577b414afbb6803be0bdcbb4f5 Mon Sep 17 00:00:00 2001 From: Evgeny Petrov Date: Wed, 13 Nov 2019 23:09:27 +0300 Subject: [PATCH 044/100] Replaced is_admin() with is_chat_admin() in filters example --- docs/source/migration_1_to_2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/migration_1_to_2.rst b/docs/source/migration_1_to_2.rst index 67684831..3d98d86e 100644 --- a/docs/source/migration_1_to_2.rst +++ b/docs/source/migration_1_to_2.rst @@ -73,7 +73,7 @@ Also you can bind your own filters for using as keyword arguments: async def check(self, message: types.Message): member = await bot.get_chat_member(message.chat.id, message.from_user.id) - return member.is_admin() + return member.is_chat_admin() dp.filters_factory.bind(MyFilter) From b9130e2e1cc975494e27ace279826d0bccba14ed Mon Sep 17 00:00:00 2001 From: alex_shavelev Date: Thu, 14 Nov 2019 19:15:44 +0200 Subject: [PATCH 045/100] enable syntax 'key in dispatcher', issue #233 --- aiogram/utils/mixins.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index e6857263..90ef4edb 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -22,6 +22,9 @@ class DataMixin: def __delitem__(self, key): del self.data[key] + def __contains__(self, key): + return key in self.data + def get(self, key, default=None): return self.data.get(key, default) From f5d008938f9ecfd55f694293f565cc9962a15ba4 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 23 Nov 2019 11:46:50 +0300 Subject: [PATCH 046/100] #239 added check for right part of token exists; removed check for left part length --- aiogram/bot/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 675626ac..9dea86ea 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -28,7 +28,7 @@ def check_token(token: str) -> bool: raise exceptions.ValidationError('Token is invalid!') left, sep, right = token.partition(':') - if (not sep) or (not left.isdigit()) or (len(left) < 3): + if (not sep) or (not left.isdigit()) or (not right): raise exceptions.ValidationError('Token is invalid!') return True From 89b0754b33e5d211bc11973ed56a1e9719e01ecd Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 23 Nov 2019 12:02:30 +0300 Subject: [PATCH 047/100] #239 added test cases for check_token --- tests/test_bot/test_api.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/test_bot/test_api.py b/tests/test_bot/test_api.py index c5193bcc..0543a11f 100644 --- a/tests/test_bot/test_api.py +++ b/tests/test_bot/test_api.py @@ -1,18 +1,28 @@ import pytest -from aiogram.bot.api import check_token +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 +INVALID_TOKENS = [ + '123456789:AABBCCDDEEFFaabbccddeeff 123456789', # space is exists + 'ABC:AABBCCDDEEFFaabbccddeeff123456789', # left part is not digit + ':AABBCCDDEEFFaabbccddeeff123456789', # there is no left part + '123456789:', # there is no right part + 'ABC AABBCCDDEEFFaabbccddeeff123456789', # there is no ':' separator +] -class Test_check_token: +@pytest.fixture(params=INVALID_TOKENS, name='invalid_token') +def invalid_token_fixture(request): + return request.param + + +class TestCheckToken: def test_valid(self): assert check_token(VALID_TOKEN) is True - def test_invalid_token(self): + def test_invalid_token(self, invalid_token): with pytest.raises(ValidationError): - check_token(INVALID_TOKEN) + check_token(invalid_token) From 4523a1cab397b3cdf36c853c43f9c70119e952a0 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 23 Nov 2019 12:45:47 +0300 Subject: [PATCH 048/100] #239 added token type validation --- aiogram/bot/api.py | 8 +++++++- tests/test_bot/test_api.py | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 9dea86ea..9589c3e5 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -24,8 +24,14 @@ def check_token(token: str) -> bool: :param token: :return: """ + if not isinstance(token, str): + message = (f"Token is invalid! " + f"It must be 'str' type instead of {type(token)} type.") + raise exceptions.ValidationError(message) + if any(x.isspace() for x in token): - raise exceptions.ValidationError('Token is invalid!') + message = "Token is invalid! It can't contains spaces." + raise exceptions.ValidationError(message) left, sep, right = token.partition(':') if (not sep) or (not left.isdigit()) or (not right): diff --git a/tests/test_bot/test_api.py b/tests/test_bot/test_api.py index 0543a11f..29418169 100644 --- a/tests/test_bot/test_api.py +++ b/tests/test_bot/test_api.py @@ -10,6 +10,10 @@ INVALID_TOKENS = [ ':AABBCCDDEEFFaabbccddeeff123456789', # there is no left part '123456789:', # there is no right part 'ABC AABBCCDDEEFFaabbccddeeff123456789', # there is no ':' separator + None, # is None + 12345678, # is digit + {}, # is dict + [], # is dict ] From a42252b5c6c898378c221bc01075e9627e95e09e Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 24 Nov 2019 00:40:11 +0300 Subject: [PATCH 049/100] #238 Added deep linking feature --- aiogram/utils/deep_linking.py | 94 +++++++++++++++++++++++++++ docs/source/utils/deep_linking.rst | 6 ++ tests/test_utils/test_deep_linking.py | 75 +++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 aiogram/utils/deep_linking.py create mode 100644 docs/source/utils/deep_linking.rst create mode 100644 tests/test_utils/test_deep_linking.py diff --git a/aiogram/utils/deep_linking.py b/aiogram/utils/deep_linking.py new file mode 100644 index 00000000..f01c4ed4 --- /dev/null +++ b/aiogram/utils/deep_linking.py @@ -0,0 +1,94 @@ +""" +Deep linking + +Telegram bots have a deep linking mechanism, that allows for passing additional +parameters to the bot on startup. It could be a command that launches the bot — or +an auth token to connect the user's Telegram account to their account on some +external service. + +You can read detailed description in the source: +https://core.telegram.org/bots#deep-linking + +We have add some utils to get deep links more handy. + +Basic link example: +>>> from aiogram.utils.deep_linking import get_start_link +>>> link = await get_start_link('foo') # result: 'https://t.me/MyBot?start=foo' + +Encoded link example: +>>> from aiogram.utils.deep_linking import get_start_link, decode_payload +>>> link = await get_start_link('foo', encode=True) # result: 'https://t.me/MyBot?start=Zm9v' +>>> data = decode_payload('Zm9v') # result: 'foo' + +""" + + +async def get_start_link(payload: str, encode=False) -> str: + """ + Use this method to handy get 'start' deep link with your payload. + If you need to encode payload or pass special characters - set encode as True + + :param payload: args passed with /start + :param encode: encode payload with base64url + :return: link + """ + return await _create_link('start', payload, encode) + + +async def get_startgroup_link(payload: str, encode=False) -> str: + """ + Use this method to handy get 'startgroup' deep link with your payload. + If you need to encode payload or pass special characters - set encode as True + + :param payload: args passed with /start + :param encode: encode payload with base64url + :return: link + """ + return await _create_link('startgroup', payload, encode) + + +async def _create_link(link_type, payload: str, encode=False): + bot = await _get_bot_user() + payload = filter_payload(payload) + if encode: + payload = encode_payload(payload) + return f'https://t.me/{bot.username}?{link_type}={payload}' + + +def encode_payload(payload: str) -> str: + """ Encode payload with URL-safe base64url. """ + from base64 import urlsafe_b64encode + result: bytes = urlsafe_b64encode(payload.encode()) + return result.decode() + + +def decode_payload(payload: str) -> str: + """ Decode payload with URL-safe base64url. """ + from base64 import urlsafe_b64decode + result: bytes = urlsafe_b64decode(payload + '=' * (4 - len(payload) % 4)) + return result.decode() + + +def filter_payload(payload: str) -> str: + """ Convert payload to text and search for not allowed symbols. """ + import re + + # convert to string + if not isinstance(payload, str): + payload = str(payload) + + # search for not allowed characters + if re.search(r'[^_A-z0-9-]', payload): + message = ('Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. ' + 'We recommend to encode parameters with binary and other ' + 'types of content.') + raise ValueError(message) + + return payload + + +async def _get_bot_user(): + """ Get current user of bot. """ + from ..bot import Bot + bot = Bot.get_current() + return await bot.me diff --git a/docs/source/utils/deep_linking.rst b/docs/source/utils/deep_linking.rst new file mode 100644 index 00000000..e00e0d20 --- /dev/null +++ b/docs/source/utils/deep_linking.rst @@ -0,0 +1,6 @@ +============ +Deep linking +============ + +.. automodule:: aiogram.utils.deep_linking + :members: diff --git a/tests/test_utils/test_deep_linking.py b/tests/test_utils/test_deep_linking.py new file mode 100644 index 00000000..f6978c41 --- /dev/null +++ b/tests/test_utils/test_deep_linking.py @@ -0,0 +1,75 @@ +import pytest + +from aiogram.utils.deep_linking import decode_payload, encode_payload, filter_payload +from aiogram.utils.deep_linking import get_start_link, get_startgroup_link +from tests.types import dataset + +# enable asyncio mode +pytestmark = pytest.mark.asyncio + +PAYLOADS = [ + 'foo', + 'AAbbCCddEEff1122334455', + 'aaBBccDDeeFF5544332211', + -12345678901234567890, + 12345678901234567890, +] + +WRONG_PAYLOADS = [ + '@BotFather', + 'spaces spaces spaces', + 1234567890123456789.0, +] + + +@pytest.fixture(params=PAYLOADS, name='payload') +def payload_fixture(request): + return request.param + + +@pytest.fixture(params=WRONG_PAYLOADS, name='wrong_payload') +def wrong_payload_fixture(request): + return request.param + + +@pytest.fixture(autouse=True) +def get_bot_user_fixture(monkeypatch): + """ Monkey patching of bot.me calling. """ + from aiogram.utils import deep_linking + + async def get_bot_user_mock(): + from aiogram.types import User + return User(**dataset.USER) + + monkeypatch.setattr(deep_linking, '_get_bot_user', get_bot_user_mock) + + +class TestDeepLinking: + async def test_get_start_link(self, payload): + link = await get_start_link(payload) + assert link == f'https://t.me/{dataset.USER["username"]}?start={payload}' + + async def test_wrong_symbols(self, wrong_payload): + with pytest.raises(ValueError): + await get_start_link(wrong_payload) + + async def test_get_startgroup_link(self, payload): + link = await get_startgroup_link(payload) + assert link == f'https://t.me/{dataset.USER["username"]}?startgroup={payload}' + + async def test_filter_encode_and_decode(self, payload): + _payload = filter_payload(payload) + encoded = encode_payload(_payload) + print(encoded) + decoded = decode_payload(encoded) + assert decoded == str(payload) + + async def test_get_start_link_with_encoding(self, payload): + # define link + link = await get_start_link(payload, encode=True) + + # define reference link + payload = filter_payload(payload) + encoded_payload = encode_payload(payload) + + assert link == f'https://t.me/{dataset.USER["username"]}?start={encoded_payload}' From 1305a06b246ce0c86ff0ddf5d232cc14e8d32120 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 24 Nov 2019 01:08:06 +0300 Subject: [PATCH 050/100] #238 Removed prints and fixed example --- aiogram/utils/deep_linking.py | 3 ++- tests/test_utils/test_deep_linking.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/deep_linking.py b/aiogram/utils/deep_linking.py index f01c4ed4..2b420cf8 100644 --- a/aiogram/utils/deep_linking.py +++ b/aiogram/utils/deep_linking.py @@ -18,7 +18,8 @@ Basic link example: Encoded link example: >>> from aiogram.utils.deep_linking import get_start_link, decode_payload >>> link = await get_start_link('foo', encode=True) # result: 'https://t.me/MyBot?start=Zm9v' ->>> data = decode_payload('Zm9v') # result: 'foo' +>>> # and decode it back: +>>> payload = decode_payload('Zm9v') # result: 'foo' """ diff --git a/tests/test_utils/test_deep_linking.py b/tests/test_utils/test_deep_linking.py index f6978c41..a1d01e4e 100644 --- a/tests/test_utils/test_deep_linking.py +++ b/tests/test_utils/test_deep_linking.py @@ -60,7 +60,6 @@ class TestDeepLinking: async def test_filter_encode_and_decode(self, payload): _payload = filter_payload(payload) encoded = encode_payload(_payload) - print(encoded) decoded = decode_payload(encoded) assert decoded == str(payload) From cf55ab76435286cc96db412c46c5acc65c1b57c6 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 24 Nov 2019 22:13:31 +0300 Subject: [PATCH 051/100] #195 aiogram is a framework --- docs/source/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index b2fd8e38..58c0f208 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -15,7 +15,7 @@ Using Pipenv 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. +*aiogram* is also available in Arch User Repository, so you can install this framework 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 ------------ From 58f9ca5802f25902912dbc932ccec2d8b45ae212 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Fri, 29 Nov 2019 23:24:55 +0300 Subject: [PATCH 052/100] #238 Deep linking implemented to CommandStart filter --- aiogram/dispatcher/filters/builtin.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 55ed63e5..ebbf4068 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -140,7 +140,9 @@ class CommandStart(Command): This filter based on :obj:`Command` filter but can handle only ``/start`` command. """ - def __init__(self, deep_link: typing.Optional[typing.Union[str, re.Pattern]] = None): + def __init__(self, + deep_link: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + encoded: bool = False): """ Also this filter can handle `deep-linking `_ arguments. @@ -151,9 +153,11 @@ class CommandStart(Command): @dp.message_handler(CommandStart(re.compile(r'ref-([\\d]+)'))) :param deep_link: string or compiled regular expression (by ``re.compile(...)``). + :param encoded: set True if you're waiting for encoded payload (default - False). """ super().__init__(['start']) self.deep_link = deep_link + self.encoded = encoded async def check(self, message: types.Message): """ @@ -162,18 +166,21 @@ class CommandStart(Command): :param message: :return: """ + from ...utils.deep_linking import decode_payload check = await super().check(message) if check and self.deep_link is not None: - if not isinstance(self.deep_link, re.Pattern): - return message.get_args() == self.deep_link + payload = decode_payload(message.get_args()) if self.encoded else message.get_args() - match = self.deep_link.match(message.get_args()) + if not isinstance(self.deep_link, typing.Pattern): + return payload == self.deep_link + + match = self.deep_link.match(payload) if match: return {'deep_link': match} return False - return check + return check is not False class CommandHelp(Command): @@ -244,7 +251,7 @@ class Text(Filter): 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, + else e, (equals, contains, endswith, startswith)) self.equals = equals self.contains = contains @@ -370,7 +377,7 @@ class Regexp(Filter): """ def __init__(self, regexp): - if not isinstance(regexp, re.Pattern): + if not isinstance(regexp, typing.Pattern): regexp = re.compile(regexp, flags=re.IGNORECASE | re.MULTILINE) self.regexp = regexp From 41191721f640ca052124e9a0ce09566014874c5a Mon Sep 17 00:00:00 2001 From: Oleg A Date: Fri, 29 Nov 2019 23:25:46 +0300 Subject: [PATCH 053/100] #238 Added CommandStart filter tests --- tests/test_filters.py | 49 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 609db736..ddb3dfc8 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,8 +1,11 @@ import pytest -from aiogram.dispatcher.filters import Text +from aiogram.dispatcher.filters import Text, CommandStart from aiogram.types import Message, CallbackQuery, InlineQuery, Poll +# enable asyncio mode +pytestmark = pytest.mark.asyncio + def data_sample_1(): return [ @@ -22,15 +25,16 @@ def data_sample_1(): ('EXample_string', 'not_example_string'), ] + class TestTextFilter: - async def _run_check(self, check, test_text): + @staticmethod + async def _run_check(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('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): @@ -49,7 +53,6 @@ class TestTextFilter: await self._run_check(check, test_text) - @pytest.mark.asyncio @pytest.mark.parametrize('ignore_case', (True, False)) @pytest.mark.parametrize("test_prefix_list, test_text", [ (['not_example', ''], ''), @@ -83,7 +86,6 @@ class TestTextFilter: await self._run_check(check, test_text) - @pytest.mark.asyncio @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): @@ -102,7 +104,6 @@ class TestTextFilter: await self._run_check(check, test_text) - @pytest.mark.asyncio @pytest.mark.parametrize('ignore_case', (True, False)) @pytest.mark.parametrize("test_postfix_list, test_text", [ (['', 'not_example'], ''), @@ -133,9 +134,9 @@ class TestTextFilter: _test_text = test_text return result is any(map(_test_text.endswith, _test_postfix_list)) + await self._run_check(check, test_text) - @pytest.mark.asyncio @pytest.mark.parametrize('ignore_case', (True, False)) @pytest.mark.parametrize("test_string, test_text", [ ('', ''), @@ -169,7 +170,6 @@ class TestTextFilter: await self._run_check(check, test_text) - @pytest.mark.asyncio @pytest.mark.parametrize('ignore_case', (True, False)) @pytest.mark.parametrize("test_filter_list, test_text", [ (['a', 'ab', 'abc'], 'A'), @@ -193,7 +193,6 @@ class TestTextFilter: await self._run_check(check, test_text) - @pytest.mark.asyncio @pytest.mark.parametrize('ignore_case', (True, False)) @pytest.mark.parametrize("test_filter_text, test_text", [ ('', ''), @@ -222,7 +221,6 @@ class TestTextFilter: await self._run_check(check, test_text) - @pytest.mark.asyncio @pytest.mark.parametrize('ignore_case', (True, False)) @pytest.mark.parametrize("test_filter_list, test_text", [ (['new_string', ''], ''), @@ -261,3 +259,34 @@ class TestTextFilter: await check(CallbackQuery(data=test_text)) await check(InlineQuery(query=test_text)) await check(Poll(question=test_text)) + + +class TestCommandStart: + START = '/start' + GOOD = 'foo' + BAD = 'bar' + ENCODED = 'Zm9v' + + async def test_start_command_without_payload(self): + test_filter = CommandStart() # empty filter + message = Message(text=self.START) + result = await test_filter.check(message) + assert result is True + + async def test_start_command_payload_is_matched(self): + test_filter = CommandStart(deep_link=self.GOOD) + message = Message(text=f'{self.START} {self.GOOD}') + result = await test_filter.check(message) + assert result is True + + async def test_start_command_payload_is_not_matched(self): + test_filter = CommandStart(deep_link=self.GOOD) + message = Message(text=f'{self.START} {self.BAD}') + result = await test_filter.check(message) + assert result is False + + async def test_start_command_payload_is_encoded(self): + test_filter = CommandStart(deep_link=self.GOOD, encoded=True) + message = Message(text=f'{self.START} {self.ENCODED}') + result = await test_filter.check(message) + assert result is True From 52f35058db111224d70c5b9e4fe1380d39959e21 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Fri, 29 Nov 2019 23:27:19 +0300 Subject: [PATCH 054/100] #238 Formatted deep linking docs --- aiogram/utils/deep_linking.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/aiogram/utils/deep_linking.py b/aiogram/utils/deep_linking.py index 2b420cf8..acb105da 100644 --- a/aiogram/utils/deep_linking.py +++ b/aiogram/utils/deep_linking.py @@ -12,14 +12,20 @@ https://core.telegram.org/bots#deep-linking We have add some utils to get deep links more handy. Basic link example: ->>> from aiogram.utils.deep_linking import get_start_link ->>> link = await get_start_link('foo') # result: 'https://t.me/MyBot?start=foo' + + .. code-block:: python + + from aiogram.utils.deep_linking import get_start_link + link = await get_start_link('foo') # result: 'https://t.me/MyBot?start=foo' Encoded link example: ->>> from aiogram.utils.deep_linking import get_start_link, decode_payload ->>> link = await get_start_link('foo', encode=True) # result: 'https://t.me/MyBot?start=Zm9v' ->>> # and decode it back: ->>> payload = decode_payload('Zm9v') # result: 'foo' + + .. code-block:: python + + from aiogram.utils.deep_linking import get_start_link, decode_payload + link = await get_start_link('foo', encode=True) # result: 'https://t.me/MyBot?start=Zm9v' + # and decode it back: + payload = decode_payload('Zm9v') # result: 'foo' """ From 746eead0dac568f9aa2114f4f4a844a07eb6242b Mon Sep 17 00:00:00 2001 From: Oleg A Date: Fri, 29 Nov 2019 23:46:38 +0300 Subject: [PATCH 055/100] #238 Fixed deep_link = None case --- aiogram/dispatcher/filters/builtin.py | 2 +- tests/test_filters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index ebbf4068..7f84c964 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -180,7 +180,7 @@ class CommandStart(Command): return {'deep_link': match} return False - return check is not False + return {'deep_link': None} class CommandHelp(Command): diff --git a/tests/test_filters.py b/tests/test_filters.py index ddb3dfc8..37f14129 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -271,7 +271,7 @@ class TestCommandStart: test_filter = CommandStart() # empty filter message = Message(text=self.START) result = await test_filter.check(message) - assert result is True + assert result is not False async def test_start_command_payload_is_matched(self): test_filter = CommandStart(deep_link=self.GOOD) From 768407eb95d2fca6f6d50e4d301ad7ce131293ba Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 30 Nov 2019 00:16:59 +0300 Subject: [PATCH 056/100] #238 Fixed getting deep_link not by pattern --- 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 7f84c964..f6aeaa14 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -173,7 +173,7 @@ class CommandStart(Command): payload = decode_payload(message.get_args()) if self.encoded else message.get_args() if not isinstance(self.deep_link, typing.Pattern): - return payload == self.deep_link + return False if payload != self.deep_link else {'deep_link': payload} match = self.deep_link.match(payload) if match: From c23c7a2025f6cbfe71a627d7151ab603d8915120 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 30 Nov 2019 00:19:08 +0300 Subject: [PATCH 057/100] #238 Improved deep_link test cases --- tests/test_filters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 37f14129..38d4cc3f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -271,13 +271,13 @@ class TestCommandStart: test_filter = CommandStart() # empty filter message = Message(text=self.START) result = await test_filter.check(message) - assert result is not False + assert result == {'deep_link': None} async def test_start_command_payload_is_matched(self): test_filter = CommandStart(deep_link=self.GOOD) message = Message(text=f'{self.START} {self.GOOD}') result = await test_filter.check(message) - assert result is True + assert result == {'deep_link': self.GOOD} async def test_start_command_payload_is_not_matched(self): test_filter = CommandStart(deep_link=self.GOOD) @@ -289,4 +289,4 @@ class TestCommandStart: test_filter = CommandStart(deep_link=self.GOOD, encoded=True) message = Message(text=f'{self.START} {self.ENCODED}') result = await test_filter.check(message) - assert result is True + assert result == {'deep_link': self.GOOD} From 5489e4cc18f32df47ea9af50b775ac2144d76214 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 30 Nov 2019 00:21:38 +0300 Subject: [PATCH 058/100] #238 Added /start Pattern test cases --- tests/test_filters.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index 38d4cc3f..0592f31b 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,3 +1,6 @@ +import re +from typing import Match + import pytest from aiogram.dispatcher.filters import Text, CommandStart @@ -265,6 +268,8 @@ class TestCommandStart: START = '/start' GOOD = 'foo' BAD = 'bar' + GOOD_PATTERN = re.compile(r'^f..$') + BAD_PATTERN = re.compile(r'^b..$') ENCODED = 'Zm9v' async def test_start_command_without_payload(self): @@ -285,6 +290,20 @@ class TestCommandStart: result = await test_filter.check(message) assert result is False + async def test_start_command_payload_pattern_is_matched(self): + test_filter = CommandStart(deep_link=self.GOOD_PATTERN) + message = Message(text=f'{self.START} {self.GOOD}') + result = await test_filter.check(message) + assert isinstance(result, dict) + match = result.get('deep_link') + assert isinstance(match, Match) + + async def test_start_command_payload_pattern_is_not_matched(self): + test_filter = CommandStart(deep_link=self.BAD_PATTERN) + message = Message(text=f'{self.START} {self.GOOD}') + result = await test_filter.check(message) + assert result is False + async def test_start_command_payload_is_encoded(self): test_filter = CommandStart(deep_link=self.GOOD, encoded=True) message = Message(text=f'{self.START} {self.ENCODED}') From 383f1078a56d9d2be81a985f65adbc010799393d Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 30 Nov 2019 11:35:25 +0300 Subject: [PATCH 059/100] Fixed opencollective organization link --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 02a9374f..fab7284d 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,13 @@ Become a financial contributor and help us sustain our community. [[Contribute]( 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 e2f428ea469b0c09d60817c630ea44042452714d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 4 Dec 2019 18:11:03 +0200 Subject: [PATCH 060/100] Update FUNDING.yml --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 82ea7257..c4430ef6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1 @@ -github: [JRootJunior] open_collective: aiogram From c0353b802d75dc50d3c1c7599f1b8011256f884d Mon Sep 17 00:00:00 2001 From: Ali Tlisov Date: Sun, 29 Dec 2019 18:49:46 +0300 Subject: [PATCH 061/100] fixed socks proxy usage --- aiogram/bot/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 8cc64e33..71b89959 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -74,9 +74,9 @@ class BaseBot: if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')): from aiohttp_socks import SocksConnector - from aiohttp_socks.helpers import parse_socks_url + from aiohttp_socks.utils import parse_proxy_url - socks_ver, host, port, username, password = parse_socks_url(proxy) + socks_ver, host, port, username, password = parse_proxy_url(proxy) if proxy_auth: if not username: username = proxy_auth.login From 2a2d578175efff1aed7ce486f6e9c87a6b55b3d7 Mon Sep 17 00:00:00 2001 From: Gabben Date: Tue, 31 Dec 2019 22:46:08 +0500 Subject: [PATCH 062/100] Add new type fields from Bot API 4.5 --- aiogram/types/animation.py | 1 + aiogram/types/audio.py | 1 + aiogram/types/chat.py | 1 + aiogram/types/chat_member.py | 1 + aiogram/types/chat_photo.py | 2 ++ aiogram/types/document.py | 1 + aiogram/types/file.py | 1 + aiogram/types/passport_file.py | 2 +- aiogram/types/photo_size.py | 1 + aiogram/types/sticker.py | 1 + aiogram/types/video.py | 1 + aiogram/types/video_note.py | 1 + aiogram/types/voice.py | 1 + 13 files changed, 14 insertions(+), 1 deletion(-) diff --git a/aiogram/types/animation.py b/aiogram/types/animation.py index fd470b38..78f5235a 100644 --- a/aiogram/types/animation.py +++ b/aiogram/types/animation.py @@ -14,6 +14,7 @@ class Animation(base.TelegramObject, mixins.Downloadable): """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() thumb: PhotoSize = fields.Field(base=PhotoSize) file_name: base.String = fields.Field() mime_type: base.String = fields.Field() diff --git a/aiogram/types/audio.py b/aiogram/types/audio.py index 9423d02c..6859668f 100644 --- a/aiogram/types/audio.py +++ b/aiogram/types/audio.py @@ -11,6 +11,7 @@ class Audio(base.TelegramObject, mixins.Downloadable): https://core.telegram.org/bots/api#audio """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() duration: base.Integer = fields.Field() performer: base.String = fields.Field() title: base.String = fields.Field() diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index d56ee0fa..07ea7987 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -30,6 +30,7 @@ class Chat(base.TelegramObject): invite_link: base.String = fields.Field() pinned_message: 'Message' = fields.Field(base='Message') permissions: ChatPermissions = fields.Field(base=ChatPermissions) + slow_mode_delay: base.Integer = fields.Field() 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 7e05a33f..347b2750 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -16,6 +16,7 @@ class ChatMember(base.TelegramObject): """ user: User = fields.Field(base=User) status: base.String = fields.Field() + custom_title: base.String = fields.Field() until_date: datetime.datetime = fields.DateTimeField() can_be_edited: base.Boolean = fields.Field() can_change_info: base.Boolean = fields.Field() diff --git a/aiogram/types/chat_photo.py b/aiogram/types/chat_photo.py index 08775d93..d0282a58 100644 --- a/aiogram/types/chat_photo.py +++ b/aiogram/types/chat_photo.py @@ -12,7 +12,9 @@ class ChatPhoto(base.TelegramObject): https://core.telegram.org/bots/api#chatphoto """ small_file_id: base.String = fields.Field() + small_file_unique_id: base.String = fields.Field() big_file_id: base.String = fields.Field() + big_file_unique_id: base.String = fields.Field() async def download_small(self, destination=None, timeout=30, chunk_size=65536, seek=True, make_dirs=True): """ diff --git a/aiogram/types/document.py b/aiogram/types/document.py index 32d943d8..e15b745d 100644 --- a/aiogram/types/document.py +++ b/aiogram/types/document.py @@ -11,6 +11,7 @@ class Document(base.TelegramObject, mixins.Downloadable): https://core.telegram.org/bots/api#document """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() thumb: PhotoSize = fields.Field(base=PhotoSize) file_name: base.String = fields.Field() mime_type: base.String = fields.Field() diff --git a/aiogram/types/file.py b/aiogram/types/file.py index f3269f29..ae813ac6 100644 --- a/aiogram/types/file.py +++ b/aiogram/types/file.py @@ -17,5 +17,6 @@ class File(base.TelegramObject, mixins.Downloadable): https://core.telegram.org/bots/api#file """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() file_size: base.Integer = fields.Field() file_path: base.String = fields.Field() diff --git a/aiogram/types/passport_file.py b/aiogram/types/passport_file.py index f00e80c7..de59e66b 100644 --- a/aiogram/types/passport_file.py +++ b/aiogram/types/passport_file.py @@ -9,7 +9,7 @@ class PassportFile(base.TelegramObject): https://core.telegram.org/bots/api#passportfile """ - file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() file_size: base.Integer = fields.Field() file_date: base.Integer = fields.Field() diff --git a/aiogram/types/photo_size.py b/aiogram/types/photo_size.py index c7ba59b6..cca95304 100644 --- a/aiogram/types/photo_size.py +++ b/aiogram/types/photo_size.py @@ -10,6 +10,7 @@ class PhotoSize(base.TelegramObject, mixins.Downloadable): https://core.telegram.org/bots/api#photosize """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() width: base.Integer = fields.Field() height: base.Integer = fields.Field() file_size: base.Integer = fields.Field() diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index 8da1e9eb..3319d6d7 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -12,6 +12,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable): https://core.telegram.org/bots/api#sticker """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() width: base.Integer = fields.Field() height: base.Integer = fields.Field() is_animated: base.Boolean = fields.Field() diff --git a/aiogram/types/video.py b/aiogram/types/video.py index bf5187cd..97dbb90f 100644 --- a/aiogram/types/video.py +++ b/aiogram/types/video.py @@ -11,6 +11,7 @@ class Video(base.TelegramObject, mixins.Downloadable): https://core.telegram.org/bots/api#video """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() width: base.Integer = fields.Field() height: base.Integer = fields.Field() duration: base.Integer = fields.Field() diff --git a/aiogram/types/video_note.py b/aiogram/types/video_note.py index 9665b6bc..8702faae 100644 --- a/aiogram/types/video_note.py +++ b/aiogram/types/video_note.py @@ -11,6 +11,7 @@ class VideoNote(base.TelegramObject, mixins.Downloadable): https://core.telegram.org/bots/api#videonote """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() length: base.Integer = fields.Field() duration: base.Integer = fields.Field() thumb: PhotoSize = fields.Field(base=PhotoSize) diff --git a/aiogram/types/voice.py b/aiogram/types/voice.py index 621f2247..fd88e402 100644 --- a/aiogram/types/voice.py +++ b/aiogram/types/voice.py @@ -10,6 +10,7 @@ class Voice(base.TelegramObject, mixins.Downloadable): https://core.telegram.org/bots/api#voice """ file_id: base.String = fields.Field() + file_unique_id: base.String = fields.Field() duration: base.Integer = fields.Field() mime_type: base.String = fields.Field() file_size: base.Integer = fields.Field() From 5ea0aa095d0c7a5961ae321d9c86d7374c490751 Mon Sep 17 00:00:00 2001 From: Gabben Date: Tue, 31 Dec 2019 23:00:37 +0500 Subject: [PATCH 063/100] New method from Bot API 4.5 - setChatAdministratorCustomTitle --- aiogram/bot/api.py | 1 + aiogram/bot/bot.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 9589c3e5..0c2d572f 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -188,6 +188,7 @@ class Methods(Helper): UNBAN_CHAT_MEMBER = Item() # unbanChatMember RESTRICT_CHAT_MEMBER = Item() # restrictChatMember PROMOTE_CHAT_MEMBER = Item() # promoteChatMember + SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE = Item() # setChatAdministratorCustomTitle SET_CHAT_PERMISSIONS = Item() # setChatPermissions EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink SET_CHAT_PHOTO = Item() # setChatPhoto diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 5e8f05d9..54f5afe2 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1118,6 +1118,22 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload) return result + + async def set_chat_administrator_custom_title(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer, custom_title: base.String) -> base.Boolean: + """ + Use this method to set a custom title for an administrator in a supergroup promoted by the bot. + + Returns True on success. + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + :param user_id: Unique identifier of the target user + :param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed + :return: True on success. + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload) + return result async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String], permissions: types.ChatPermissions) -> base.Boolean: From 98d3f789d2c7d00ae359d522f724d177014a3911 Mon Sep 17 00:00:00 2001 From: Gabben Date: Wed, 1 Jan 2020 00:09:22 +0500 Subject: [PATCH 064/100] Add chat shortcast --- aiogram/bot/bot.py | 2 ++ aiogram/types/chat.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 54f5afe2..6f3d8eb8 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1125,6 +1125,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): Returns True on success. + Source: https://core.telegram.org/bots/api#setchatadministratorcustomtitle + :param chat_id: Unique identifier for the target chat or username of the target supergroup :param user_id: Unique identifier of the target user :param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 07ea7987..b12331b0 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -297,6 +297,21 @@ class Chat(base.TelegramObject): can_pin_messages=can_pin_messages, can_promote_members=can_promote_members) + async def set_administrator_custom_title(self, user_id: base.Integer, custom_title: base.String) -> base.Boolean: + """ + Use this method to set a custom title for an administrator in a supergroup promoted by the bot. + + Returns True on success. + + Source: https://core.telegram.org/bots/api#setchatadministratorcustomtitle + + :param chat_id: Unique identifier for the target chat or username of the target supergroup + :param user_id: Unique identifier of the target user + :param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed + :return: True on success. + """ + return await self.bot.set_chat_administrator_custom_title(chat_id=self.id, user_id=user_id, custom_title=custom_title) + async def pin_message(self, message_id: int, disable_notification: bool = False): """ Use this method to pin a message in a supergroup. From efc45ed96c0f440eb262da3da35aa00174e43ea0 Mon Sep 17 00:00:00 2001 From: Gabben Date: Wed, 1 Jan 2020 00:12:26 +0500 Subject: [PATCH 065/100] Update chat.py --- aiogram/types/chat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index b12331b0..bd475f59 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -305,7 +305,6 @@ class Chat(base.TelegramObject): Source: https://core.telegram.org/bots/api#setchatadministratorcustomtitle - :param chat_id: Unique identifier for the target chat or username of the target supergroup :param user_id: Unique identifier of the target user :param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed :return: True on success. From 04ca6d353d76d1ac485047b0df3b679f0a9ed74f Mon Sep 17 00:00:00 2001 From: Gabben Date: Wed, 1 Jan 2020 00:40:31 +0500 Subject: [PATCH 066/100] Replace get_user_profile_photos with get_profile_photos --- aiogram/types/user.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 27ee27e0..211c8588 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -6,7 +6,7 @@ import babel from . import base from . import fields -from ..utils import markdown +from ..utils import markdown, deprecated class User(base.TelegramObject): @@ -73,9 +73,13 @@ class User(base.TelegramObject): return markdown.hlink(name, self.url) return markdown.link(name, self.url) + @deprecated('`get_user_profile_photos` is outdated, please use `get_profile_photos`', stacklevel=3) async def get_user_profile_photos(self, offset=None, limit=None): return await self.bot.get_user_profile_photos(self.id, offset, limit) + async def get_profile_photos(self, offset=None, limit=None): + return await self.bot.get_user_profile_photos(self.id, offset, limit) + def __hash__(self): return self.id From 2ff504ad41688a87dbeffecbe858246e86060ce3 Mon Sep 17 00:00:00 2001 From: Gabben Date: Wed, 1 Jan 2020 00:42:49 +0500 Subject: [PATCH 067/100] Add new shortcuts --- aiogram/types/chat.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index d56ee0fa..45f5fbcf 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -296,6 +296,19 @@ class Chat(base.TelegramObject): can_pin_messages=can_pin_messages, can_promote_members=can_promote_members) + async def set_permissions(self, permissions: 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 permissions: New default chat permissions + :return: True on success. + """ + return await self.bot.set_chat_permissions(self.id, permissions=permissions) + async def pin_message(self, message_id: int, disable_notification: bool = False): """ Use this method to pin a message in a supergroup. @@ -374,6 +387,23 @@ class Chat(base.TelegramObject): """ return await self.bot.get_chat_member(self.id, user_id) + async def set_sticker_set(self, sticker_set_name: base.String) -> base.Boolean: + """ + Use this method to set a new group sticker set for a supergroup. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Use the field can_set_sticker_set optionally returned in getChat requests to check + if the bot can use this method. + + Source: https://core.telegram.org/bots/api#setchatstickerset + + :param sticker_set_name: Name of the sticker set to be set as the group sticker set + :type sticker_set_name: :obj:`base.String` + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + return await self.bot.set_chat_sticker_set(self.id, sticker_set_name=sticker_set_name) + async def do(self, action): """ Use this method when you need to tell the user that something is happening on the bot's side. From b01095e61c2f129a8696015b26dd918d5a87bf6d Mon Sep 17 00:00:00 2001 From: Gabben Date: Wed, 1 Jan 2020 00:45:43 +0500 Subject: [PATCH 068/100] Add new shortcut --- aiogram/types/chat.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 45f5fbcf..e2ea0e9f 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -404,6 +404,21 @@ class Chat(base.TelegramObject): """ return await self.bot.set_chat_sticker_set(self.id, sticker_set_name=sticker_set_name) + async def delete_sticker_set(self) -> base.Boolean: + """ + Use this method to delete a group sticker set from a supergroup. + The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. + + Use the field can_set_sticker_set optionally returned in getChat requests + to check if the bot can use this method. + + Source: https://core.telegram.org/bots/api#deletechatstickerset + + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + return self.bot.delete_chat_sticker_set(self.id) + async def do(self, action): """ Use this method when you need to tell the user that something is happening on the bot's side. From 4cd59971db24a7b1a1a0f5ff94c9033f71e55db4 Mon Sep 17 00:00:00 2001 From: Gabben Date: Wed, 1 Jan 2020 00:46:27 +0500 Subject: [PATCH 069/100] Type hints --- aiogram/types/chat.py | 48 +++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index e2ea0e9f..900bb5a8 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -4,12 +4,12 @@ import asyncio import datetime import typing -from . import base -from . import fields +from ..utils import helper, markdown +from . import base, fields +from .chat_member import ChatMember from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto -from ..utils import helper -from ..utils import markdown +from .input_file import InputFile class Chat(base.TelegramObject): @@ -37,7 +37,7 @@ class Chat(base.TelegramObject): return self.id @property - def full_name(self): + def full_name(self) -> base.String: if self.type == ChatType.PRIVATE: full_name = self.first_name if self.last_name: @@ -46,7 +46,7 @@ class Chat(base.TelegramObject): return self.title @property - def mention(self): + def mention(self) -> typing.Union[base.String, None]: """ Get mention if a Chat has a username, or get full name if this is a Private Chat, otherwise None is returned """ @@ -57,20 +57,20 @@ class Chat(base.TelegramObject): return None @property - def user_url(self): + def user_url(self) -> base.String: if self.type != ChatType.PRIVATE: raise TypeError('`user_url` property is only available in private chats!') return f"tg://user?id={self.id}" - def get_mention(self, name=None, as_html=True): + def get_mention(self, name=None, as_html=True) -> base.String: if name is None: name = self.mention if as_html: return markdown.hlink(name, self.user_url) return markdown.link(name, self.user_url) - async def get_url(self): + async def get_url(self) -> base.String: """ Use this method to get chat link. Private chat returns user link. @@ -101,7 +101,7 @@ class Chat(base.TelegramObject): for key, value in other: self[key] = value - async def set_photo(self, photo): + async def set_photo(self, photo: InputFile) -> base.Boolean: """ Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -118,7 +118,7 @@ class Chat(base.TelegramObject): """ return await self.bot.set_chat_photo(self.id, photo) - async def delete_photo(self): + async def delete_photo(self) -> base.Boolean: """ Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -133,7 +133,7 @@ class Chat(base.TelegramObject): """ return await self.bot.delete_chat_photo(self.id) - async def set_title(self, title): + async def set_title(self, title: base.String) -> base.Boolean: """ Use this method to change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -150,7 +150,7 @@ class Chat(base.TelegramObject): """ return await self.bot.set_chat_title(self.id, title) - async def set_description(self, description): + async def set_description(self, description: base.String) -> base.Boolean: """ Use this method to change the description of a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -165,7 +165,7 @@ class Chat(base.TelegramObject): return await self.bot.delete_chat_description(self.id, description) async def kick(self, user_id: base.Integer, - until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None): + until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None) -> base.Boolean: """ Use this method to kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group @@ -188,7 +188,7 @@ class Chat(base.TelegramObject): """ return await self.bot.kick_chat_member(self.id, user_id=user_id, until_date=until_date) - async def unban(self, user_id: base.Integer): + async def unban(self, user_id: base.Integer) -> base.Boolean: """ Use this method to unban a previously kicked user in a supergroup or channel. ` The user will not return to the group or channel automatically, but will be able to join via link, etc. @@ -309,7 +309,7 @@ class Chat(base.TelegramObject): """ return await self.bot.set_chat_permissions(self.id, permissions=permissions) - async def pin_message(self, message_id: int, disable_notification: bool = False): + async def pin_message(self, message_id: base.Integer, disable_notification: base.Boolean = False) -> base.Boolean: """ Use this method to pin a message in a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -326,7 +326,7 @@ class Chat(base.TelegramObject): """ return await self.bot.pin_chat_message(self.id, message_id, disable_notification) - async def unpin_message(self): + async def unpin_message(self) -> base.Boolean: """ Use this method to unpin a message in a supergroup chat. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. @@ -338,7 +338,7 @@ class Chat(base.TelegramObject): """ return await self.bot.unpin_chat_message(self.id) - async def leave(self): + async def leave(self) -> base.Boolean: """ Use this method for your bot to leave a group, supergroup or channel. @@ -349,7 +349,7 @@ class Chat(base.TelegramObject): """ return await self.bot.leave_chat(self.id) - async def get_administrators(self): + async def get_administrators(self) -> typing.List[ChatMember]: """ Use this method to get a list of administrators in a chat. @@ -363,7 +363,7 @@ class Chat(base.TelegramObject): """ return await self.bot.get_chat_administrators(self.id) - async def get_members_count(self): + async def get_members_count(self) -> base.Integer: """ Use this method to get the number of members in a chat. @@ -374,7 +374,7 @@ class Chat(base.TelegramObject): """ return await self.bot.get_chat_members_count(self.id) - async def get_member(self, user_id): + async def get_member(self, user_id: base.Integer) -> ChatMember: """ Use this method to get information about a member of a chat. @@ -417,9 +417,9 @@ class Chat(base.TelegramObject): :return: Returns True on success :rtype: :obj:`base.Boolean` """ - return self.bot.delete_chat_sticker_set(self.id) + return await self.bot.delete_chat_sticker_set(self.id) - async def do(self, action): + async def do(self, action: base.String) -> base.Boolean: """ Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less @@ -437,7 +437,7 @@ class Chat(base.TelegramObject): """ return await self.bot.send_chat_action(self.id, action) - async def export_invite_link(self): + async def export_invite_link(self) -> base.String: """ Use this method to export an invite link to a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. From 655554862d81c8289c5c1e21bd801a4f1cb44f0d Mon Sep 17 00:00:00 2001 From: Gabben Date: Wed, 1 Jan 2020 00:53:41 +0500 Subject: [PATCH 070/100] New Sticker shortcuts - New delete_from_set shortcut - Fix docs --- aiogram/bot/bot.py | 2 -- aiogram/types/sticker.py | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 5e8f05d9..b7b4fb43 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1825,8 +1825,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ Use this method to delete a sticker from a set created by the bot. - The following methods and objects allow your bot to work in inline mode. - Source: https://core.telegram.org/bots/api#deletestickerfromset :param sticker: File identifier of the sticker diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index 8da1e9eb..cf99ddcc 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -20,3 +20,29 @@ class Sticker(base.TelegramObject, mixins.Downloadable): set_name: base.String = fields.Field() mask_position: MaskPosition = fields.Field(base=MaskPosition) file_size: base.Integer = fields.Field() + + async def set_position_in_set(self, position: base.Integer) -> base.Boolean: + """ + Use this method to move a sticker in a set created by the bot to a specific position. + + Source: https://core.telegram.org/bots/api#setstickerpositioninset + + :param position: New sticker position in the set, zero-based + :type position: :obj:`base.Integer` + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + return await self.bot.set_sticker_position_in_set(self.file_id, position=position) + + async def delete_from_set(self) -> base.Boolean: + """ + Use this method to delete a sticker from a set created by the bot. + + Source: https://core.telegram.org/bots/api#deletestickerfromset + + :param sticker: File identifier of the sticker + :type sticker: :obj:`base.String` + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + return await self.bot.delete_sticker_from_set(self.file_id) From 4df615446d55ee22044aad571a89f8e0cd58c243 Mon Sep 17 00:00:00 2001 From: Gabben Date: Wed, 1 Jan 2020 00:56:55 +0500 Subject: [PATCH 071/100] Try to resolve conflict --- aiogram/types/chat.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 900bb5a8..81e0ac1f 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -309,6 +309,20 @@ class Chat(base.TelegramObject): """ return await self.bot.set_chat_permissions(self.id, permissions=permissions) + async def set_administrator_custom_title(self, user_id: base.Integer, custom_title: base.String) -> base.Boolean: + """ + Use this method to set a custom title for an administrator in a supergroup promoted by the bot. + + Returns True on success. + + Source: https://core.telegram.org/bots/api#setchatadministratorcustomtitle + + :param user_id: Unique identifier of the target user + :param custom_title: New custom title for the administrator; 0-16 characters, emoji are not allowed + :return: True on success. + """ + return await self.bot.set_chat_administrator_custom_title(chat_id=self.id, user_id=user_id, custom_title=custom_title) + async def pin_message(self, message_id: base.Integer, disable_notification: base.Boolean = False) -> base.Boolean: """ Use this method to pin a message in a supergroup. From 4933261d6516e365f34b83f57b49d52f1999b516 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 16:05:09 +0200 Subject: [PATCH 072/100] Fix deprecation warning in user module --- aiogram/types/user.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 211c8588..2bcdd032 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -6,7 +6,8 @@ import babel from . import base from . import fields -from ..utils import markdown, deprecated +from ..utils import markdown +from ..utils.deprecated import deprecated class User(base.TelegramObject): @@ -73,7 +74,10 @@ class User(base.TelegramObject): return markdown.hlink(name, self.url) return markdown.link(name, self.url) - @deprecated('`get_user_profile_photos` is outdated, please use `get_profile_photos`', stacklevel=3) + @deprecated( + '`get_user_profile_photos` is outdated, please use `get_profile_photos`', + stacklevel=3 + ) async def get_user_profile_photos(self, offset=None, limit=None): return await self.bot.get_user_profile_photos(self.id, offset, limit) From 18d1115b50af85e81a32d6507e03b9796d828e22 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 16:05:45 +0200 Subject: [PATCH 073/100] Replace .reply(reply=False) to .answer() in examples --- examples/echo_bot.py | 2 +- examples/id_filter_example.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/echo_bot.py b/examples/echo_bot.py index 00046f3a..0055d155 100644 --- a/examples/echo_bot.py +++ b/examples/echo_bot.py @@ -46,7 +46,7 @@ async def echo(message: types.Message): # old style: # await bot.send_message(message.chat.id, message.text) - await message.reply(message.text, reply=False) + await message.answer(message.text) if __name__ == '__main__': diff --git a/examples/id_filter_example.py b/examples/id_filter_example.py index 343253e3..bb9bba6a 100644 --- a/examples/id_filter_example.py +++ b/examples/id_filter_example.py @@ -24,12 +24,12 @@ async def handler2(msg: types.Message): @dp.message_handler(user_id=user_id_required, chat_id=chat_id_required) async def handler3(msg: types.Message): - await msg.reply("Hello from user= & chat_id=", reply=False) + await msg.answer("Hello from user= & chat_id=") @dp.message_handler(user_id=[user_id_required, 42]) # TODO: You can add any number of ids here async def handler4(msg: types.Message): - await msg.reply("Checked user_id with list!", reply=False) + await msg.answer("Checked user_id with list!") if __name__ == '__main__': From ce026dfa71273708852ad2863433331d455bd545 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 16:05:59 +0200 Subject: [PATCH 074/100] Add exception MethodIsNotAvailable --- aiogram/utils/exceptions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index f77fe257..bec48d97 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -65,6 +65,7 @@ - UnsupportedUrlProtocol - CantParseEntities - ResultIdDuplicate + - MethodIsNotAvailable - ConflictError - TerminatedByOtherGetUpdates - CantGetUpdates @@ -461,6 +462,10 @@ class BotDomainInvalid(BadRequest): text = 'Invalid bot domain' +class MethodIsNotAvailable(BadRequest): + match = "Method is available only for supergroups" + + class NotFound(TelegramAPIError, _MatchErrorMixin): __group = True From 9115a44be687e627a2cbac7a6ffe5cbabef9a547 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 16:39:31 +0200 Subject: [PATCH 075/100] Backport of text decoration utils from 3.0 --- aiogram/bot/base.py | 5 + aiogram/types/message.py | 35 +------ aiogram/types/message_entity.py | 10 +- aiogram/utils/markdown.py | 158 ++++++++++++++++-------------- aiogram/utils/text_decorations.py | 143 +++++++++++++++++++++++++++ 5 files changed, 242 insertions(+), 109 deletions(-) create mode 100644 aiogram/utils/text_decorations.py diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 8cc64e33..0b7468be 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -3,6 +3,7 @@ import contextlib import io import ssl import typing +import warnings from contextvars import ContextVar from typing import Dict, List, Optional, Union @@ -269,6 +270,10 @@ class BaseBot: if value not in ParseMode.all(): raise ValueError(f"Parse mode must be one of {ParseMode.all()}") setattr(self, '_parse_mode', value) + if value == 'markdown': + warnings.warn("Parse mode `Markdown` is legacy since Telegram Bot API 4.5, " + "retained for backward compatibility. Use `MarkdownV2` instead.\n" + "https://core.telegram.org/bots/api#markdown-style", stacklevel=3) @parse_mode.deleter def parse_mode(self): diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 8fdece2b..dbe35738 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -2,7 +2,6 @@ from __future__ import annotations import datetime import functools -import sys import typing from . import base @@ -32,6 +31,7 @@ from .video_note import VideoNote from .voice import Voice from ..utils import helper from ..utils import markdown as md +from ..utils.text_decorations import html_decoration, markdown_decoration class Message(base.TelegramObject): @@ -200,38 +200,10 @@ class Message(base.TelegramObject): if text is None: raise TypeError("This message doesn't have any text.") - quote_fn = md.quote_html if as_html else md.escape_md - entities = self.entities or self.caption_entities - if not entities: - return quote_fn(text) + text_decorator = html_decoration if as_html else markdown_decoration - if not sys.maxunicode == 0xffff: - text = text.encode('utf-16-le') - - result = '' - offset = 0 - - for entity in sorted(entities, key=lambda item: item.offset): - entity_text = entity.parse(text, as_html=as_html) - - if sys.maxunicode == 0xffff: - part = text[offset:entity.offset] - result += quote_fn(part) + entity_text - else: - part = text[offset * 2:entity.offset * 2] - result += quote_fn(part.decode('utf-16-le')) + entity_text - - offset = entity.offset + entity.length - - if sys.maxunicode == 0xffff: - part = text[offset:] - result += quote_fn(part) - else: - part = text[offset * 2:] - result += quote_fn(part.decode('utf-16-le')) - - return result + return text_decorator.unparse(text, entities) @property def md_text(self) -> str: @@ -1798,4 +1770,5 @@ class ParseMode(helper.Helper): mode = helper.HelperMode.lowercase MARKDOWN = helper.Item() + MARKDOWN_V2 = helper.Item() HTML = helper.Item() diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index f0ad75d6..98191e43 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -4,6 +4,7 @@ from . import base from . import fields from .user import User from ..utils import helper, markdown +from ..utils.deprecated import deprecated class MessageEntity(base.TelegramObject): @@ -36,6 +37,7 @@ class MessageEntity(base.TelegramObject): entity_text = entity_text[self.offset * 2:(self.offset + self.length) * 2] return entity_text.decode('utf-16-le') + @deprecated("This method doesn't work with nested entities and will be removed in aiogram 3.0") def parse(self, text, as_html=True): """ Get entity value with markup @@ -87,6 +89,8 @@ class MessageEntityType(helper.Helper): :key: ITALIC :key: CODE :key: PRE + :key: UNDERLINE + :key: STRIKETHROUGH :key: TEXT_LINK :key: TEXT_MENTION """ @@ -101,7 +105,9 @@ class MessageEntityType(helper.Helper): PHONE_NUMBER = helper.Item() # phone_number BOLD = helper.Item() # bold - bold text ITALIC = helper.Item() # italic - italic text - CODE = helper.Item() # code - monowidth string - PRE = helper.Item() # pre - monowidth block + CODE = helper.Item() # code - monowidth string + PRE = helper.Item() # pre - monowidth block + UNDERLINE = helper.Item() # underline + STRIKETHROUGH = helper.Item() # strikethrough TEXT_LINK = helper.Item() # text_link - for clickable text URLs TEXT_MENTION = helper.Item() # text_mention - for users without usernames diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index 89a23d94..7b217b4f 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -1,59 +1,28 @@ -LIST_MD_SYMBOLS = '*_`[' +from .text_decorations import html_decoration, markdown_decoration + +LIST_MD_SYMBOLS = "*_`[" MD_SYMBOLS = ( (LIST_MD_SYMBOLS[0], LIST_MD_SYMBOLS[0]), (LIST_MD_SYMBOLS[1], LIST_MD_SYMBOLS[1]), (LIST_MD_SYMBOLS[2], LIST_MD_SYMBOLS[2]), - (LIST_MD_SYMBOLS[2] * 3 + '\n', '\n' + LIST_MD_SYMBOLS[2] * 3), - ('', ''), - ('', ''), - ('', ''), - ('
', '
'), + (LIST_MD_SYMBOLS[2] * 3 + "\n", "\n" + LIST_MD_SYMBOLS[2] * 3), + ("", ""), + ("", ""), + ("", ""), + ("
", "
"), ) -HTML_QUOTES_MAP = { - '<': '<', - '>': '>', - '&': '&', - '"': '"' -} +HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """} _HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS -def _join(*content, sep=' '): +def _join(*content, sep=" "): return sep.join(map(str, content)) -def _escape(s, symbols=LIST_MD_SYMBOLS): - for symbol in symbols: - s = s.replace(symbol, '\\' + symbol) - return s - - -def _md(string, symbols=('', '')): - start, end = symbols - return start + string + end - - -def quote_html(content): - """ - Quote HTML symbols - - All <, >, & and " symbols that are not a part of a tag or - an HTML entity must be replaced with the corresponding HTML entities - (< with < > with > & with & and " with "). - - :param content: str - :return: str - """ - new_content = '' - for symbol in content: - new_content += HTML_QUOTES_MAP[symbol] if symbol in _HQS else symbol - return new_content - - -def text(*content, sep=' '): +def text(*content, sep=" "): """ Join all elements with a separator @@ -64,7 +33,7 @@ def text(*content, sep=' '): return _join(*content, sep=sep) -def bold(*content, sep=' '): +def bold(*content, sep=" "): """ Make bold text (Markdown) @@ -72,10 +41,10 @@ def bold(*content, sep=' '): :param sep: :return: """ - return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[0]) + return markdown_decoration.bold.format(value=html_decoration.quote(_join(*content, sep=sep))) -def hbold(*content, sep=' '): +def hbold(*content, sep=" "): """ Make bold text (HTML) @@ -83,10 +52,10 @@ def hbold(*content, sep=' '): :param sep: :return: """ - return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[4]) + return html_decoration.bold.format(value=html_decoration.quote(_join(*content, sep=sep))) -def italic(*content, sep=' '): +def italic(*content, sep=" "): """ Make italic text (Markdown) @@ -94,10 +63,10 @@ def italic(*content, sep=' '): :param sep: :return: """ - return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[1]) + return markdown_decoration.italic.format(value=html_decoration.quote(_join(*content, sep=sep))) -def hitalic(*content, sep=' '): +def hitalic(*content, sep=" "): """ Make italic text (HTML) @@ -105,10 +74,10 @@ def hitalic(*content, sep=' '): :param sep: :return: """ - return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[5]) + return html_decoration.italic.format(value=html_decoration.quote(_join(*content, sep=sep))) -def code(*content, sep=' '): +def code(*content, sep=" "): """ Make mono-width text (Markdown) @@ -116,10 +85,10 @@ def code(*content, sep=' '): :param sep: :return: """ - return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[2]) + return markdown_decoration.code.format(value=html_decoration.quote(_join(*content, sep=sep))) -def hcode(*content, sep=' '): +def hcode(*content, sep=" "): """ Make mono-width text (HTML) @@ -127,10 +96,10 @@ def hcode(*content, sep=' '): :param sep: :return: """ - return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[6]) + return html_decoration.code.format(value=html_decoration.quote(_join(*content, sep=sep))) -def pre(*content, sep='\n'): +def pre(*content, sep="\n"): """ Make mono-width text block (Markdown) @@ -138,10 +107,10 @@ def pre(*content, sep='\n'): :param sep: :return: """ - return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[3]) + return markdown_decoration.pre.format(value=html_decoration.quote(_join(*content, sep=sep))) -def hpre(*content, sep='\n'): +def hpre(*content, sep="\n"): """ Make mono-width text block (HTML) @@ -149,10 +118,60 @@ def hpre(*content, sep='\n'): :param sep: :return: """ - return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[7]) + return html_decoration.pre.format(value=html_decoration.quote(_join(*content, sep=sep))) -def link(title, url): +def underline(*content, sep=" "): + """ + Make underlined text (Markdown) + + :param content: + :param sep: + :return: + """ + return markdown_decoration.underline.format( + value=markdown_decoration.quote(_join(*content, sep=sep)) + ) + + +def hunderline(*content, sep=" "): + """ + Make underlined text (HTML) + + :param content: + :param sep: + :return: + """ + return html_decoration.underline.format(value=html_decoration.quote(_join(*content, sep=sep))) + + +def strikethrough(*content, sep=" "): + """ + Make strikethrough text (Markdown) + + :param content: + :param sep: + :return: + """ + return markdown_decoration.strikethrough.format( + value=markdown_decoration.quote(_join(*content, sep=sep)) + ) + + +def hstrikethrough(*content, sep=" "): + """ + Make strikethrough text (HTML) + + :param content: + :param sep: + :return: + """ + return html_decoration.strikethrough.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) + + +def link(title: str, url: str) -> str: """ Format URL (Markdown) @@ -160,10 +179,10 @@ def link(title, url): :param url: :return: """ - return "[{0}]({1})".format(title, url) + return markdown_decoration.link.format(value=html_decoration.quote(title), link=url) -def hlink(title, url): +def hlink(title: str, url: str) -> str: """ Format URL (HTML) @@ -171,23 +190,10 @@ def hlink(title, url): :param url: :return: """ - return '{1}'.format(url, quote_html(title)) + return html_decoration.link.format(value=html_decoration.quote(title), link=url) -def escape_md(*content, sep=' '): - """ - Escape markdown text - - E.g. for usernames - - :param content: - :param sep: - :return: - """ - return _escape(_join(*content, sep=sep)) - - -def hide_link(url): +def hide_link(url: str) -> str: """ Hide URL (HTML only) Can be used for adding an image to a text message diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py new file mode 100644 index 00000000..5b2cf51c --- /dev/null +++ b/aiogram/utils/text_decorations.py @@ -0,0 +1,143 @@ +from __future__ import annotations +import html +import re +import struct +from dataclasses import dataclass +from typing import TYPE_CHECKING, AnyStr, Callable, Generator, Iterable, List, Optional + +if TYPE_CHECKING: + from aiogram.types import MessageEntity + +__all__ = ( + "TextDecoration", + "html_decoration", + "markdown_decoration", + "add_surrogate", + "remove_surrogate", +) + + +@dataclass +class TextDecoration: + link: str + bold: str + italic: str + code: str + pre: str + underline: str + strikethrough: str + quote: Callable[[AnyStr], AnyStr] + + def apply_entity(self, entity: MessageEntity, text: str) -> str: + """ + Apply single entity to text + + :param entity: + :param text: + :return: + """ + if entity.type in ( + "bold", + "italic", + "code", + "pre", + "underline", + "strikethrough", + ): + return getattr(self, entity.type).format(value=text) + elif entity.type == "text_mention": + return self.link.format(value=text, link=f"tg://user?id={entity.user.id}") + elif entity.type == "text_link": + return self.link.format(value=text, link=entity.url) + elif entity.type == "url": + return text + return self.quote(text) + + def unparse(self, text, entities: Optional[List[MessageEntity]] = None) -> str: + """ + Unparse message entities + + :param text: raw text + :param entities: Array of MessageEntities + :return: + """ + text = add_surrogate(text) + result = "".join( + self._unparse_entities( + text, sorted(entities, key=lambda item: item.offset) if entities else [] + ) + ) + return remove_surrogate(result) + + def _unparse_entities( + self, + text: str, + entities: Iterable[MessageEntity], + offset: Optional[int] = None, + length: Optional[int] = None, + ) -> Generator[str, None, None]: + offset = offset or 0 + length = length or len(text) + + for index, entity in enumerate(entities): + if entity.offset < offset: + continue + if entity.offset > offset: + yield self.quote(text[offset : entity.offset]) + start = entity.offset + offset = entity.offset + entity.length + + sub_entities = list( + filter(lambda e: e.offset < offset, entities[index + 1 :]) + ) + yield self.apply_entity( + entity, + "".join( + self._unparse_entities( + text, sub_entities, offset=start, length=offset + ) + ), + ) + + if offset < length: + yield self.quote(text[offset:length]) + + +html_decoration = TextDecoration( + link='{value}', + bold="{value}", + italic="{value}", + code="{value}", + pre="
{value}
", + underline="{value}", + strikethrough="{value}", + quote=html.escape, +) + +MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-|{}.!])") + +markdown_decoration = TextDecoration( + link="[{value}]({link})", + bold="*{value}*", + italic="_{value}_\r", + code="`{value}`", + pre="```{value}```", + underline="__{value}__", + strikethrough="~{value}~", + quote=lambda text: re.sub( + pattern=MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=text + ), +) + + +def add_surrogate(text: str) -> str: + return "".join( + "".join(chr(d) for d in struct.unpack(" str: + return text.encode("utf-16", "surrogatepass").decode("utf-16") From 856c938871ac6ebd966609e0247b8fba540d4185 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 16:42:58 +0200 Subject: [PATCH 076/100] Bump dev requirements (aiohttp-socks>=0.3.3) --- dev_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 06bc3e9c..be2c8f7d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -13,6 +13,6 @@ wheel>=0.31.1 sphinx>=2.0.1 sphinx-rtd-theme>=0.4.3 sphinxcontrib-programoutput>=0.14 -aiohttp-socks>=0.2.2 +aiohttp-socks>=0.3.3 rethinkdb>=2.4.1 coverage==4.5.3 From 23325e09e35685b0901e0877c20a9b498a05f808 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 16:44:49 +0200 Subject: [PATCH 077/100] #236 remove bad link --- examples/i18n_example.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/i18n_example.py b/examples/i18n_example.py index 3bb624bd..2d65655a 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -62,8 +62,7 @@ async def cmd_start(message: types.Message): @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 + # For setting custom lang you have to modify i18n middleware await message.reply(_('Your current language: {language}').format(language=locale)) # If you care about pluralization, here's small handler From 84de4e95f6576657338a92bd9ced82d44036538a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 16:47:21 +0200 Subject: [PATCH 078/100] #237 Prevent syntax warning --- aiogram/contrib/middlewares/i18n.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 67ab8cca..63f54510 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -95,7 +95,7 @@ class I18nMiddleware(BaseMiddleware): locale = self.ctx_locale.get() if locale not in self.locales: - if n is 1: + if n == 1: return singular return plural From 2323771cb99d2eb552de3eba791ad27072be08da Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 16:55:33 +0200 Subject: [PATCH 079/100] Bump versions and URL's --- README.md | 8 ++++---- README.rst | 8 ++++---- aiogram/__init__.py | 2 +- aiogram/bot/api.py | 2 +- docs/source/index.rst | 6 +++--- docs/source/migration_1_to_2.rst | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fab7284d..04a95017 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ [![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.4-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) -[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://aiogram.readthedocs.io/en/latest/?badge=latest) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.5-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) **aiogram** is a pretty simple and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler. -You can [read the docs here](http://aiogram.readthedocs.io/en/latest/). +You can [read the docs here](http://docs.aiogram.dev/en/latest/). ## Official aiogram resources: @@ -21,7 +21,7 @@ You can [read the docs here](http://aiogram.readthedocs.io/en/latest/). - Community: [@aiogram](https://t.me/aiogram) - Russian community: [@aiogram_ru](https://t.me/aiogram_ru) - Pip: [aiogram](https://pypi.python.org/pypi/aiogram) - - Docs: [ReadTheDocs](http://aiogram.readthedocs.io) + - Docs: [ReadTheDocs](http://docs.aiogram.dev) - 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) diff --git a/README.rst b/README.rst index f7c3e951..da323e03 100644 --- a/README.rst +++ b/README.rst @@ -21,12 +21,12 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.4-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.5-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API .. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square - :target: http://aiogram.readthedocs.io/en/latest/?badge=latest + :target: http://docs.aiogram.dev/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square @@ -40,7 +40,7 @@ AIOGramBot **aiogram** is a pretty simple and fully asynchronous framework for `Telegram Bot API `_ written in Python 3.7 with `asyncio `_ and `aiohttp `_. It helps you to make your bots faster and simpler. -You can `read the docs here `_. +You can `read the docs here `_. Official aiogram resources -------------------------- @@ -49,7 +49,7 @@ Official aiogram resources - Community: `@aiogram `_ - Russian community: `@aiogram_ru `_ - Pip: `aiogram `_ -- Docs: `ReadTheDocs `_ +- Docs: `ReadTheDocs `_ - Source: `Github repo `_ - Issues/Bug tracker: `Github issues tracker `_ - Test bot: `@aiogram_bot `_ diff --git a/aiogram/__init__.py b/aiogram/__init__.py index b81ceedb..b55283fe 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -39,4 +39,4 @@ __all__ = [ ] __version__ = '2.3.dev1' -__api_version__ = '4.4' +__api_version__ = '4.5' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 0c2d572f..5c6ce74d 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -153,7 +153,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 4.4 + List is updated to Bot API 4.5 """ mode = HelperMode.lowerCamelCase diff --git a/docs/source/index.rst b/docs/source/index.rst index 7b6fd231..e81deb7f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,12 +22,12 @@ 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.4-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.5-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API .. image:: https://img.shields.io/readthedocs/aiogram?style=flat-square - :target: http://aiogram.readthedocs.io/en/latest/?badge=latest + :target: http://docs.aiogram.dev/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square @@ -48,7 +48,7 @@ Official aiogram resources - Community: `@aiogram `_ - Russian community: `@aiogram_ru `_ - Pip: `aiogram `_ -- Docs: `ReadTheDocs `_ +- Docs: `ReadTheDocs `_ - Source: `Github repo `_ - Issues/Bug tracker: `Github issues tracker `_ - Test bot: `@aiogram_bot `_ diff --git a/docs/source/migration_1_to_2.rst b/docs/source/migration_1_to_2.rst index 3d98d86e..7016063a 100644 --- a/docs/source/migration_1_to_2.rst +++ b/docs/source/migration_1_to_2.rst @@ -195,7 +195,7 @@ Example: .. code-block:: python - URL = 'https://aiogram.readthedocs.io/en/dev-2.x/_static/logo.png' + URL = 'https://docs.aiogram.dev/en/dev-2.x/_static/logo.png' @dp.message_handler(commands=['image, img']) From a2f4f193c5873d51e3b1fff3a63a5caf808a8452 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 17:14:54 +0200 Subject: [PATCH 080/100] Update setup.py --- setup.py | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index a7876f96..c63094b9 100755 --- a/setup.py +++ b/setup.py @@ -5,11 +5,6 @@ import sys from setuptools import find_packages, setup -try: - from pip.req import parse_requirements -except ImportError: # pip >= 10.0.0 - from pip._internal.req import parse_requirements - WORK_DIR = pathlib.Path(__file__).parent # Check python version @@ -42,22 +37,6 @@ def get_description(): return f.read() -def get_requirements(filename=None): - """ - Read requirements from 'requirements txt' - - :return: requirements - :rtype: list - """ - if filename is None: - filename = 'requirements.txt' - - file = WORK_DIR / filename - - install_reqs = parse_requirements(str(file), session='hack') - return [str(ir.req) for ir in install_reqs] - - setup( name='aiogram', version=get_version(), @@ -77,9 +56,22 @@ setup( 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Libraries :: Application Frameworks', ], - install_requires=get_requirements(), - package_data={'': ['requirements.txt']}, + install_requires=[ + 'aiohttp>=3.5.4,<4.0.0', + 'Babel>=2.6.0', + 'certifi>=2019.3.9', + ], + extras_require={ + 'proxy': [ + 'aiohttp-socks>=3.3,<4.0.0', + ], + 'fast': [ + 'uvloop>=0.14.0,<0.15.0', + 'ujson>=1.35', + ], + }, include_package_data=False, ) From 5a559584ff51d4b2c7572f85462e178891a4c589 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 20:34:56 +0200 Subject: [PATCH 081/100] Fix deeplink filter --- 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 f6aeaa14..484dd973 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -180,7 +180,7 @@ class CommandStart(Command): return {'deep_link': match} return False - return {'deep_link': None} + return False class CommandHelp(Command): From 3783e7052a5b1be2c97f5f767ad7f3ecf6f75b37 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 21:40:31 +0200 Subject: [PATCH 082/100] Get back quote_html and escape_md functions --- aiogram/utils/markdown.py | 64 +++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index 7b217b4f..d3c8583b 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -18,6 +18,34 @@ HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """} _HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS +def quote_html(*content, sep=" "): + """ + Quote HTML symbols + + All <, >, & and " symbols that are not a part of a tag or + an HTML entity must be replaced with the corresponding HTML entities + (< with < > with > & with & and " with "). + + :param content: + :param sep: + :return: + """ + return html_decoration.quote(_join(*content, sep=sep)) + + +def escape_md(*content, sep=" "): + """ + Escape markdown text + + E.g. for usernames + + :param content: + :param sep: + :return: + """ + return markdown_decoration.quote(_join(*content, sep=sep)) + + def _join(*content, sep=" "): return sep.join(map(str, content)) @@ -41,7 +69,9 @@ def bold(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.bold.format(value=html_decoration.quote(_join(*content, sep=sep))) + return markdown_decoration.bold.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def hbold(*content, sep=" "): @@ -52,7 +82,9 @@ def hbold(*content, sep=" "): :param sep: :return: """ - return html_decoration.bold.format(value=html_decoration.quote(_join(*content, sep=sep))) + return html_decoration.bold.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def italic(*content, sep=" "): @@ -63,7 +95,9 @@ def italic(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.italic.format(value=html_decoration.quote(_join(*content, sep=sep))) + return markdown_decoration.italic.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def hitalic(*content, sep=" "): @@ -74,7 +108,9 @@ def hitalic(*content, sep=" "): :param sep: :return: """ - return html_decoration.italic.format(value=html_decoration.quote(_join(*content, sep=sep))) + return html_decoration.italic.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def code(*content, sep=" "): @@ -85,7 +121,9 @@ def code(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.code.format(value=html_decoration.quote(_join(*content, sep=sep))) + return markdown_decoration.code.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def hcode(*content, sep=" "): @@ -96,7 +134,9 @@ def hcode(*content, sep=" "): :param sep: :return: """ - return html_decoration.code.format(value=html_decoration.quote(_join(*content, sep=sep))) + return html_decoration.code.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def pre(*content, sep="\n"): @@ -107,7 +147,9 @@ def pre(*content, sep="\n"): :param sep: :return: """ - return markdown_decoration.pre.format(value=html_decoration.quote(_join(*content, sep=sep))) + return markdown_decoration.pre.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def hpre(*content, sep="\n"): @@ -118,7 +160,9 @@ def hpre(*content, sep="\n"): :param sep: :return: """ - return html_decoration.pre.format(value=html_decoration.quote(_join(*content, sep=sep))) + return html_decoration.pre.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def underline(*content, sep=" "): @@ -142,7 +186,9 @@ def hunderline(*content, sep=" "): :param sep: :return: """ - return html_decoration.underline.format(value=html_decoration.quote(_join(*content, sep=sep))) + return html_decoration.underline.format( + value=html_decoration.quote(_join(*content, sep=sep)) + ) def strikethrough(*content, sep=" "): From 538ba2bab1d71d95f3eb742a14aeb928e2887369 Mon Sep 17 00:00:00 2001 From: cybernet Date: Wed, 1 Jan 2020 20:20:09 +0000 Subject: [PATCH 083/100] aiogram dev --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04a95017..53abcd3c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ You can [read the docs here](http://docs.aiogram.dev/en/latest/). - Community: [@aiogram](https://t.me/aiogram) - Russian community: [@aiogram_ru](https://t.me/aiogram_ru) - Pip: [aiogram](https://pypi.python.org/pypi/aiogram) - - Docs: [ReadTheDocs](http://docs.aiogram.dev) + - Docs: [AiOGram Dev](https://docs.aiogram.dev/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) From 59a223ce81de40c8e28e27d964016d728d54e192 Mon Sep 17 00:00:00 2001 From: TC-b64 Date: Sat, 4 Jan 2020 17:09:01 +1100 Subject: [PATCH 084/100] Data validity condition changed --- aiogram/utils/callback_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py index b0162a7e..e24ad7b1 100644 --- a/aiogram/utils/callback_data.py +++ b/aiogram/utils/callback_data.py @@ -75,7 +75,7 @@ class CallbackData: raise TypeError('Too many arguments were passed!') callback_data = self.sep.join(data) - if len(callback_data) > 64: + if len(callback_data.encode()) > 64: raise ValueError('Resulted callback data is too long!') return callback_data From b436cf8e27922dfd87460bf637aa71d7d58d88b2 Mon Sep 17 00:00:00 2001 From: Egor Date: Sat, 4 Jan 2020 20:17:22 +0500 Subject: [PATCH 085/100] fix: renamed_argument decorator error Also, I removed hidden mutation of input in _handling function --- aiogram/utils/deprecated.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 5232e8a3..83a9034c 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -102,14 +102,18 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve is_coroutine = asyncio.iscoroutinefunction(func) def _handling(kwargs): + """ + Returns updated version of kwargs. + """ routine_type = 'coroutine' if is_coroutine else 'function' if old_name in kwargs: warn_deprecated(f"In {routine_type} '{func.__name__}' argument '{old_name}' " f"is renamed to '{new_name}' " f"and will be removed in aiogram {until_version}", stacklevel=stacklevel) + kwargs = kwargs.copy() kwargs.update({new_name: kwargs.pop(old_name)}) - return kwargs + return kwargs if is_coroutine: @functools.wraps(func) From 7917a196eddf3881823b552ffe4bceed5eab237a Mon Sep 17 00:00:00 2001 From: klaipher Date: Sat, 4 Jan 2020 18:08:41 +0200 Subject: [PATCH 086/100] Fix CommandStart filter --- 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 484dd973..b80448c9 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -180,7 +180,7 @@ class CommandStart(Command): return {'deep_link': match} return False - return False + return check class CommandHelp(Command): From a8ec717d32defe89ed5b00f890fc4475da19c739 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 5 Jan 2020 20:22:16 +0200 Subject: [PATCH 087/100] Fix tests --- 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 0592f31b..f29e1982 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -276,7 +276,7 @@ class TestCommandStart: test_filter = CommandStart() # empty filter message = Message(text=self.START) result = await test_filter.check(message) - assert result == {'deep_link': None} + assert result async def test_start_command_payload_is_matched(self): test_filter = CommandStart(deep_link=self.GOOD) From 29aa4daf2c3cc29073d44f59e61609facc9bc2d6 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 8 Jan 2020 00:09:56 +0500 Subject: [PATCH 088/100] Update i18n_example.py --- examples/i18n_example.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/i18n_example.py b/examples/i18n_example.py index 2d65655a..b626d048 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -83,7 +83,6 @@ def get_likes() -> int: def increase_likes() -> int: LIKES_STORAGE['count'] += 1 return get_likes() -# @dp.message_handler(commands='like') From 13b4f345a6abc13a00e8021ea93ff88d1c0f1bc3 Mon Sep 17 00:00:00 2001 From: cybernet Date: Wed, 8 Jan 2020 15:14:31 +0100 Subject: [PATCH 089/100] Update README.md as sugested by @JrooTJunior Co-Authored-By: Alex Root Junior --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 53abcd3c..4bea22bc 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ You can [read the docs here](http://docs.aiogram.dev/en/latest/). - Community: [@aiogram](https://t.me/aiogram) - Russian community: [@aiogram_ru](https://t.me/aiogram_ru) - Pip: [aiogram](https://pypi.python.org/pypi/aiogram) - - Docs: [AiOGram Dev](https://docs.aiogram.dev/en/latest/) + - Docs: [aiogram site](https://docs.aiogram.dev/) - 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) From a0261003535c771919ad3f11b36fc4af29bc814d Mon Sep 17 00:00:00 2001 From: gabbhack <43146729+gabbhack@users.noreply.github.com> Date: Thu, 16 Jan 2020 17:14:57 +0500 Subject: [PATCH 090/100] Fix ContentTypeFilter Now the ContentTypeFilter works correctly with single elements. --- aiogram/dispatcher/filters/builtin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index b80448c9..683ee841 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -444,6 +444,8 @@ class ContentTypeFilter(BoundFilter): default = types.ContentTypes.TEXT def __init__(self, content_types): + if isinstance(content_types, str): + content_types = (content_types,) self.content_types = content_types async def check(self, message): From ee803303aa07dee68266d89a430914d699478cd3 Mon Sep 17 00:00:00 2001 From: gabbhack <43146729+gabbhack@users.noreply.github.com> Date: Thu, 23 Jan 2020 20:23:08 +0500 Subject: [PATCH 091/100] Bot API 4.6 --- aiogram/bot/bot.py | 15 +++++++ aiogram/dispatcher/dispatcher.py | 76 ++++++++++++++++++++++++++++++++ aiogram/types/message_entity.py | 1 + aiogram/types/poll.py | 27 ++++++++++++ aiogram/types/reply_keyboard.py | 16 ++++++- aiogram/types/update.py | 5 ++- aiogram/types/user.py | 3 ++ 7 files changed, 141 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 81a15603..30974f3a 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -863,6 +863,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_poll(self, chat_id: typing.Union[base.Integer, base.String], question: base.String, options: typing.List[base.String], + is_anonymous: typing.Optional[base.Boolean] = None, + type: typing.Optional[base.String] = None, + allows_multiple_answers: typing.Optional[base.Boolean] = None, + correct_option_id: typing.Optional[base.Integer] = None, + is_closed: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, @@ -881,6 +886,16 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type question: :obj:`base.String` :param options: List of answer options, 2-10 strings 1-100 characters each :param options: :obj:`typing.List[base.String]` + :param is_anonymous: True, if the poll needs to be anonymous, defaults to True + :param is_anonymous: :obj:`typing.Optional[base.Boolean]` + :param type: Poll type, “quiz” or “regular”, defaults to “regular” + :param type: :obj:`typing.Optional[base.String]` + :param allows_multiple_answers: True, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to False + :param allows_multiple_answers: :obj:`typing.Optional[base.Boolean]` + :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param correct_option_id: :obj:`typing.Optional[base.Integer]` + :param is_closed: Pass True, if the poll needs to be immediately closed + :param is_closed: :obj:`typing.Optional[base.Boolean]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[Boolean]` :param reply_to_message_id: If the message is a reply, ID of the original message diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 600e25ba..af39bbc7 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -69,6 +69,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.shipping_query_handlers = Handler(self, middleware_key='shipping_query') self.pre_checkout_query_handlers = Handler(self, middleware_key='pre_checkout_query') self.poll_handlers = Handler(self, middleware_key='poll') + self.poll_answer_handlers = Handler(self, middleware_key='poll_answer') self.errors_handlers = Handler(self, once=False, middleware_key='error') self.middleware = MiddlewareManager(self) @@ -87,6 +88,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(StateFilter, exclude_event_handlers=[ self.errors_handlers, self.poll_handlers, + self.poll_answer_handlers, ]) filters_factory.bind(ContentTypeFilter, event_handlers=[ self.message_handlers, @@ -226,6 +228,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query) if update.poll: return await self.poll_handlers.notify(update.poll) + if update.poll_answer: + return await self.poll_answer_handlers.notify(update.poll_answer) except Exception as e: err = await self.errors_handlers.notify(update, e) if err: @@ -853,18 +857,90 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return decorator def register_poll_handler(self, callback, *custom_filters, run_task=None, **kwargs): + """ + Register handler for poll + + Example: + + .. code-block:: python3 + + dp.register_poll_handler(some_poll_handler) + + :param callback: + :param custom_filters: + :param run_task: run callback in task (no wait results) + :param kwargs: + """ filters_set = self.filters_factory.resolve(self.poll_handlers, *custom_filters, **kwargs) self.poll_handlers.register(self._wrap_async_task(callback, run_task), filters_set) def poll_handler(self, *custom_filters, run_task=None, **kwargs): + """ + Decorator for poll handler + + Example: + + .. code-block:: python3 + + @dp.poll_handler() + async def some_poll_handler(poll: types.Poll) + + :param custom_filters: + :param run_task: run callback in task (no wait results) + :param kwargs: + """ + def decorator(callback): self.register_poll_handler(callback, *custom_filters, run_task=run_task, **kwargs) return callback return decorator + + def register_poll_answer_handler(self, callback, *custom_filters, run_task=None, **kwargs): + """ + Register handler for poll_answer + + Example: + + .. code-block:: python3 + + dp.register_poll_answer_handler(some_poll_answer_handler) + + :param callback: + :param custom_filters: + :param run_task: run callback in task (no wait results) + :param kwargs: + """ + filters_set = self.filters_factory.resolve(self.poll_answer_handlers, + *custom_filters, + **kwargs) + self.poll_handlers.register(self._wrap_async_task(callback, run_task), filters_set) + + def poll_answer_handler(self, *custom_filters, run_task=None, **kwargs): + """ + Decorator for poll_answer handler + + Example: + + .. code-block:: python3 + + @dp.poll_answer_handler() + async def some_poll_answer_handler(poll: types.Poll) + + :param custom_filters: + :param run_task: run callback in task (no wait results) + :param kwargs: + """ + + def decorator(callback): + self.register_poll_answer_handler(callback, *custom_filters, run_task=run_task, + **kwargs) + return callback + + return decorator def register_errors_handler(self, callback, *custom_filters, exception=None, run_task=None, **kwargs): """ diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 98191e43..d0dce7c5 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -18,6 +18,7 @@ class MessageEntity(base.TelegramObject): length: base.Integer = fields.Field() url: base.String = fields.Field() user: User = fields.Field(base=User) + langugage: base.String = fields.Field() def get_text(self, text): """ diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index 316bca2d..16f83634 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -2,15 +2,42 @@ import typing from . import base from . import fields +from .user import User class PollOption(base.TelegramObject): + """ + This object contains information about one answer option in a poll. + + https://core.telegram.org/bots/api#polloption + """ text: base.String = fields.Field() voter_count: base.Integer = fields.Field() +class PollAnswer(base.TelegramObject): + """ + This object represents an answer of a user in a non-anonymous poll. + + https://core.telegram.org/bots/api#pollanswer + """ + poll_id: base.String = fields.Field() + user: User = fields.Field() + option_ids: typing.List[base.Integer] = fields.ListField() + + class Poll(base.TelegramObject): + """ + This object contains information about a poll. + + https://core.telegram.org/bots/api#poll + """ id: base.String = fields.Field() question: base.String = fields.Field() options: typing.List[PollOption] = fields.ListField(base=PollOption) + total_voter_count: base.Integer = fields.Field() is_closed: base.Boolean = fields.Field() + is_anonymous: base.Boolean = fields.Field() + poll_type: base.String = fields.Field(alias="type") + allows_multiple_answers: base.Boolean = fields.Field() + correct_option_id: base.Integer = fields.Field() diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 8eda21f9..4f4c2404 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -4,6 +4,18 @@ from . import base from . import fields +class KeyboardButtonPollType(base.TelegramObject): + """ + This object represents type of a poll, which is allowed to be created and sent when the corresponding button is pressed. + + https://core.telegram.org/bots/api#keyboardbuttonpolltype + """ + poll_type: base.String = fields.Field(alias="type") + + def __init__(self, poll_type: base.String): + super(KeyboardButtonPollType, self).__init__(poll_type=poll_type) + + class ReplyKeyboardMarkup(base.TelegramObject): """ This object represents a custom keyboard with reply options (see Introduction to bots for details and examples). @@ -81,14 +93,16 @@ class ReplyKeyboardMarkup(base.TelegramObject): class KeyboardButton(base.TelegramObject): """ - This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. Optional fields are mutually exclusive. + This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. Optional fields request_contact, request_location, and request_poll are mutually exclusive. Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. + Note: request_poll option will only work in Telegram versions released after 23 January, 2020. Older clients will receive unsupported message. https://core.telegram.org/bots/api#keyboardbutton """ text: base.String = fields.Field() request_contact: base.Boolean = fields.Field() request_location: base.Boolean = fields.Field() + request_poll: KeyboardButtonPollType = fields.Field() def __init__(self, text: base.String, request_contact: base.Boolean = None, diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 9f8ce0fb..2146cb9d 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -6,7 +6,7 @@ from .callback_query import CallbackQuery from .chosen_inline_result import ChosenInlineResult from .inline_query import InlineQuery from .message import Message -from .poll import Poll +from .poll import Poll, PollAnswer from .pre_checkout_query import PreCheckoutQuery from .shipping_query import ShippingQuery from ..utils import helper @@ -30,6 +30,7 @@ class Update(base.TelegramObject): shipping_query: ShippingQuery = fields.Field(base=ShippingQuery) pre_checkout_query: PreCheckoutQuery = fields.Field(base=PreCheckoutQuery) poll: Poll = fields.Field(base=Poll) + poll_answer: PollAnswer = fields.Field(base=PollAnswer) def __hash__(self): return self.update_id @@ -58,3 +59,5 @@ class AllowedUpdates(helper.Helper): CALLBACK_QUERY = helper.ListItem() # callback_query SHIPPING_QUERY = helper.ListItem() # shipping_query PRE_CHECKOUT_QUERY = helper.ListItem() # pre_checkout_query + POLL = helper.ListItem() # poll + POLL_ANSWER = helper.ListItem() # poll_answer diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 2bcdd032..8263cfc2 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -22,6 +22,9 @@ class User(base.TelegramObject): last_name: base.String = fields.Field() username: base.String = fields.Field() language_code: base.String = fields.Field() + can_join_groups: base.Boolean = fields.Field() + can_read_all_group_messages: base.Boolean = fields.Field() + supports_inline_queries: base.Boolean = fields.Field() @property def full_name(self): From caf9f9e411e94970fefcb6f035bde43862ce4d16 Mon Sep 17 00:00:00 2001 From: gabbhack <43146729+gabbhack@users.noreply.github.com> Date: Thu, 23 Jan 2020 20:49:43 +0500 Subject: [PATCH 092/100] typofix --- aiogram/types/message_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index d0dce7c5..77b23c5c 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -18,7 +18,7 @@ class MessageEntity(base.TelegramObject): length: base.Integer = fields.Field() url: base.String = fields.Field() user: User = fields.Field(base=User) - langugage: base.String = fields.Field() + language: base.String = fields.Field() def get_text(self, text): """ From a8debbba0462bd2d789b34e5d4869929b6274ba2 Mon Sep 17 00:00:00 2001 From: gabbhack <43146729+gabbhack@users.noreply.github.com> Date: Thu, 23 Jan 2020 20:51:25 +0500 Subject: [PATCH 093/100] poll_type to type --- aiogram/types/poll.py | 2 +- aiogram/types/reply_keyboard.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index 16f83634..84fde48d 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -38,6 +38,6 @@ class Poll(base.TelegramObject): total_voter_count: base.Integer = fields.Field() is_closed: base.Boolean = fields.Field() is_anonymous: base.Boolean = fields.Field() - poll_type: base.String = fields.Field(alias="type") + type: base.String = fields.Field() allows_multiple_answers: base.Boolean = fields.Field() correct_option_id: base.Integer = fields.Field() diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 4f4c2404..7d2283ec 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -10,10 +10,10 @@ class KeyboardButtonPollType(base.TelegramObject): https://core.telegram.org/bots/api#keyboardbuttonpolltype """ - poll_type: base.String = fields.Field(alias="type") + type: base.String = fields.Field() - def __init__(self, poll_type: base.String): - super(KeyboardButtonPollType, self).__init__(poll_type=poll_type) + def __init__(self, type: base.String): + super(KeyboardButtonPollType, self).__init__(type=type) class ReplyKeyboardMarkup(base.TelegramObject): From ca9a4440d1542267f07bc33fe9096a3965a7b6ef Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 23 Jan 2020 22:37:54 +0200 Subject: [PATCH 094/100] - Update docstring of KeyboardButton - Fix PollAnswer model and export it from `aiogram.types` - Fix dispatcher poll_answer handlers registration - Add actions to LoggingMiddleware --- aiogram/contrib/middlewares/logging.py | 14 ++++++++++++++ aiogram/dispatcher/dispatcher.py | 2 +- aiogram/types/__init__.py | 3 ++- aiogram/types/poll.py | 2 +- aiogram/types/reply_keyboard.py | 10 +++++++--- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index 9f389b60..308d0e10 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -146,6 +146,20 @@ class LoggingMiddleware(BaseMiddleware): if timeout > 0: self.logger.info(f"Process update [ID:{update.update_id}]: [failed] (in {timeout} ms)") + async def on_pre_process_poll(self, poll, data): + self.logger.info(f"Received poll [ID:{poll.id}]") + + async def on_post_process_poll(self, poll, results, data): + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} poll [ID:{poll.id}]") + + async def on_pre_process_poll_answer(self, poll_answer, data): + self.logger.info(f"Received poll answer [ID:{poll_answer.poll_id}] " + f"from user [ID:{poll_answer.user.id}]") + + async def on_post_process_poll_answer(self, poll_answer, results, data): + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} poll answer [ID:{poll_answer.poll_id}] " + f"from user [ID:{poll_answer.user.id}]") + class LoggingFilter(logging.Filter): """ diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index af39bbc7..dc793b76 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -917,7 +917,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_set = self.filters_factory.resolve(self.poll_answer_handlers, *custom_filters, **kwargs) - self.poll_handlers.register(self._wrap_async_task(callback, run_task), filters_set) + self.poll_answer_handlers.register(self._wrap_async_task(callback, run_task), filters_set) def poll_answer_handler(self, *custom_filters, run_task=None, **kwargs): """ diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 37dc4b3e..018c593a 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -45,7 +45,7 @@ from .passport_element_error import PassportElementError, PassportElementErrorDa PassportElementErrorSelfie from .passport_file import PassportFile from .photo_size import PhotoSize -from .poll import PollOption, Poll +from .poll import PollOption, Poll, PollAnswer from .pre_checkout_query import PreCheckoutQuery from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove from .response_parameters import ResponseParameters @@ -147,6 +147,7 @@ __all__ = ( 'PassportFile', 'PhotoSize', 'Poll', + 'PollAnswer', 'PollOption', 'PreCheckoutQuery', 'ReplyKeyboardMarkup', diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index 84fde48d..e5a485d4 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -22,7 +22,7 @@ class PollAnswer(base.TelegramObject): https://core.telegram.org/bots/api#pollanswer """ poll_id: base.String = fields.Field() - user: User = fields.Field() + user: User = fields.Field(base=User) option_ids: typing.List[base.Integer] = fields.ListField() diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 7d2283ec..59804f08 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -93,9 +93,13 @@ class ReplyKeyboardMarkup(base.TelegramObject): class KeyboardButton(base.TelegramObject): """ - This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. Optional fields request_contact, request_location, and request_poll are mutually exclusive. - Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016. Older clients will ignore them. - Note: request_poll option will only work in Telegram versions released after 23 January, 2020. Older clients will receive unsupported message. + This object represents one button of the reply keyboard. + For simple text buttons String can be used instead of this object to specify text of the button. + Optional fields request_contact, request_location, and request_poll are mutually exclusive. + Note: request_contact and request_location options will only work in Telegram versions released after 9 April, 2016. + Older clients will ignore them. + Note: request_poll option will only work in Telegram versions released after 23 January, 2020. + Older clients will receive unsupported message. https://core.telegram.org/bots/api#keyboardbutton """ From d1ff0046a519a2245869edf08656ae294244aa3c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 23 Jan 2020 22:44:24 +0200 Subject: [PATCH 095/100] Export KeyboardButtonPollType --- aiogram/types/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 018c593a..e4e27e1a 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -47,7 +47,7 @@ from .passport_file import PassportFile from .photo_size import PhotoSize from .poll import PollOption, Poll, PollAnswer from .pre_checkout_query import PreCheckoutQuery -from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove +from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType from .response_parameters import ResponseParameters from .shipping_address import ShippingAddress from .shipping_option import ShippingOption @@ -126,6 +126,7 @@ __all__ = ( 'InputVenueMessageContent', 'Invoice', 'KeyboardButton', + 'KeyboardButtonPollType', 'LabeledPrice', 'Location', 'LoginUrl', From 7c8006d742d26ddba8088be26f85f02f3a703798 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 23 Jan 2020 22:49:35 +0200 Subject: [PATCH 096/100] #262: Fix aiohttp-socks version in setup.py --- dev_requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index be2c8f7d..40a74f81 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -13,6 +13,6 @@ wheel>=0.31.1 sphinx>=2.0.1 sphinx-rtd-theme>=0.4.3 sphinxcontrib-programoutput>=0.14 -aiohttp-socks>=0.3.3 +aiohttp-socks>=0.3.4 rethinkdb>=2.4.1 coverage==4.5.3 diff --git a/setup.py b/setup.py index c63094b9..b21b4e57 100755 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( ], extras_require={ 'proxy': [ - 'aiohttp-socks>=3.3,<4.0.0', + 'aiohttp-socks>=0.3.4,<0.4.0', ], 'fast': [ 'uvloop>=0.14.0,<0.15.0', From 5db726d7585c5252343642f1201c4775ac47bfeb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 23 Jan 2020 23:13:07 +0200 Subject: [PATCH 097/100] Add IsSenderContact filter --- aiogram/dispatcher/dispatcher.py | 7 +++++++ aiogram/dispatcher/filters/__init__.py | 3 ++- aiogram/dispatcher/filters/builtin.py | 22 ++++++++++++++++++++++ docs/source/dispatcher/filters.rst | 6 ++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index dc793b76..ec4a3fa8 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -11,6 +11,7 @@ from aiohttp.helpers import sentinel from aiogram.utils.deprecated import renamed_argument from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \ RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter +from .filters.builtin import IsSenderContact from .handler import Handler from .middlewares import MiddlewareManager from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \ @@ -153,6 +154,12 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.channel_post_handlers, self.edited_channel_post_handlers, ]) + filters_factory.bind(IsSenderContact, event_handlers=[ + self.message_handlers, + self.edited_message_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers, + ]) def __del__(self): self.stop_polling() diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 67c13872..6de3cc7a 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,6 +1,6 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \ - Text, IDFilter, AdminFilter, IsReplyFilter + Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -26,6 +26,7 @@ __all__ = [ 'Text', 'IDFilter', 'IsReplyFilter', + 'IsSenderContact', 'AdminFilter', 'get_filter_spec', 'get_filters_spec', diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 683ee841..0a81998a 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -453,6 +453,28 @@ class ContentTypeFilter(BoundFilter): message.content_type in self.content_types +class IsSenderContact(BoundFilter): + """ + Filter check that the contact matches the sender + + `is_sender_contact=True` - contact matches the sender + `is_sender_contact=False` - result will be inverted + """ + key = 'is_sender_contact' + + def __init__(self, is_sender_contact: bool): + self.is_sender_contact = is_sender_contact + + async def check(self, message: types.Message) -> bool: + if not message.contact: + return False + is_sender_contact = message.contact.user_id == message.from_user.id + if self.is_sender_contact: + return is_sender_contact + else: + return not is_sender_contact + + class StateFilter(BoundFilter): """ Check user state diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index b174f1ef..af06b73e 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -94,6 +94,12 @@ ContentTypeFilter :members: :show-inheritance: +IsSenderContact +--------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.IsSenderContact + :members: + :show-inheritance: StateFilter ----------- From 1aa80c80d1294d47fa0977dcf3c93a90d644485e Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 24 Jan 2020 00:00:06 +0200 Subject: [PATCH 098/100] #247: Don't mutate TelegramObject in to_* and as_* methods --- aiogram/__init__.py | 2 +- aiogram/types/base.py | 15 ++++++++------- aiogram/types/input_media.py | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 1eb6ca66..ed995988 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.5' +__version__ = '2.5.1dev1' __api_version__ = '4.5' diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 2fd9129d..e64d3398 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -120,7 +120,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return getattr(self, ALIASES_ATTR_NAME, {}) @property - def values(self) -> typing.Tuple[str]: + def values(self) -> typing.Dict[str, typing.Any]: """ Get values @@ -161,9 +161,10 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): :return: """ - self.clean() result = {} for name, value in self.values.items(): + if value is None: + continue if name in self.props: value = self.props[name].export(self) if isinstance(value, TelegramObject): @@ -191,7 +192,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return json.dumps(self.to_python()) @classmethod - def create(cls: Type[T], *args: typing.Any, **kwargs: typing.Any) -> T: + def create(cls: typing.Type[T], *args: typing.Any, **kwargs: typing.Any) -> T: raise NotImplemented def __str__(self) -> str: @@ -225,15 +226,15 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return self.props[key].set_value(self, value, self.conf.get('parent', None)) raise KeyError(key) - def __contains__(self, item: typing.Dict[str, typing.Any]) -> bool: + def __contains__(self, item: str) -> bool: """ Check key contains in that object :param item: :return: """ - self.clean() - return item in self.values + # self.clean() + return bool(self.values.get(item, None)) def __iter__(self) -> typing.Iterator[str]: """ @@ -263,7 +264,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): yield value def __hash__(self) -> int: - def _hash(obj)-> int: + def _hash(obj) -> int: buf: int = 0 if isinstance(obj, list): for item in obj: diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 95ca75ae..952e7a55 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -349,7 +349,7 @@ class MediaGroup(base.TelegramObject): :return: """ - self.clean() + # self.clean() result = [] for obj in self.media: if isinstance(obj, base.TelegramObject): From 7a874c7a58b01959bae4a352559dbdd717c8e783 Mon Sep 17 00:00:00 2001 From: Evgen Date: Fri, 24 Jan 2020 10:49:12 +0500 Subject: [PATCH 099/100] Update reply_keyboard.py Fix missing request_poll in KeyboardButton.__init__ --- aiogram/types/reply_keyboard.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 59804f08..6aa4ad24 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -110,10 +110,12 @@ class KeyboardButton(base.TelegramObject): def __init__(self, text: base.String, request_contact: base.Boolean = None, - request_location: base.Boolean = None): + request_location: base.Boolean = None, + request_poll: KeyboardButtonPollType = None): super(KeyboardButton, self).__init__(text=text, request_contact=request_contact, - request_location=request_location) + request_location=request_location, + request_poll=request_poll) class ReplyKeyboardRemove(base.TelegramObject): From 20ba5faf5c1d09daff99bd42f393ff15a7f4c677 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Jan 2020 17:56:43 +0200 Subject: [PATCH 100/100] Improve poll type --- aiogram/types/__init__.py | 3 ++- aiogram/types/poll.py | 14 ++++++++++++-- aiogram/types/reply_keyboard.py | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index e4e27e1a..35461bb9 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -45,7 +45,7 @@ from .passport_element_error import PassportElementError, PassportElementErrorDa PassportElementErrorSelfie from .passport_file import PassportFile from .photo_size import PhotoSize -from .poll import PollOption, Poll, PollAnswer +from .poll import PollOption, Poll, PollAnswer, PollType from .pre_checkout_query import PreCheckoutQuery from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType from .response_parameters import ResponseParameters @@ -150,6 +150,7 @@ __all__ = ( 'Poll', 'PollAnswer', 'PollOption', + 'PollType', 'PreCheckoutQuery', 'ReplyKeyboardMarkup', 'ReplyKeyboardRemove', diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index e5a485d4..86b41d7e 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -1,7 +1,7 @@ import typing -from . import base -from . import fields +from ..utils import helper +from . import base, fields from .user import User @@ -11,6 +11,7 @@ class PollOption(base.TelegramObject): https://core.telegram.org/bots/api#polloption """ + text: base.String = fields.Field() voter_count: base.Integer = fields.Field() @@ -21,6 +22,7 @@ class PollAnswer(base.TelegramObject): https://core.telegram.org/bots/api#pollanswer """ + poll_id: base.String = fields.Field() user: User = fields.Field(base=User) option_ids: typing.List[base.Integer] = fields.ListField() @@ -32,6 +34,7 @@ class Poll(base.TelegramObject): https://core.telegram.org/bots/api#poll """ + id: base.String = fields.Field() question: base.String = fields.Field() options: typing.List[PollOption] = fields.ListField(base=PollOption) @@ -41,3 +44,10 @@ class Poll(base.TelegramObject): type: base.String = fields.Field() allows_multiple_answers: base.Boolean = fields.Field() correct_option_id: base.Integer = fields.Field() + + +class PollType(helper.Helper): + mode = helper.HelperMode.snake_case + + REGULAR = helper.Item() + QUIZ = helper.Item() diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 6aa4ad24..ced20417 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -12,7 +12,7 @@ class KeyboardButtonPollType(base.TelegramObject): """ type: base.String = fields.Field() - def __init__(self, type: base.String): + def __init__(self, type: typing.Optional[base.String] = None): super(KeyboardButtonPollType, self).__init__(type=type)