From 68a9df92227b76b754fe3723679af37eb4e324cb Mon Sep 17 00:00:00 2001 From: Suren Khorenyan Date: Wed, 9 Oct 2019 20:03:48 +0300 Subject: [PATCH 001/165] 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 002/165] 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 003/165] 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 004/165] 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 005/165] 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 006/165] 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 007/165] 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 008/165] 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 009/165] 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 010/165] 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 011/165] 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 012/165] 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 013/165] 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 014/165] 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 015/165] 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 016/165] 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 017/165] 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 018/165] 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 019/165] 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 020/165] 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 021/165] 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 022/165] 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 023/165] 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 1863ac85714e020c2a8677a2cb99bd50187fbfa3 Mon Sep 17 00:00:00 2001 From: uburuntu Date: Sun, 10 Nov 2019 00:34:20 +0300 Subject: [PATCH 024/165] enh: private chat links --- aiogram/types/chat.py | 10 ++++++++++ aiogram/types/message.py | 15 +++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 66b8fe4d..a05ad47e 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -63,6 +63,16 @@ class Chat(base.TelegramObject): return f"tg://user?id={self.id}" + @property + def shifted_id(self) -> int: + """ + Get shifted id of chat, e.g. for private links + + For example: -1001122334455 -> 1122334455 + """ + shift = -1000000000000 + return shift - self.id + def get_mention(self, name=None, as_html=False): if name is None: name = self.mention diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 8fdece2b..5fb012f1 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -258,12 +258,19 @@ class Message(base.TelegramObject): :return: str """ - if self.chat.type not in [ChatType.SUPER_GROUP, ChatType.CHANNEL]: + if ChatType.is_private(self.chat): raise TypeError('Invalid chat type!') - elif not self.chat.username: - raise TypeError('This chat does not have @username') - return f"https://t.me/{self.chat.username}/{self.message_id}" + url = 'https://t.me/' + if self.chat.username: + # Generates public link + url += f'{self.chat.username}/' + else: + # Generates private link available for chat members + url += f'c/{self.chat.shifted_id}/' + url += f'{self.message_id}' + + return url def link(self, text, as_html=True) -> str: """ From 386a1586e068211d3b7df672aea6bea12123c6e7 Mon Sep 17 00:00:00 2001 From: uburuntu Date: Sun, 10 Nov 2019 00:46:29 +0300 Subject: [PATCH 025/165] 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 026/165] 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 027/165] 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 deddcf540eba16244a264583c000d5f9018342cb Mon Sep 17 00:00:00 2001 From: uburuntu Date: Tue, 19 Nov 2019 20:40:18 +0300 Subject: [PATCH 028/165] enh: add check for private chat --- aiogram/types/chat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 1792613c..3f0c4e10 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -70,6 +70,8 @@ class Chat(base.TelegramObject): For example: -1001122334455 -> 1122334455 """ + if self.type == ChatType.PRIVATE: + raise TypeError('`shifted_id` property is not available for private chats') shift = -1000000000000 return shift - self.id From f5d008938f9ecfd55f694293f565cc9962a15ba4 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 23 Nov 2019 11:46:50 +0300 Subject: [PATCH 029/165] #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 030/165] #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 031/165] #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 032/165] #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 033/165] #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 034/165] #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 035/165] #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 036/165] #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 037/165] #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 038/165] #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 039/165] #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 040/165] #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 041/165] #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 042/165] 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 043/165] 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 044/165] 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 045/165] 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 046/165] 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 047/165] 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 048/165] 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 049/165] 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 050/165] 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 051/165] 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 052/165] 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 053/165] 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 054/165] 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 055/165] 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 056/165] 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 057/165] 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 058/165] 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 059/165] 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 060/165] #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 061/165] #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 062/165] 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 063/165] 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 064/165] 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 e1cf030a30f024e981140e1fbb7711e4fe37788d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 20:36:08 +0200 Subject: [PATCH 065/165] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 1eb6ca66..3d86ce7e 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.5' +__version__ = '2.5.1' __api_version__ = '4.5' From 3783e7052a5b1be2c97f5f767ad7f3ecf6f75b37 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 21:40:31 +0200 Subject: [PATCH 066/165] 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 9d70b095b30d92754905d5fc95b6591f1cf85984 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 1 Jan 2020 21:41:11 +0200 Subject: [PATCH 067/165] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 3d86ce7e..a85f73af 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.5.1' +__version__ = '2.5.2' __api_version__ = '4.5' From 538ba2bab1d71d95f3eb742a14aeb928e2887369 Mon Sep 17 00:00:00 2001 From: cybernet Date: Wed, 1 Jan 2020 20:20:09 +0000 Subject: [PATCH 068/165] 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 dee654ae9fa0b6793a965113946190fd758b2bf2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 3 Jan 2020 21:42:34 +0200 Subject: [PATCH 069/165] Update markdown quote pattern --- aiogram/utils/text_decorations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 5b2cf51c..ad52c9d7 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -114,7 +114,7 @@ html_decoration = TextDecoration( quote=html.escape, ) -MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-|{}.!])") +MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-=|{}.!])") markdown_decoration = TextDecoration( link="[{value}]({link})", From 59a223ce81de40c8e28e27d964016d728d54e192 Mon Sep 17 00:00:00 2001 From: TC-b64 Date: Sat, 4 Jan 2020 17:09:01 +1100 Subject: [PATCH 070/165] 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 071/165] 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 072/165] 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 073/165] 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 d41be211d4c09fe27651d2bf2a780f4fb2098eec Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 5 Jan 2020 20:28:48 +0200 Subject: [PATCH 074/165] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index a85f73af..8e008f39 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.5.2' +__version__ = '2.5.3' __api_version__ = '4.5' From 29aa4daf2c3cc29073d44f59e61609facc9bc2d6 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 8 Jan 2020 00:09:56 +0500 Subject: [PATCH 075/165] 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 076/165] 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 077/165] 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 078/165] 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 079/165] 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 080/165] 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 081/165] - 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 082/165] 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 083/165] #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 084/165] 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 ad2bf96eb7dc23ed890d6052f6f632ca4940529a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 23 Jan 2020 23:21:49 +0200 Subject: [PATCH 085/165] Bump API and framework versions --- README.md | 2 +- README.rst | 2 +- aiogram/__init__.py | 4 ++-- aiogram/bot/api.py | 2 +- docs/source/index.rst | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4bea22bc..c54c2f50 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.5-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index da323e03..39c5ac25 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.5-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 8e008f39..e12ba3d9 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.5.3' -__api_version__ = '4.5' +__version__ = '2.6' +__api_version__ = '4.6' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 5c6ce74d..a6dbd0ea 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.5 + List is updated to Bot API 4.6 """ mode = HelperMode.lowerCamelCase diff --git a/docs/source/index.rst b/docs/source/index.rst index e81deb7f..71ed04d9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.5-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From 1aa80c80d1294d47fa0977dcf3c93a90d644485e Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 24 Jan 2020 00:00:06 +0200 Subject: [PATCH 086/165] #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 087/165] 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 088/165] 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) From b8311c034e23241fdb28ae139cd7d85caa69e5f6 Mon Sep 17 00:00:00 2001 From: 2noik <2noik@MacBookAir-8.local> Date: Tue, 28 Jan 2020 23:45:35 +0300 Subject: [PATCH 089/165] add send_phone_number_to_provider and send_email_to_provider --- aiogram/bot/bot.py | 7 +++++++ aiogram/dispatcher/webhook.py | 13 ++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 30974f3a..39313558 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -210,6 +210,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ + reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) if self.parse_mode: @@ -1930,6 +1931,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): need_phone_number: typing.Union[base.Boolean, None] = None, need_email: typing.Union[base.Boolean, None] = None, need_shipping_address: typing.Union[base.Boolean, None] = None, + send_phone_number_to_provider: typing.Union[base.Boolean, None] = None, + send_email_to_provider: typing.Union[base.Boolean, None] = None, is_flexible: typing.Union[base.Boolean, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, reply_to_message_id: typing.Union[base.Integer, None] = None, @@ -1976,6 +1979,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type need_email: :obj:`typing.Union[base.Boolean, None]` :param need_shipping_address: Pass True, if you require the user's shipping address to complete the order :type need_shipping_address: :obj:`typing.Union[base.Boolean, None]` + :param send_phone_number_to_provider: Pass True, if user's phone number should be sent to provider + :type send_phone_number_to_provider: :obj:`typing.Union[base.Boolean, None]` + :param send_email_to_provider: Pass True, if user's email address should be sent to provider + :type send_email_to_provider: :obj:`typing.Union[base.Boolean, None]` :param is_flexible: Pass True, if the final price depends on the shipping method :type is_flexible: :obj:`typing.Union[base.Boolean, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 135fe21e..a717b486 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -1967,7 +1967,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): __slots__ = ('chat_id', 'title', 'description', 'payload', 'provider_token', 'start_parameter', 'currency', 'prices', 'photo_url', 'photo_size', 'photo_width', 'photo_height', - 'need_name', 'need_phone_number', 'need_email', 'need_shipping_address', 'is_flexible', + 'need_name', 'need_phone_number', 'need_email', 'need_shipping_address', + 'send_phone_number_to_provider', 'send_email_to_provider', 'is_flexible', 'disable_notification', 'reply_to_message_id', 'reply_markup') method = api.Methods.SEND_INVOICE @@ -1988,6 +1989,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): need_phone_number: Optional[Boolean] = None, need_email: Optional[Boolean] = None, need_shipping_address: Optional[Boolean] = None, + send_phone_number_to_provider: Optional[Boolean] = None, + send_email_to_provider: Optional[Boolean] = None, is_flexible: Optional[Boolean] = None, disable_notification: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, @@ -2016,6 +2019,10 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param need_email: Boolean (Optional) - Pass True, if you require the user's email to complete the order :param need_shipping_address: Boolean (Optional) - Pass True, if you require the user's shipping address to complete the order + :param send_phone_number_to_provider: Boolean (Optional) - Pass True, if user's phone number should be sent + to provider + :param send_email_to_provider: Boolean (Optional) - Pass True, if user's email address should be sent + to provider :param is_flexible: Boolean (Optional) - Pass True, if the final price depends on the shipping method :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. @@ -2039,6 +2046,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): self.need_phone_number = need_phone_number self.need_email = need_email self.need_shipping_address = need_shipping_address + self.send_phone_number_to_provider = send_phone_number_to_provider + self.send_email_to_provider = send_email_to_provider self.is_flexible = is_flexible self.disable_notification = disable_notification self.reply_to_message_id = reply_to_message_id @@ -2062,6 +2071,8 @@ class SendInvoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): 'need_phone_number': self.need_phone_number, 'need_email': self.need_email, 'need_shipping_address': self.need_shipping_address, + 'send_phone_number_to_provider': self.send_phone_number_to_provider, + 'send_email_to_provider': self.send_email_to_provider, 'is_flexible': self.is_flexible, 'disable_notification': self.disable_notification, 'reply_to_message_id': self.reply_to_message_id, From 30a17413f0092b864fbb7347a95c27ef05036afe Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Fri, 7 Feb 2020 15:02:00 +0300 Subject: [PATCH 090/165] fix: send_copy for poll: missed attribute getting --- 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 8fdece2b..e9343b0f 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1654,7 +1654,7 @@ class Message(base.TelegramObject): elif self.poll: kwargs.pop("parse_mode") return await self.bot.send_poll( - question=self.poll.question, options=self.poll.options, **kwargs + question=self.poll.question, options=[option.text for option in self.poll.options], **kwargs ) else: raise TypeError("This type of message can't be copied.") From cffec23371c937366f0fa122757cd68480881e2c Mon Sep 17 00:00:00 2001 From: Bachynin Ivan Date: Wed, 12 Feb 2020 19:10:34 +0200 Subject: [PATCH 091/165] fix redis pool connection closing - add locks in closing methods of RedisStorage - don't remove reference to the pool in the `close()` method --- aiogram/contrib/fsm_storage/redis.py | 38 +++++++++++++--------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 106a7b97..bf88eff7 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -44,19 +44,19 @@ class RedisStorage(BaseStorage): self._loop = loop or asyncio.get_event_loop() self._kwargs = kwargs - self._redis: aioredis.RedisConnection = None + self._redis: typing.Optional[aioredis.RedisConnection] = None self._connection_lock = asyncio.Lock(loop=self._loop) async def close(self): - if self._redis and not self._redis.closed: - self._redis.close() - del self._redis - self._redis = None + async with self._connection_lock: + if self._redis and not self._redis.closed: + self._redis.close() async def wait_closed(self): - if self._redis: - return await self._redis.wait_closed() - return True + async with self._connection_lock: + if self._redis: + return await self._redis.wait_closed() + return True async def redis(self) -> aioredis.RedisConnection: """ @@ -64,7 +64,7 @@ class RedisStorage(BaseStorage): """ # Use thread-safe asyncio Lock because this method without that is not safe async with self._connection_lock: - if self._redis is None: + if self._redis is None or self._redis.closed: self._redis = await aioredis.create_connection((self._host, self._port), db=self._db, password=self._password, ssl=self._ssl, loop=self._loop, @@ -144,7 +144,7 @@ class RedisStorage(BaseStorage): record_data.update(data, **kwargs) await self.set_record(chat=chat, user=user, state=record['state'], data=record_data) - async def get_states_list(self) -> typing.List[typing.Tuple[int]]: + async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]: """ Get list of all stored chat's and user's @@ -220,11 +220,11 @@ class RedisStorage2(BaseStorage): """ def __init__(self, host: str = 'localhost', port=6379, db=None, password=None, - ssl=None, pool_size=10, loop=None, prefix='fsm', - state_ttl: int = 0, - data_ttl: int = 0, - bucket_ttl: int = 0, - **kwargs): + ssl=None, pool_size=10, loop=None, prefix='fsm', + state_ttl: int = 0, + data_ttl: int = 0, + bucket_ttl: int = 0, + **kwargs): self._host = host self._port = port self._db = db @@ -239,7 +239,7 @@ class RedisStorage2(BaseStorage): self._data_ttl = data_ttl self._bucket_ttl = bucket_ttl - self._redis: aioredis.RedisConnection = None + self._redis: typing.Optional[aioredis.RedisConnection] = None self._connection_lock = asyncio.Lock(loop=self._loop) async def redis(self) -> aioredis.Redis: @@ -248,7 +248,7 @@ class RedisStorage2(BaseStorage): """ # Use thread-safe asyncio Lock because this method without that is not safe async with self._connection_lock: - if self._redis is None: + if self._redis is None or self._redis.closed: self._redis = await aioredis.create_redis_pool((self._host, self._port), db=self._db, password=self._password, ssl=self._ssl, minsize=1, maxsize=self._pool_size, @@ -262,8 +262,6 @@ class RedisStorage2(BaseStorage): async with self._connection_lock: if self._redis and not self._redis.closed: self._redis.close() - del self._redis - self._redis = None async def wait_closed(self): async with self._connection_lock: @@ -357,7 +355,7 @@ class RedisStorage2(BaseStorage): keys = await conn.keys(self.generate_key('*')) await conn.delete(*keys) - async def get_states_list(self) -> typing.List[typing.Tuple[int]]: + async def get_states_list(self) -> typing.List[typing.Tuple[str, str]]: """ Get list of all stored chat's and user's From 5a9a4c888ee38161bfd48275c68169706c151493 Mon Sep 17 00:00:00 2001 From: Bachynin Ivan Date: Wed, 12 Feb 2020 20:07:14 +0200 Subject: [PATCH 092/165] add simple tests for RedisStorage2 --- .gitignore | 3 +++ tests/conftest.py | 36 ++++++++++++++++++++++++- tests/contrib/fsm_storage/test_redis.py | 33 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/contrib/fsm_storage/test_redis.py diff --git a/.gitignore b/.gitignore index a8b34bd1..d20c39ba 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ docs/html # i18n/l10n *.mo + +# pynev +.python-version diff --git a/tests/conftest.py b/tests/conftest.py index fe936e18..03c8dbe4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1 +1,35 @@ -# pytest_plugins = "pytest_asyncio.plugin" +import pytest +from _pytest.config import UsageError +import aioredis.util + + +def pytest_addoption(parser): + parser.addoption("--redis", default=None, + help="run tests which require redis connection") + + +def pytest_configure(config): + config.addinivalue_line("markers", "redis: marked tests require redis connection to run") + + +def pytest_collection_modifyitems(config, items): + redis_uri = config.getoption("--redis") + if redis_uri is None: + skip_redis = pytest.mark.skip(reason="need --redis option with redis URI to run") + for item in items: + if "redis" in item.keywords: + item.add_marker(skip_redis) + return + try: + address, options = aioredis.util.parse_url(redis_uri) + assert isinstance(address, tuple), "Only redis and rediss schemas are supported, eg redis://foo." + except AssertionError as e: + raise UsageError(f"Invalid redis URI {redis_uri!r}: {e}") + + +@pytest.fixture(scope='session') +def redis_options(request): + redis_uri = request.config.getoption("--redis") + (host, port), options = aioredis.util.parse_url(redis_uri) + options.update({'host': host, 'port': port}) + return options diff --git a/tests/contrib/fsm_storage/test_redis.py b/tests/contrib/fsm_storage/test_redis.py new file mode 100644 index 00000000..527c905e --- /dev/null +++ b/tests/contrib/fsm_storage/test_redis.py @@ -0,0 +1,33 @@ +import pytest + +from aiogram.contrib.fsm_storage.redis import RedisStorage2 + + +@pytest.fixture() +async def store(redis_options): + s = RedisStorage2(**redis_options) + try: + yield s + finally: + conn = await s.redis() + await conn.flushdb() + await s.close() + await s.wait_closed() + + +@pytest.mark.redis +class TestRedisStorage2: + @pytest.mark.asyncio + async def test_set_get(self, store): + assert await store.get_data(chat='1234') == {} + await store.set_data(chat='1234', data={'foo': 'bar'}) + assert await store.get_data(chat='1234') == {'foo': 'bar'} + + @pytest.mark.asyncio + async def test_close_and_open_connection(self, store): + await store.set_data(chat='1234', data={'foo': 'bar'}) + assert await store.get_data(chat='1234') == {'foo': 'bar'} + pool_id = id(store._redis) + await store.close() + assert await store.get_data(chat='1234') == {'foo': 'bar'} # new pool was opened at this point + assert id(store._redis) != pool_id From cefdeb43f816bdd4bedc4944bad8e2bbf04ade54 Mon Sep 17 00:00:00 2001 From: Gabben <43146729+gabbhack@users.noreply.github.com> Date: Mon, 9 Mar 2020 20:56:19 +0500 Subject: [PATCH 093/165] Update dispatcher.py --- 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 ec4a3fa8..950ce60f 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -935,7 +935,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): .. code-block:: python3 @dp.poll_answer_handler() - async def some_poll_answer_handler(poll: types.Poll) + async def some_poll_answer_handler(poll_answer: types.PollAnswer) :param custom_filters: :param run_task: run callback in task (no wait results) From 577070c1d0593dd759c5edbcce7000f92d21bbb7 Mon Sep 17 00:00:00 2001 From: Jugal Kishore <49350241+crazyuploader@users.noreply.github.com> Date: Sun, 29 Mar 2020 11:24:23 +0530 Subject: [PATCH 094/165] Update install.rst Fix grammatical mistakes --- docs/source/install.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index 58c0f208..e4717a42 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 framework 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 the `python-aiogram `_ package. From sources ------------ @@ -52,7 +52,7 @@ You can speedup your bots by following next instructions: $ pip install uvloop -- Use `ujson `_ instead of default json module. +- Use `ujson `_ instead of the default json module. *UltraJSON* is an ultra fast JSON encoder and decoder written in pure C with bindings for Python 2.5+ and 3. @@ -64,9 +64,9 @@ You can speedup your bots by following next instructions: - Use aiohttp speedups - - Use `cchardet `_ instead of chardet module. + - Use `cchardet `_ instead of the chardet module. - *cChardet* is high speed universal character encoding detector. + *cChardet* is a high speed universal character encoding detector. **Installation:** @@ -94,4 +94,4 @@ You can speedup your bots by following next instructions: $ pip install aiohttp[speedups] -In addition, you don't need do nothing, *aiogram* is automatically starts using that if is found in your environment. +In addition, you don't need do anything, *aiogram* automatically starts using that if it is found in your environment. From 6ef8362a21f8ab6e3599b2425fca5f8caa6df7f7 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 31 Mar 2020 10:39:10 +0300 Subject: [PATCH 095/165] OpenCollective organization display fix Fixed issue https://github.com/opencollective/opencollective/issues/2668 with organization's avatar display --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c54c2f50..6af84f0b 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 d87ec767fb0c2a53f20f321db86323e9c0ecbe32 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 2 Apr 2020 01:05:48 +0300 Subject: [PATCH 096/165] Added default parse mode for send_animation method --- aiogram/bot/bot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 39313558..c7b6dcbe 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -523,6 +523,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals(), exclude=["animation", "thumb"]) + if self.parse_mode: + payload.setdefault('parse_mode', self.parse_mode) files = {} prepare_file(payload, files, 'animation', animation) From bd8e4fbb41bcab89c508a6211ec2d6da465388f2 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 15:45:29 +0300 Subject: [PATCH 097/165] #289 Added sendDice method --- aiogram/bot/api.py | 1 + aiogram/bot/bot.py | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index a6dbd0ea..8e2538a6 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -181,6 +181,7 @@ class Methods(Helper): SEND_VENUE = Item() # sendVenue SEND_CONTACT = Item() # sendContact SEND_POLL = Item() # sendPoll + SEND_DICE = Item() # sendDice SEND_CHAT_ACTION = Item() # sendChatAction GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos GET_FILE = Item() # getFile diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 39313558..914fbd82 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -914,6 +914,41 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_POLL, payload) return types.Message(**result) + async def send_dice(self, chat_id: typing.Union[base.Integer, base.String], + disable_notification: typing.Union[base.Boolean, None] = None, + reply_to_message_id: typing.Union[base.Integer, None] = None, + reply_markup: typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, + types.ReplyKeyboardRemove, + types.ForceReply, None] = None) -> types.Message: + """ + Use this method to send a dice, which will have a random value from 1 to 6. + On success, the sent Message is returned. + (Yes, we're aware of the “proper” singular of die. + But it's awkward, and we decided to help it change. One dice at a time!) + + Source: https://core.telegram.org/bots/api#senddice + + :param chat_id: Unique identifier for the target chat or username of the target channel + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_to_message_id: If the message is a reply, ID of the original message + :type reply_to_message_id: :obj:`typing.Union[base.Integer, None]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :return: On success, the sent Message is returned + :rtype: :obj:`types.Message` + """ + + reply_markup = prepare_arg(reply_markup) + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.SEND_DICE, payload) + return types.Message(**result) + async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String], action: base.String) -> base.Boolean: """ @@ -1134,8 +1169,9 @@ 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: + + 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. From 13462abe473858d332d612b682879aea143a53ec Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 15:49:18 +0300 Subject: [PATCH 098/165] test_bot minor refactoring --- tests/test_bot.py | 76 +++-------------------------------------------- 1 file changed, 4 insertions(+), 72 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 3e48ea57..6b7a04dd 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1,48 +1,19 @@ -import aresponses import pytest from aiogram import Bot, types +from . import FakeTelegram, TOKEN -TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' +pytestmark = pytest.mark.asyncio -class FakeTelegram(aresponses.ResponsesMockServer): - def __init__(self, message_dict, **kwargs): - super().__init__(**kwargs) - self._body, self._headers = self.parse_data(message_dict) - - async def __aenter__(self): - await super().__aenter__() - _response = self.Response(text=self._body, headers=self._headers, status=200, reason='OK') - self.add(self.ANY, response=_response) - - @staticmethod - def parse_data(message_dict): - import json - - _body = '{"ok":true,"result":' + json.dumps(message_dict) + '}' - _headers = {'Server': 'nginx/1.12.2', - 'Date': 'Tue, 03 Apr 2018 16:59:54 GMT', - 'Content-Type': 'application/json', - 'Content-Length': str(len(_body)), - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', - 'Access-Control-Expose-Headers': 'Content-Length,Content-Type,Date,Server,Connection', - 'Strict-Transport-Security': 'max-age=31536000; includeSubdomains'} - return _body, _headers - - -@pytest.yield_fixture() -@pytest.mark.asyncio -async def bot(event_loop): +@pytest.yield_fixture(name='bot') +async def bot_fixture(event_loop): """ Bot fixture """ _bot = Bot(TOKEN, loop=event_loop, parse_mode=types.ParseMode.MARKDOWN) yield _bot await _bot.close() -@pytest.mark.asyncio async def test_get_me(bot: Bot, event_loop): """ getMe method test """ from .types.dataset import USER @@ -53,7 +24,6 @@ async def test_get_me(bot: Bot, event_loop): assert result == user -@pytest.mark.asyncio async def test_send_message(bot: Bot, event_loop): """ sendMessage method test """ from .types.dataset import MESSAGE @@ -64,7 +34,6 @@ async def test_send_message(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_forward_message(bot: Bot, event_loop): """ forwardMessage method test """ from .types.dataset import FORWARDED_MESSAGE @@ -76,7 +45,6 @@ async def test_forward_message(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_photo(bot: Bot, event_loop): """ sendPhoto method test with file_id """ from .types.dataset import MESSAGE_WITH_PHOTO, PHOTO @@ -89,7 +57,6 @@ async def test_send_photo(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_audio(bot: Bot, event_loop): """ sendAudio method test with file_id """ from .types.dataset import MESSAGE_WITH_AUDIO @@ -102,7 +69,6 @@ async def test_send_audio(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_document(bot: Bot, event_loop): """ sendDocument method test with file_id """ from .types.dataset import MESSAGE_WITH_DOCUMENT @@ -114,7 +80,6 @@ async def test_send_document(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_video(bot: Bot, event_loop): """ sendVideo method test with file_id """ from .types.dataset import MESSAGE_WITH_VIDEO, VIDEO @@ -129,7 +94,6 @@ async def test_send_video(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_voice(bot: Bot, event_loop): """ sendVoice method test with file_id """ from .types.dataset import MESSAGE_WITH_VOICE, VOICE @@ -143,7 +107,6 @@ async def test_send_voice(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_video_note(bot: Bot, event_loop): """ sendVideoNote method test with file_id """ from .types.dataset import MESSAGE_WITH_VIDEO_NOTE, VIDEO_NOTE @@ -157,7 +120,6 @@ async def test_send_video_note(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_media_group(bot: Bot, event_loop): """ sendMediaGroup method test with file_id """ from .types.dataset import MESSAGE_WITH_MEDIA_GROUP, PHOTO @@ -171,7 +133,6 @@ async def test_send_media_group(bot: Bot, event_loop): assert result.pop().media_group_id -@pytest.mark.asyncio async def test_send_location(bot: Bot, event_loop): """ sendLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION @@ -184,7 +145,6 @@ async def test_send_location(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_edit_message_live_location_by_bot(bot: Bot, event_loop): """ editMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION @@ -198,7 +158,6 @@ async def test_edit_message_live_location_by_bot(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_edit_message_live_location_by_user(bot: Bot, event_loop): """ editMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION @@ -212,7 +171,6 @@ async def test_edit_message_live_location_by_user(bot: Bot, event_loop): assert isinstance(result, bool) and result is True -@pytest.mark.asyncio async def test_stop_message_live_location_by_bot(bot: Bot, event_loop): """ stopMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION @@ -224,7 +182,6 @@ async def test_stop_message_live_location_by_bot(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_stop_message_live_location_by_user(bot: Bot, event_loop): """ stopMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION @@ -237,7 +194,6 @@ async def test_stop_message_live_location_by_user(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_send_venue(bot: Bot, event_loop): """ sendVenue method test """ from .types.dataset import MESSAGE_WITH_VENUE, VENUE, LOCATION @@ -252,7 +208,6 @@ async def test_send_venue(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_contact(bot: Bot, event_loop): """ sendContact method test """ from .types.dataset import MESSAGE_WITH_CONTACT, CONTACT @@ -265,7 +220,6 @@ async def test_send_contact(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_send_chat_action(bot: Bot, event_loop): """ sendChatAction method test """ from .types.dataset import CHAT @@ -277,7 +231,6 @@ async def test_send_chat_action(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_get_user_profile_photo(bot: Bot, event_loop): """ getUserProfilePhotos method test """ from .types.dataset import USER_PROFILE_PHOTOS, USER @@ -288,7 +241,6 @@ async def test_get_user_profile_photo(bot: Bot, event_loop): assert isinstance(result, types.UserProfilePhotos) -@pytest.mark.asyncio async def test_get_file(bot: Bot, event_loop): """ getFile method test """ from .types.dataset import FILE @@ -299,7 +251,6 @@ async def test_get_file(bot: Bot, event_loop): assert isinstance(result, types.File) -@pytest.mark.asyncio async def test_kick_chat_member(bot: Bot, event_loop): """ kickChatMember method test """ from .types.dataset import USER, CHAT @@ -312,7 +263,6 @@ async def test_kick_chat_member(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_unban_chat_member(bot: Bot, event_loop): """ unbanChatMember method test """ from .types.dataset import USER, CHAT @@ -325,7 +275,6 @@ async def test_unban_chat_member(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_restrict_chat_member(bot: Bot, event_loop): """ restrictChatMember method test """ from .types.dataset import USER, CHAT @@ -346,7 +295,6 @@ async def test_restrict_chat_member(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_promote_chat_member(bot: Bot, event_loop): """ promoteChatMember method test """ from .types.dataset import USER, CHAT @@ -362,7 +310,6 @@ async def test_promote_chat_member(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_export_chat_invite_link(bot: Bot, event_loop): """ exportChatInviteLink method test """ from .types.dataset import CHAT, INVITE_LINK @@ -373,7 +320,6 @@ async def test_export_chat_invite_link(bot: Bot, event_loop): assert result == INVITE_LINK -@pytest.mark.asyncio async def test_delete_chat_photo(bot: Bot, event_loop): """ deleteChatPhoto method test """ from .types.dataset import CHAT @@ -385,7 +331,6 @@ async def test_delete_chat_photo(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_set_chat_title(bot: Bot, event_loop): """ setChatTitle method test """ from .types.dataset import CHAT @@ -397,7 +342,6 @@ async def test_set_chat_title(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_set_chat_description(bot: Bot, event_loop): """ setChatDescription method test """ from .types.dataset import CHAT @@ -409,7 +353,6 @@ async def test_set_chat_description(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_pin_chat_message(bot: Bot, event_loop): """ pinChatMessage method test """ from .types.dataset import MESSAGE @@ -422,7 +365,6 @@ async def test_pin_chat_message(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_unpin_chat_message(bot: Bot, event_loop): """ unpinChatMessage method test """ from .types.dataset import CHAT @@ -434,7 +376,6 @@ async def test_unpin_chat_message(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_leave_chat(bot: Bot, event_loop): """ leaveChat method test """ from .types.dataset import CHAT @@ -446,7 +387,6 @@ async def test_leave_chat(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_get_chat(bot: Bot, event_loop): """ getChat method test """ from .types.dataset import CHAT @@ -457,7 +397,6 @@ async def test_get_chat(bot: Bot, event_loop): assert result == chat -@pytest.mark.asyncio async def test_get_chat_administrators(bot: Bot, event_loop): """ getChatAdministrators method test """ from .types.dataset import CHAT, CHAT_MEMBER @@ -470,7 +409,6 @@ async def test_get_chat_administrators(bot: Bot, event_loop): assert len(result) == 2 -@pytest.mark.asyncio async def test_get_chat_members_count(bot: Bot, event_loop): """ getChatMembersCount method test """ from .types.dataset import CHAT @@ -482,7 +420,6 @@ async def test_get_chat_members_count(bot: Bot, event_loop): assert result == count -@pytest.mark.asyncio async def test_get_chat_member(bot: Bot, event_loop): """ getChatMember method test """ from .types.dataset import CHAT, CHAT_MEMBER @@ -495,7 +432,6 @@ async def test_get_chat_member(bot: Bot, event_loop): assert result == member -@pytest.mark.asyncio async def test_set_chat_sticker_set(bot: Bot, event_loop): """ setChatStickerSet method test """ from .types.dataset import CHAT @@ -507,7 +443,6 @@ async def test_set_chat_sticker_set(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_delete_chat_sticker_set(bot: Bot, event_loop): """ setChatStickerSet method test """ from .types.dataset import CHAT @@ -519,7 +454,6 @@ async def test_delete_chat_sticker_set(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_answer_callback_query(bot: Bot, event_loop): """ answerCallbackQuery method test """ @@ -529,7 +463,6 @@ async def test_answer_callback_query(bot: Bot, event_loop): assert result is True -@pytest.mark.asyncio async def test_edit_message_text_by_bot(bot: Bot, event_loop): """ editMessageText method test """ from .types.dataset import EDITED_MESSAGE @@ -541,7 +474,6 @@ async def test_edit_message_text_by_bot(bot: Bot, event_loop): assert result == msg -@pytest.mark.asyncio async def test_edit_message_text_by_user(bot: Bot, event_loop): """ editMessageText method test """ from .types.dataset import EDITED_MESSAGE From 44221872eb52e81869966c9a904013986e30e325 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 16:07:01 +0300 Subject: [PATCH 099/165] #289 added Dice type and ContentType --- aiogram/types/__init__.py | 1 + aiogram/types/dice.py | 13 +++++++++++++ aiogram/types/message.py | 14 ++++++++++---- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 aiogram/types/dice.py diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 35461bb9..7d201ede 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -11,6 +11,7 @@ from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .chosen_inline_result import ChosenInlineResult from .contact import Contact +from .dice import Dice from .document import Document from .encrypted_credentials import EncryptedCredentials from .encrypted_passport_element import EncryptedPassportElement diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py new file mode 100644 index 00000000..9d0e22d0 --- /dev/null +++ b/aiogram/types/dice.py @@ -0,0 +1,13 @@ +from . import base, fields + + +class Dice(base.TelegramObject): + """ + This object represents a dice with random value from 1 to 6. + (Yes, we're aware of the “proper” singular of die. + But it's awkward, and we decided to help it change. One dice at a time!) + + https://core.telegram.org/bots/api#dice + """ + + value: base.Integer = fields.Field() diff --git a/aiogram/types/message.py b/aiogram/types/message.py index badf1d51..d50eaf28 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -10,6 +10,7 @@ from .animation import Animation from .audio import Audio from .chat import Chat, ChatType from .contact import Contact +from .dice import Dice from .document import Document from .force_reply import ForceReply from .game import Game @@ -70,6 +71,8 @@ class Message(base.TelegramObject): contact: Contact = fields.Field(base=Contact) location: Location = fields.Field(base=Location) venue: Venue = fields.Field(base=Venue) + poll: Poll = fields.Field(base=Poll) + dice: Dice = fields.Field(base=Dice) new_chat_members: typing.List[User] = fields.ListField(base=User) left_chat_member: User = fields.Field(base=User) new_chat_title: base.String = fields.Field() @@ -85,7 +88,6 @@ class Message(base.TelegramObject): successful_payment: SuccessfulPayment = fields.Field(base=SuccessfulPayment) connected_website: base.String = fields.Field() passport_data: PassportData = fields.Field(base=PassportData) - poll: Poll = fields.Field(base=Poll) reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup) @property @@ -117,6 +119,10 @@ class Message(base.TelegramObject): return ContentType.VENUE if self.location: return ContentType.LOCATION + if self.poll: + return ContentType.POLL + if self.dice: + return ContentType.DICE if self.new_chat_members: return ContentType.NEW_CHAT_MEMBERS if self.left_chat_member: @@ -143,8 +149,7 @@ class Message(base.TelegramObject): return ContentType.GROUP_CHAT_CREATED if self.passport_data: return ContentType.PASSPORT_DATA - if self.poll: - return ContentType.POLL + return ContentType.UNKNOWN @@ -1685,6 +1690,8 @@ class ContentType(helper.Helper): CONTACT = helper.Item() # contact LOCATION = helper.Item() # location VENUE = helper.Item() # venue + POLL = helper.Item() # poll + DICE = helper.Item() # dice NEW_CHAT_MEMBERS = helper.Item() # new_chat_member LEFT_CHAT_MEMBER = helper.Item() # left_chat_member INVOICE = helper.Item() # invoice @@ -1698,7 +1705,6 @@ class ContentType(helper.Helper): DELETE_CHAT_PHOTO = helper.Item() # delete_chat_photo GROUP_CHAT_CREATED = helper.Item() # group_chat_created PASSPORT_DATA = helper.Item() # passport_data - POLL = helper.Item() UNKNOWN = helper.Item() # unknown ANY = helper.Item() # any From 0f245bbf56863ba79a34aca7c95539c60df18f78 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 16:14:26 +0300 Subject: [PATCH 100/165] #289 added sendDice test and dataset --- tests/test_bot.py | 10 ++++++++++ tests/types/dataset.py | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 6b7a04dd..0fa23df4 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -220,6 +220,16 @@ async def test_send_contact(bot: Bot, event_loop): assert result == msg +async def test_send_dice(bot: Bot, event_loop): + """ sendDice method test """ + from .types.dataset import MESSAGE_WITH_DICE + msg = types.Message(**MESSAGE_WITH_DICE) + + async with FakeTelegram(message_dict=MESSAGE_WITH_DICE, loop=event_loop): + result = await bot.send_dice(msg.chat.id, disable_notification=False) + assert result == msg + + async def test_send_chat_action(bot: Bot, event_loop): """ sendChatAction method test """ from .types.dataset import CHAT diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 18bcbdad..8950344c 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -53,6 +53,10 @@ CONTACT = { "last_name": "Smith", } +DICE = { + "value": 6 +} + DOCUMENT = { "file_name": "test.docx", "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -255,6 +259,14 @@ MESSAGE_WITH_CONTACT = { MESSAGE_WITH_DELETE_CHAT_PHOTO = {} +MESSAGE_WITH_DICE = { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508768012, + "dice": DICE +} + MESSAGE_WITH_DOCUMENT = { "message_id": 12345, "from": USER, From b77ed1ad92ff543f34e4a8a4b2211691e8f26e48 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 16:35:56 +0300 Subject: [PATCH 101/165] #289 added setMyCommands method; added BotCommand type --- aiogram/bot/api.py | 1 + aiogram/bot/bot.py | 18 ++++++++++++++++++ aiogram/types/__init__.py | 1 + aiogram/types/bot_command.py | 12 ++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 aiogram/types/bot_command.py diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 8e2538a6..6f3dc951 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -206,6 +206,7 @@ class Methods(Helper): SET_CHAT_STICKER_SET = Item() # setChatStickerSet DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery + SET_MY_COMMANDS = Item() # setMyCommands # Updating messages EDIT_MESSAGE_TEXT = Item() # editMessageText diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 914fbd82..1b1e8615 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1520,6 +1520,24 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload) return result + async def set_my_commands(self, commands: typing.List[types.BotCommand]) -> base.Boolean: + """ + Use this method to change the list of the bot's commands. + + Source: https://core.telegram.org/bots/api#setmycommands + + :param commands: A JSON-serialized list of bot commands to be set as the list of the bot's commands. + At most 100 commands can be specified. + :type commands: :obj: `typing.List[types.BotCommand]` + :return: Returns True on success. + :rtype: base.Boolean + """ + commands = prepare_arg(commands) + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.SET_MY_COMMANDS, payload) + return result + async def edit_message_text(self, text: base.String, chat_id: typing.Union[base.Integer, base.String, None] = None, message_id: typing.Union[base.Integer, None] = None, diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 7d201ede..693bd7d5 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -3,6 +3,7 @@ from . import fields from .animation import Animation from .audio import Audio from .auth_widget_data import AuthWidgetData +from .bot_command import BotCommand from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType diff --git a/aiogram/types/bot_command.py b/aiogram/types/bot_command.py new file mode 100644 index 00000000..a97238e5 --- /dev/null +++ b/aiogram/types/bot_command.py @@ -0,0 +1,12 @@ +from . import base +from . import fields + + +class BotCommand(base.TelegramObject): + """ + This object represents a bot command. + + https://core.telegram.org/bots/api#botcommand + """ + command: base.String = fields.Field() + description: base.String = fields.Field() From c8f126b8ea5e08a38a8a61e32c57339372f0fcbd Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 16:42:19 +0300 Subject: [PATCH 102/165] #289 added getMyCommands method --- aiogram/bot/api.py | 1 + aiogram/bot/bot.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 6f3dc951..5b760d07 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -207,6 +207,7 @@ class Methods(Helper): DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery SET_MY_COMMANDS = Item() # setMyCommands + GET_MY_COMMANDS = Item() # getMyCommands # Updating messages EDIT_MESSAGE_TEXT = Item() # editMessageText diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 1b1e8615..e8de768d 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1530,7 +1530,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): At most 100 commands can be specified. :type commands: :obj: `typing.List[types.BotCommand]` :return: Returns True on success. - :rtype: base.Boolean + :rtype: :obj:`base.Boolean` """ commands = prepare_arg(commands) payload = generate_payload(**locals()) @@ -1538,6 +1538,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SET_MY_COMMANDS, payload) return result + async def get_my_commands(self) -> typing.List[types.BotCommand]: + """ + Use this method to get the current list of the bot's commands. + + Source: https://core.telegram.org/bots/api#getmycommands + :return: Returns Array of BotCommand on success. + :rtype: :obj:`typing.List[types.BotCommand]` + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.GET_MY_COMMANDS, payload) + return result + async def edit_message_text(self, text: base.String, chat_id: typing.Union[base.Integer, base.String, None] = None, message_id: typing.Union[base.Integer, None] = None, From 38a2309c32579e11d131995ad337fb24f296f2f8 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 16:48:34 +0300 Subject: [PATCH 103/165] #289 added setMyCommands test --- tests/test_bot.py | 11 +++++++++++ tests/types/dataset.py | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 0fa23df4..b2289c8f 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -473,6 +473,17 @@ async def test_answer_callback_query(bot: Bot, event_loop): assert result is True +async def test_set_my_commands(bot: Bot, event_loop): + """ setMyCommands method test """ + from .types.dataset import BOT_COMMAND + + async with FakeTelegram(message_dict=True, loop=event_loop): + commands = [types.BotCommand(**BOT_COMMAND), types.BotCommand(**BOT_COMMAND)] + result = await bot.set_my_commands(commands) + assert isinstance(result, bool) + assert result is True + + async def test_edit_message_text_by_bot(bot: Bot, event_loop): """ editMessageText method test """ from .types.dataset import EDITED_MESSAGE diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 8950344c..310024cb 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -35,6 +35,11 @@ AUDIO = { "file_size": 9507774, } +BOT_COMMAND = { + "command": "start", + "description": "Start bot", +} + CHAT_MEMBER = { "user": USER, "status": "administrator", From a5f9373381d5cd4bdea458fe8f2e0a3f9244f047 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 17:02:43 +0300 Subject: [PATCH 104/165] refactored message_dict to message_data in FakeTelegram --- tests/__init__.py | 6 ++-- tests/test_bot.py | 84 +++++++++++++++++++++---------------------- tests/test_message.py | 2 +- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 262c9395..71ed023b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,9 +6,11 @@ TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' class FakeTelegram(aresponses.ResponsesMockServer): - def __init__(self, message_dict, bot=None, **kwargs): + def __init__(self, message_data, bot=None, **kwargs): + from aiogram.utils.payload import _normalize super().__init__(**kwargs) - self._body, self._headers = self.parse_data(message_dict) + message_data = _normalize(message_data) + self._body, self._headers = self.parse_data(message_data) if isinstance(bot, Bot): Bot.set_current(bot) diff --git a/tests/test_bot.py b/tests/test_bot.py index b2289c8f..c33b07d7 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -19,7 +19,7 @@ async def test_get_me(bot: Bot, event_loop): from .types.dataset import USER user = types.User(**USER) - async with FakeTelegram(message_dict=USER, loop=event_loop): + async with FakeTelegram(message_data=USER, loop=event_loop): result = await bot.me assert result == user @@ -29,7 +29,7 @@ async def test_send_message(bot: Bot, event_loop): from .types.dataset import MESSAGE msg = types.Message(**MESSAGE) - async with FakeTelegram(message_dict=MESSAGE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE, loop=event_loop): result = await bot.send_message(chat_id=msg.chat.id, text=msg.text) assert result == msg @@ -39,7 +39,7 @@ async def test_forward_message(bot: Bot, event_loop): from .types.dataset import FORWARDED_MESSAGE msg = types.Message(**FORWARDED_MESSAGE) - async with FakeTelegram(message_dict=FORWARDED_MESSAGE, loop=event_loop): + async with FakeTelegram(message_data=FORWARDED_MESSAGE, loop=event_loop): result = await bot.forward_message(chat_id=msg.chat.id, from_chat_id=msg.forward_from_chat.id, message_id=msg.forward_from_message_id) assert result == msg @@ -51,7 +51,7 @@ async def test_send_photo(bot: Bot, event_loop): msg = types.Message(**MESSAGE_WITH_PHOTO) photo = types.PhotoSize(**PHOTO) - async with FakeTelegram(message_dict=MESSAGE_WITH_PHOTO, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_PHOTO, loop=event_loop): result = await bot.send_photo(msg.chat.id, photo=photo.file_id, caption=msg.caption, parse_mode=types.ParseMode.HTML, disable_notification=False) assert result == msg @@ -62,7 +62,7 @@ async def test_send_audio(bot: Bot, event_loop): from .types.dataset import MESSAGE_WITH_AUDIO msg = types.Message(**MESSAGE_WITH_AUDIO) - async with FakeTelegram(message_dict=MESSAGE_WITH_AUDIO, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_AUDIO, loop=event_loop): result = await bot.send_audio(chat_id=msg.chat.id, audio=msg.audio.file_id, caption=msg.caption, parse_mode=types.ParseMode.HTML, duration=msg.audio.duration, performer=msg.audio.performer, title=msg.audio.title, disable_notification=False) @@ -74,7 +74,7 @@ async def test_send_document(bot: Bot, event_loop): from .types.dataset import MESSAGE_WITH_DOCUMENT msg = types.Message(**MESSAGE_WITH_DOCUMENT) - async with FakeTelegram(message_dict=MESSAGE_WITH_DOCUMENT, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_DOCUMENT, loop=event_loop): result = await bot.send_document(chat_id=msg.chat.id, document=msg.document.file_id, caption=msg.caption, parse_mode=types.ParseMode.HTML, disable_notification=False) assert result == msg @@ -86,7 +86,7 @@ async def test_send_video(bot: Bot, event_loop): msg = types.Message(**MESSAGE_WITH_VIDEO) video = types.Video(**VIDEO) - async with FakeTelegram(message_dict=MESSAGE_WITH_VIDEO, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_VIDEO, loop=event_loop): result = await bot.send_video(chat_id=msg.chat.id, video=video.file_id, duration=video.duration, width=video.width, height=video.height, caption=msg.caption, parse_mode=types.ParseMode.HTML, supports_streaming=True, @@ -100,7 +100,7 @@ async def test_send_voice(bot: Bot, event_loop): msg = types.Message(**MESSAGE_WITH_VOICE) voice = types.Voice(**VOICE) - async with FakeTelegram(message_dict=MESSAGE_WITH_VOICE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_VOICE, loop=event_loop): result = await bot.send_voice(chat_id=msg.chat.id, voice=voice.file_id, caption=msg.caption, parse_mode=types.ParseMode.HTML, duration=voice.duration, disable_notification=False) @@ -113,7 +113,7 @@ async def test_send_video_note(bot: Bot, event_loop): msg = types.Message(**MESSAGE_WITH_VIDEO_NOTE) video_note = types.VideoNote(**VIDEO_NOTE) - async with FakeTelegram(message_dict=MESSAGE_WITH_VIDEO_NOTE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_VIDEO_NOTE, loop=event_loop): result = await bot.send_video_note(chat_id=msg.chat.id, video_note=video_note.file_id, duration=video_note.duration, length=video_note.length, disable_notification=False) @@ -127,7 +127,7 @@ async def test_send_media_group(bot: Bot, event_loop): photo = types.PhotoSize(**PHOTO) media = [types.InputMediaPhoto(media=photo.file_id), types.InputMediaPhoto(media=photo.file_id)] - async with FakeTelegram(message_dict=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP], loop=event_loop): + async with FakeTelegram(message_data=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP], loop=event_loop): result = await bot.send_media_group(msg.chat.id, media=media, disable_notification=False) assert len(result) == len(media) assert result.pop().media_group_id @@ -139,7 +139,7 @@ async def test_send_location(bot: Bot, event_loop): msg = types.Message(**MESSAGE_WITH_LOCATION) location = types.Location(**LOCATION) - async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop): result = await bot.send_location(msg.chat.id, latitude=location.latitude, longitude=location.longitude, live_period=10, disable_notification=False) assert result == msg @@ -152,7 +152,7 @@ async def test_edit_message_live_location_by_bot(bot: Bot, event_loop): location = types.Location(**LOCATION) # editing bot message - async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop): result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id, latitude=location.latitude, longitude=location.longitude) assert result == msg @@ -165,7 +165,7 @@ async def test_edit_message_live_location_by_user(bot: Bot, event_loop): location = types.Location(**LOCATION) # editing user's message - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id, latitude=location.latitude, longitude=location.longitude) assert isinstance(result, bool) and result is True @@ -177,7 +177,7 @@ async def test_stop_message_live_location_by_bot(bot: Bot, event_loop): msg = types.Message(**MESSAGE_WITH_LOCATION) # stopping bot message - async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop): result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id) assert result == msg @@ -188,7 +188,7 @@ async def test_stop_message_live_location_by_user(bot: Bot, event_loop): msg = types.Message(**MESSAGE_WITH_LOCATION) # stopping user's message - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id) assert isinstance(result, bool) assert result is True @@ -201,7 +201,7 @@ async def test_send_venue(bot: Bot, event_loop): location = types.Location(**LOCATION) venue = types.Venue(**VENUE) - async with FakeTelegram(message_dict=MESSAGE_WITH_VENUE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_VENUE, loop=event_loop): result = await bot.send_venue(msg.chat.id, latitude=location.latitude, longitude=location.longitude, title=venue.title, address=venue.address, foursquare_id=venue.foursquare_id, disable_notification=False) @@ -214,7 +214,7 @@ async def test_send_contact(bot: Bot, event_loop): msg = types.Message(**MESSAGE_WITH_CONTACT) contact = types.Contact(**CONTACT) - async with FakeTelegram(message_dict=MESSAGE_WITH_CONTACT, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_CONTACT, loop=event_loop): result = await bot.send_contact(msg.chat.id, phone_number=contact.phone_number, first_name=contact.first_name, last_name=contact.last_name, disable_notification=False) assert result == msg @@ -225,7 +225,7 @@ async def test_send_dice(bot: Bot, event_loop): from .types.dataset import MESSAGE_WITH_DICE msg = types.Message(**MESSAGE_WITH_DICE) - async with FakeTelegram(message_dict=MESSAGE_WITH_DICE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_DICE, loop=event_loop): result = await bot.send_dice(msg.chat.id, disable_notification=False) assert result == msg @@ -235,7 +235,7 @@ async def test_send_chat_action(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.send_chat_action(chat_id=chat.id, action=types.ChatActions.TYPING) assert isinstance(result, bool) assert result is True @@ -246,7 +246,7 @@ async def test_get_user_profile_photo(bot: Bot, event_loop): from .types.dataset import USER_PROFILE_PHOTOS, USER user = types.User(**USER) - async with FakeTelegram(message_dict=USER_PROFILE_PHOTOS, loop=event_loop): + async with FakeTelegram(message_data=USER_PROFILE_PHOTOS, loop=event_loop): result = await bot.get_user_profile_photos(user_id=user.id, offset=1, limit=1) assert isinstance(result, types.UserProfilePhotos) @@ -256,7 +256,7 @@ async def test_get_file(bot: Bot, event_loop): from .types.dataset import FILE file = types.File(**FILE) - async with FakeTelegram(message_dict=FILE, loop=event_loop): + async with FakeTelegram(message_data=FILE, loop=event_loop): result = await bot.get_file(file_id=file.file_id) assert isinstance(result, types.File) @@ -267,7 +267,7 @@ async def test_kick_chat_member(bot: Bot, event_loop): user = types.User(**USER) chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.kick_chat_member(chat_id=chat.id, user_id=user.id, until_date=123) assert isinstance(result, bool) assert result is True @@ -279,7 +279,7 @@ async def test_unban_chat_member(bot: Bot, event_loop): user = types.User(**USER) chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.unban_chat_member(chat_id=chat.id, user_id=user.id) assert isinstance(result, bool) assert result is True @@ -291,7 +291,7 @@ async def test_restrict_chat_member(bot: Bot, event_loop): user = types.User(**USER) chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.restrict_chat_member( chat_id=chat.id, user_id=user.id, @@ -311,7 +311,7 @@ async def test_promote_chat_member(bot: Bot, event_loop): user = types.User(**USER) chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.promote_chat_member(chat_id=chat.id, user_id=user.id, can_change_info=True, can_delete_messages=True, can_edit_messages=True, can_invite_users=True, can_pin_messages=True, can_post_messages=True, @@ -325,7 +325,7 @@ async def test_export_chat_invite_link(bot: Bot, event_loop): from .types.dataset import CHAT, INVITE_LINK chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=INVITE_LINK, loop=event_loop): + async with FakeTelegram(message_data=INVITE_LINK, loop=event_loop): result = await bot.export_chat_invite_link(chat_id=chat.id) assert result == INVITE_LINK @@ -335,7 +335,7 @@ async def test_delete_chat_photo(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.delete_chat_photo(chat_id=chat.id) assert isinstance(result, bool) assert result is True @@ -346,7 +346,7 @@ async def test_set_chat_title(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.set_chat_title(chat_id=chat.id, title='Test title') assert isinstance(result, bool) assert result is True @@ -357,7 +357,7 @@ async def test_set_chat_description(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.set_chat_description(chat_id=chat.id, description='Test description') assert isinstance(result, bool) assert result is True @@ -368,7 +368,7 @@ async def test_pin_chat_message(bot: Bot, event_loop): from .types.dataset import MESSAGE message = types.Message(**MESSAGE) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.pin_chat_message(chat_id=message.chat.id, message_id=message.message_id, disable_notification=False) assert isinstance(result, bool) @@ -380,7 +380,7 @@ async def test_unpin_chat_message(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.unpin_chat_message(chat_id=chat.id) assert isinstance(result, bool) assert result is True @@ -391,7 +391,7 @@ async def test_leave_chat(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.leave_chat(chat_id=chat.id) assert isinstance(result, bool) assert result is True @@ -402,7 +402,7 @@ async def test_get_chat(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=CHAT, loop=event_loop): + async with FakeTelegram(message_data=CHAT, loop=event_loop): result = await bot.get_chat(chat_id=chat.id) assert result == chat @@ -413,7 +413,7 @@ async def test_get_chat_administrators(bot: Bot, event_loop): chat = types.Chat(**CHAT) member = types.ChatMember(**CHAT_MEMBER) - async with FakeTelegram(message_dict=[CHAT_MEMBER, CHAT_MEMBER], loop=event_loop): + async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER], loop=event_loop): result = await bot.get_chat_administrators(chat_id=chat.id) assert result[0] == member assert len(result) == 2 @@ -425,7 +425,7 @@ async def test_get_chat_members_count(bot: Bot, event_loop): chat = types.Chat(**CHAT) count = 5 - async with FakeTelegram(message_dict=count, loop=event_loop): + async with FakeTelegram(message_data=count, loop=event_loop): result = await bot.get_chat_members_count(chat_id=chat.id) assert result == count @@ -436,7 +436,7 @@ async def test_get_chat_member(bot: Bot, event_loop): chat = types.Chat(**CHAT) member = types.ChatMember(**CHAT_MEMBER) - async with FakeTelegram(message_dict=CHAT_MEMBER, loop=event_loop): + async with FakeTelegram(message_data=CHAT_MEMBER, loop=event_loop): result = await bot.get_chat_member(chat_id=chat.id, user_id=member.user.id) assert isinstance(result, types.ChatMember) assert result == member @@ -447,7 +447,7 @@ async def test_set_chat_sticker_set(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.set_chat_sticker_set(chat_id=chat.id, sticker_set_name='aiogram_stickers') assert isinstance(result, bool) assert result is True @@ -458,7 +458,7 @@ async def test_delete_chat_sticker_set(bot: Bot, event_loop): from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.delete_chat_sticker_set(chat_id=chat.id) assert isinstance(result, bool) assert result is True @@ -467,7 +467,7 @@ async def test_delete_chat_sticker_set(bot: Bot, event_loop): async def test_answer_callback_query(bot: Bot, event_loop): """ answerCallbackQuery method test """ - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.answer_callback_query(callback_query_id='QuERyId', text='Test Answer') assert isinstance(result, bool) assert result is True @@ -477,7 +477,7 @@ async def test_set_my_commands(bot: Bot, event_loop): """ setMyCommands method test """ from .types.dataset import BOT_COMMAND - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): commands = [types.BotCommand(**BOT_COMMAND), types.BotCommand(**BOT_COMMAND)] result = await bot.set_my_commands(commands) assert isinstance(result, bool) @@ -490,7 +490,7 @@ async def test_edit_message_text_by_bot(bot: Bot, event_loop): msg = types.Message(**EDITED_MESSAGE) # message by bot - async with FakeTelegram(message_dict=EDITED_MESSAGE, loop=event_loop): + async with FakeTelegram(message_data=EDITED_MESSAGE, loop=event_loop): result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id) assert result == msg @@ -501,7 +501,7 @@ async def test_edit_message_text_by_user(bot: Bot, event_loop): msg = types.Message(**EDITED_MESSAGE) # message by user - async with FakeTelegram(message_dict=True, loop=event_loop): + async with FakeTelegram(message_data=True, loop=event_loop): result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id) assert isinstance(result, bool) assert result is True diff --git a/tests/test_message.py b/tests/test_message.py index 996529f3..32168d57 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -28,7 +28,7 @@ async def message(bot, event_loop): from .types.dataset import MESSAGE msg = types.Message(**MESSAGE) - async with FakeTelegram(message_dict=MESSAGE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE, loop=event_loop): _message = await bot.send_message(chat_id=msg.chat.id, text=msg.text) yield _message From 93b60b6d75a2530e0ffb14c12cf104255c055b6c Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 17:25:16 +0300 Subject: [PATCH 105/165] #289 fixed getMyCommands output --- aiogram/bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index e8de768d..d677e060 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1549,7 +1549,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) result = await self.request(api.Methods.GET_MY_COMMANDS, payload) - return result + return [types.BotCommand(**bot_command_data) for bot_command_data in result] async def edit_message_text(self, text: base.String, chat_id: typing.Union[base.Integer, base.String, None] = None, From 505d6bf75bccc3aa9f519d8f808f0e01bdc6f631 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 17:26:18 +0300 Subject: [PATCH 106/165] normalized FakeTelegram passed data --- tests/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 71ed023b..3a85ce3f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,9 +7,7 @@ TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' class FakeTelegram(aresponses.ResponsesMockServer): def __init__(self, message_data, bot=None, **kwargs): - from aiogram.utils.payload import _normalize super().__init__(**kwargs) - message_data = _normalize(message_data) self._body, self._headers = self.parse_data(message_data) if isinstance(bot, Bot): @@ -26,10 +24,11 @@ class FakeTelegram(aresponses.ResponsesMockServer): await super().__aexit__(exc_type, exc_val, exc_tb) @staticmethod - def parse_data(message_dict): + def parse_data(message_data): import json + from aiogram.utils.payload import _normalize - _body = '{"ok":true,"result":' + json.dumps(message_dict) + '}' + _body = '{"ok":true,"result":' + json.dumps(_normalize(message_data)) + '}' _headers = {'Server': 'nginx/1.12.2', 'Date': 'Tue, 03 Apr 2018 16:59:54 GMT', 'Content-Type': 'application/json', From 29d767bd78cbed213ee72edbd376eccb575e4d26 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 17:26:53 +0300 Subject: [PATCH 107/165] #289 added getMyCommands test --- tests/test_bot.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index c33b07d7..70f8eb89 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -484,6 +484,17 @@ async def test_set_my_commands(bot: Bot, event_loop): assert result is True +async def test_get_my_commands(bot: Bot, event_loop): + """ getMyCommands method test """ + from .types.dataset import BOT_COMMAND + command = types.BotCommand(**BOT_COMMAND) + commands = [command, command] + async with FakeTelegram(message_data=commands, loop=event_loop): + result = await bot.get_my_commands() + assert isinstance(result, list) + assert all([isinstance(command, types.BotCommand) for command in result]) + + async def test_edit_message_text_by_bot(bot: Bot, event_loop): """ editMessageText method test """ from .types.dataset import EDITED_MESSAGE From 3ab5b80cafbf9bf7c4dc7b2e9eb6fd47d0365744 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 17:48:35 +0300 Subject: [PATCH 108/165] #289 Added the ability to create animated sticker sets by specifying the parameter tgs_sticker instead of png_sticker in the method createNewStickerSet --- aiogram/bot/bot.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index d677e060..01c66e03 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1837,8 +1837,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.UPLOAD_STICKER_FILE, payload, files) return types.File(**result) - async def create_new_sticker_set(self, user_id: base.Integer, name: base.String, title: base.String, - png_sticker: typing.Union[base.InputFile, base.String], emojis: base.String, + async def create_new_sticker_set(self, + user_id: base.Integer, + name: base.String, + title: base.String, + png_sticker: typing.Union[base.InputFile, base.String], + tgs_sticker: base.InputFile, + emojis: base.String, contains_masks: typing.Union[base.Boolean, None] = None, mask_position: typing.Union[types.MaskPosition, None] = None) -> base.Boolean: """ @@ -1855,6 +1860,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. :type png_sticker: :obj:`typing.Union[base.InputFile, base.String]` + :param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data. + See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements + :type tgs_sticker: :obj:`base.InputFile` :param emojis: One or more emoji corresponding to the sticker :type emojis: :obj:`base.String` :param contains_masks: Pass True, if a set of mask stickers should be created @@ -1865,10 +1873,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`base.Boolean` """ mask_position = prepare_arg(mask_position) - payload = generate_payload(**locals(), exclude=['png_sticker']) + payload = generate_payload(**locals(), exclude=['png_sticker', 'tgs_sticker']) files = {} prepare_file(payload, files, 'png_sticker', png_sticker) + prepare_file(payload, files, 'tgs_sticker', tgs_sticker) result = await self.request(api.Methods.CREATE_NEW_STICKER_SET, payload, files) return result From 12faef50f81df80e7d26ef95151377fb8bc9f5df Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 17:55:00 +0300 Subject: [PATCH 109/165] #289 updated createNewStickerSet docs --- aiogram/bot/bot.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 01c66e03..a63d4f00 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1847,18 +1847,26 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): contains_masks: typing.Union[base.Boolean, None] = None, mask_position: typing.Union[types.MaskPosition, None] = None) -> base.Boolean: """ - Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. + Use this method to create a new sticker set owned by a user. + The bot will be able to edit the sticker set thus created. + You must use exactly one of the fields png_sticker or tgs_sticker. Source: https://core.telegram.org/bots/api#createnewstickerset :param user_id: User identifier of created sticker set owner :type user_id: :obj:`base.Integer` - :param name: Short name of sticker set, to be used in t.me/addstickers/ URLs (e.g., animals) + :param name: Short name of sticker set, to be used in t.me/addstickers/ URLs (e.g., animals). + Can contain only english letters, digits and underscores. + Must begin with a letter, can't contain consecutive underscores and must end in “_by_”. + is case insensitive. 1-64 characters. :type name: :obj:`base.String` :param title: Sticker set title, 1-64 characters :type title: :obj:`base.String` - :param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size, + :param png_sticker: PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. + Pass a file_id as a String to send a file that already exists on the Telegram servers, + pass an HTTP URL as a String for Telegram to get a file from the Internet, or + upload a new one using multipart/form-data. More info on https://core.telegram.org/bots/api#sending-files :type png_sticker: :obj:`typing.Union[base.InputFile, base.String]` :param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data. See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements From 5e3df8bfec16f905ac4ff862c7beab2d4eafb963 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 5 Apr 2020 18:22:27 +0300 Subject: [PATCH 110/165] #277: Get default parse mode from context in chat.get_mention() --- aiogram/types/chat.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 08d935e5..0d3947f6 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -77,6 +77,9 @@ class Chat(base.TelegramObject): return shift - self.id def get_mention(self, name=None, as_html=True) -> base.String: + if as_html is None and self.bot.parse_mode and self.bot.parse_mode.lower() == 'html': + as_html = True + if name is None: name = self.mention if as_html: From 8f07b1248e9e69fd64edefd80f2b5b2d2f808dc9 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 5 Apr 2020 18:22:52 +0300 Subject: [PATCH 111/165] More safe bot deleter --- aiogram/bot/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 7d5ecc45..6750e8a8 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -101,7 +101,7 @@ class BaseBot: self.parse_mode = parse_mode def __del__(self): - if not hasattr(self, 'loop'): + if not hasattr(self, 'loop') or not hasattr(self, 'session'): return if self.loop.is_running(): self.loop.create_task(self.close()) From 5de9c9853e3af4611c59d144db858d76ea4ce5f3 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 5 Apr 2020 18:28:59 +0300 Subject: [PATCH 112/165] #270: Add new exception --- aiogram/utils/exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index bec48d97..cee2820a 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -272,6 +272,10 @@ class PollQuestionLengthTooLong(PollSizeError): match = "poll question length must not exceed 255" +class PollCanBeRequestedInPrivateChatsOnly(PollError): + match = "Poll can be requested in private chats only" + + class MessageWithPollNotFound(PollError, MessageError): """ Will be raised when you try to stop poll with message without poll From 181f1f0e6dbbd61f48c3ed451b8819117a02f57d Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 18:48:37 +0300 Subject: [PATCH 113/165] #289 Added the ability to add animated stickers to sets created by the bot by specifying the parameter tgs_sticker instead of png_sticker in the method addStickerToSet. --- aiogram/bot/bot.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index a63d4f00..9d242681 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1841,9 +1841,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): user_id: base.Integer, name: base.String, title: base.String, - png_sticker: typing.Union[base.InputFile, base.String], - tgs_sticker: base.InputFile, emojis: base.String, + png_sticker: typing.Union[base.InputFile, base.String] = None, + tgs_sticker: base.InputFile = None, contains_masks: typing.Union[base.Boolean, None] = None, mask_position: typing.Union[types.MaskPosition, None] = None) -> base.Boolean: """ @@ -1890,11 +1890,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.CREATE_NEW_STICKER_SET, payload, files) return result - async def add_sticker_to_set(self, user_id: base.Integer, name: base.String, - png_sticker: typing.Union[base.InputFile, base.String], emojis: base.String, + async def add_sticker_to_set(self, + user_id: base.Integer, + name: base.String, + emojis: base.String, + png_sticker: typing.Union[base.InputFile, base.String] = None, + tgs_sticker: base.InputFile = None, mask_position: typing.Union[types.MaskPosition, None] = None) -> base.Boolean: """ Use this method to add a new sticker to a set created by the bot. + You must use exactly one of the fields png_sticker or tgs_sticker. + Animated stickers can be added to animated sticker sets and only to them. + Animated sticker sets can have up to 50 stickers. + Static sticker sets can have up to 120 stickers. Source: https://core.telegram.org/bots/api#addstickertoset @@ -1902,9 +1910,15 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type user_id: :obj:`base.Integer` :param name: Sticker set name :type name: :obj:`base.String` - :param png_sticker: Png image with the sticker, must be up to 512 kilobytes in size, + :param png_sticker: PNG image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. + Pass a file_id as a String to send a file that already exists on the Telegram servers, + pass an HTTP URL as a String for Telegram to get a file from the Internet, or + upload a new one using multipart/form-data. More info on https://core.telegram.org/bots/api#sending-files :type png_sticker: :obj:`typing.Union[base.InputFile, base.String]` + :param tgs_sticker: TGS animation with the sticker, uploaded using multipart/form-data. + See https://core.telegram.org/animated_stickers#technical-requirements for technical requirements + :type tgs_sticker: :obj:`base.InputFile` :param emojis: One or more emoji corresponding to the sticker :type emojis: :obj:`base.String` :param mask_position: A JSON-serialized object for position where the mask should be placed on faces @@ -1913,10 +1927,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`base.Boolean` """ mask_position = prepare_arg(mask_position) - payload = generate_payload(**locals(), exclude=['png_sticker']) + payload = generate_payload(**locals(), exclude=['png_sticker', 'tgs_sticker']) files = {} prepare_file(payload, files, 'png_sticker', png_sticker) + prepare_file(payload, files, 'tgs_sticker', png_sticker) result = await self.request(api.Methods.ADD_STICKER_TO_SET, payload, files) return result From 3d632fea1c13ff1c1b6300c42ec877a31f0953a4 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 18:54:10 +0300 Subject: [PATCH 114/165] #289 Added the field thumb to the StickerSet object. --- aiogram/types/sticker_set.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/types/sticker_set.py b/aiogram/types/sticker_set.py index cb30abe6..3b5290c3 100644 --- a/aiogram/types/sticker_set.py +++ b/aiogram/types/sticker_set.py @@ -2,6 +2,7 @@ import typing from . import base from . import fields +from .photo_size import PhotoSize from .sticker import Sticker @@ -16,3 +17,4 @@ class StickerSet(base.TelegramObject): is_animated: base.Boolean = fields.Field() contains_masks: base.Boolean = fields.Field() stickers: typing.List[Sticker] = fields.ListField(base=Sticker) + thumb: PhotoSize = fields.Field(base=PhotoSize) From 9bbc7510f4dbd252847626df09023b21c080f894 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 19:04:48 +0300 Subject: [PATCH 115/165] #289 Added the ability to change thumbnails of sticker sets created by the bot using the method setStickerSetThumb. --- aiogram/bot/api.py | 1 + aiogram/bot/bot.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 5b760d07..f0553644 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -225,6 +225,7 @@ class Methods(Helper): ADD_STICKER_TO_SET = Item() # addStickerToSet SET_STICKER_POSITION_IN_SET = Item() # setStickerPositionInSet DELETE_STICKER_FROM_SET = Item() # deleteStickerFromSet + SET_STICKER_SET_THUMB = Item() # setStickerSetThumb # Inline mode ANSWER_INLINE_QUERY = Item() # answerInlineQuery diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 9d242681..d6946513 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1970,6 +1970,39 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.DELETE_STICKER_FROM_SET, payload) return result + async def set_sticker_set_thumb(self, + name: base.String, + user_id: base.Integer, + thumb: typing.Union[base.InputFile, base.String] = None) -> base.Boolean: + """ + Use this method to set the thumbnail of a sticker set. + Animated thumbnails can be set for animated sticker sets only. + + Source: https://core.telegram.org/bots/api#setstickersetthumb + + :param name: Sticker set name + :type name: :obj:`base.String` + :param user_id: User identifier of the sticker set owner + :type user_id: :obj:`base.Integer` + :param thumb: A PNG image with the thumbnail, must be up to 128 kilobytes in size and have width and height + exactly 100px, or a TGS animation with the thumbnail up to 32 kilobytes in size; + see https://core.telegram.org/animated_stickers#technical-requirements for animated sticker technical + requirements. Pass a file_id as a String to send a file that already exists on the Telegram servers, + pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using + multipart/form-data. More info on https://core.telegram.org/bots/api#sending-files. + Animated sticker set thumbnail can't be uploaded via HTTP URL. + :type thumb: :obj:`typing.Union[base.InputFile, base.String]` + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + payload = generate_payload(**locals(), exclude=['thumb']) + + files = {} + prepare_file(payload, files, 'thumb', thumb) + + result = await self.request(api.Methods.SET_STICKER_SET_THUMB, payload, files) + return result + async def answer_inline_query(self, inline_query_id: base.String, results: typing.List[types.InlineQueryResult], cache_time: typing.Union[base.Integer, None] = None, From 6cc769ce4f22234dadd021f7f46228242b99a2d5 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 19:13:33 +0300 Subject: [PATCH 116/165] #289 Added setStickerSetThumb test --- tests/test_bot.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 70f8eb89..92a2bd0f 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -516,3 +516,12 @@ async def test_edit_message_text_by_user(bot: Bot, event_loop): result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id) assert isinstance(result, bool) assert result is True + + +async def test_set_sticker_set_thumb(bot: Bot, event_loop): + """ setStickerSetThumb method test """ + + async with FakeTelegram(message_data=True, loop=event_loop): + result = await bot.set_sticker_set_thumb(name='test', user_id=123456789, thumb='file_id') + assert isinstance(result, bool) + assert result is True From 683e2befb560c9d490f733025510233f5fd74a1b Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 5 Apr 2020 19:55:53 +0300 Subject: [PATCH 117/165] #289 Added BotCammand and Dice to __all__ types --- aiogram/types/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 693bd7d5..2cd15ac6 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -71,6 +71,7 @@ __all__ = ( 'Animation', 'Audio', 'AuthWidgetData', + 'BotCommand', 'CallbackGame', 'CallbackQuery', 'Chat', @@ -83,6 +84,7 @@ __all__ = ( 'Contact', 'ContentType', 'ContentTypes', + 'Dice', 'Document', 'EncryptedCredentials', 'EncryptedPassportElement', From 79f62f9e613e91f53c646ac2790bc41d58552f3c Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Mon, 6 Apr 2020 18:20:39 +0500 Subject: [PATCH 118/165] Check whether Python is 3.7+ --- aiogram/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 169eed5f..415abb78 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,3 +1,8 @@ +import sys +if sys.version_info < (3, 7): + raise ImportError('Your Python version {0} is not supported by aiogram, please install ' + 'Python 3.7+'.format('.'.join(map(str, sys.version_info[:3])))) + import asyncio import os From 97bda44718397e050e68f9f64516d4d79ce032d0 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Mon, 6 Apr 2020 23:59:37 +0300 Subject: [PATCH 119/165] #289 BotCommand type init fix --- aiogram/types/bot_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/types/bot_command.py b/aiogram/types/bot_command.py index a97238e5..39e38e4f 100644 --- a/aiogram/types/bot_command.py +++ b/aiogram/types/bot_command.py @@ -10,3 +10,6 @@ class BotCommand(base.TelegramObject): """ command: base.String = fields.Field() description: base.String = fields.Field() + + def __init__(self, command: base.String, description: base.String): + super(BotCommand, self).__init__(command=command, description=description) \ No newline at end of file From 27e02e007c9f658a069cce2b65c214e6281503cf Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 7 Apr 2020 00:59:59 +0300 Subject: [PATCH 120/165] Bump version --- README.md | 2 +- README.rst | 2 +- aiogram/__init__.py | 4 ++-- aiogram/bot/api.py | 2 +- docs/source/index.rst | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6af84f0b..a4c5b5fc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.7-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index 39c5ac25..6d5d1f69 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.7-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 169eed5f..aabf331e 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.6.1' -__api_version__ = '4.6' +__version__ = '2.7' +__api_version__ = '4.7' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index f0553644..49ad849b 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.6 + List is updated to Bot API 4.7 """ mode = HelperMode.lowerCamelCase diff --git a/docs/source/index.rst b/docs/source/index.rst index 71ed04d9..fb1c9595 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.7-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From 8cd781048ab598254720ffd3806a930d2b2b0ab2 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 7 Apr 2020 09:36:16 +0300 Subject: [PATCH 121/165] #296 Get bot's user_id without get_me --- aiogram/bot/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 6750e8a8..b7015881 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -61,6 +61,7 @@ class BaseBot: api.check_token(token) self._token = None self.__token = token + self.id = int(token.split(sep=':')[0]) self.proxy = proxy self.proxy_auth = proxy_auth From cd75d3c4685e771e1ed9bf7a57cd4e93d7a8e345 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 7 Apr 2020 09:43:11 +0300 Subject: [PATCH 122/165] #296 Added test for getting bot's user_id --- tests/__init__.py | 3 ++- tests/test_bot.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 3a85ce3f..920d5663 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,7 +2,8 @@ import aresponses from aiogram import Bot -TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' +BOT_ID = 123456789 +TOKEN = f'{BOT_ID}:AABBCCDDEEFFaabbccddeeff-1234567890' class FakeTelegram(aresponses.ResponsesMockServer): diff --git a/tests/test_bot.py b/tests/test_bot.py index 92a2bd0f..cf1c3c3b 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1,7 +1,7 @@ import pytest from aiogram import Bot, types -from . import FakeTelegram, TOKEN +from . import FakeTelegram, TOKEN, BOT_ID pytestmark = pytest.mark.asyncio @@ -525,3 +525,9 @@ async def test_set_sticker_set_thumb(bot: Bot, event_loop): result = await bot.set_sticker_set_thumb(name='test', user_id=123456789, thumb='file_id') assert isinstance(result, bool) assert result is True + + +async def test_bot_id(bot: Bot): + """ Check getting id from token. """ + bot = Bot(TOKEN) + assert bot.id == BOT_ID # BOT_ID is a correct id from TOKEN From 43a210e21f9faa64f0915135582c84be708630bf Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 7 Apr 2020 12:55:24 +0300 Subject: [PATCH 123/165] Updated telegram version shield --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6af84f0b..a4c5b5fc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.7-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) From e4c1f4acc85c392bab99912c8dd887736b01c434 Mon Sep 17 00:00:00 2001 From: culnaen <54394508+culnaen@users.noreply.github.com> Date: Wed, 8 Apr 2020 18:30:12 +0300 Subject: [PATCH 124/165] fix --- aiogram/dispatcher/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index a717b486..ed2ebf99 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -182,7 +182,7 @@ class WebhookRequestHandler(web.View): try: try: await waiter - except asyncio.futures.CancelledError: + except asyncio.CancelledError: fut.remove_done_callback(cb) fut.cancel() raise From 2ff880bb5f9894828044403a65307b8a5b7f7e05 Mon Sep 17 00:00:00 2001 From: LimiO Date: Sat, 18 Apr 2020 16:09:45 +0300 Subject: [PATCH 125/165] add answer_dice method --- aiogram/types/message.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index d50eaf28..ca46c1ef 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -810,6 +810,35 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) + async def answer_dice(self, disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: + """ + Use this method to send a dice, which will have a random value from 1 to 6. + On success, the sent Message is returned. + (Yes, we're aware of the “proper” singular of die. + But it's awkward, and we decided to help it change. One dice at a time!) + + Source: https://core.telegram.org/bots/api#senddice + + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_dice(chat_id=self.chat.id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + async def reply(self, text: base.String, parse_mode: typing.Union[base.String, None] = None, disable_web_page_preview: typing.Union[base.Boolean, None] = None, From a7a06b730180ac50f45f53fc96db38827142e1d8 Mon Sep 17 00:00:00 2001 From: LimiO Date: Sat, 18 Apr 2020 17:40:26 +0300 Subject: [PATCH 126/165] Add reply_dice method --- aiogram/types/message.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index ca46c1ef..33b9d8ad 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1381,6 +1381,35 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) + async def reply_dice(self, disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: + """ + Use this method to send a dice, which will have a random value from 1 to 6. + On success, the sent Message is returned. + (Yes, we're aware of the “proper” singular of die. + But it's awkward, and we decided to help it change. One dice at a time!) + + Source: https://core.telegram.org/bots/api#senddice + + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_dice(chat_id=self.chat.id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + async def forward(self, chat_id: typing.Union[base.Integer, base.String], disable_notification: typing.Union[base.Boolean, None] = None) -> Message: """ From e50f9e5269c4d628606b7425f933e90f8beadd46 Mon Sep 17 00:00:00 2001 From: LimiO Date: Sat, 18 Apr 2020 17:47:03 +0300 Subject: [PATCH 127/165] Change False to True at reply_dice --- 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 33b9d8ad..4dd853ac 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1386,7 +1386,7 @@ class Message(base.TelegramObject): ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + reply: base.Boolean = True) -> Message: """ Use this method to send a dice, which will have a random value from 1 to 6. On success, the sent Message is returned. From 4fafba17b91b26e5aac8e5f2d8d2dfaab2e260b1 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 24 Apr 2020 20:09:36 +0200 Subject: [PATCH 128/165] Add emoji to send_dice method --- aiogram/bot/bot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 10b7ef7c..0953edc0 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -918,6 +918,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_dice(self, chat_id: typing.Union[base.Integer, base.String], disable_notification: typing.Union[base.Boolean, None] = None, + emoji: typing.Union[base.String, None] = None, reply_to_message_id: typing.Union[base.Integer, None] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, @@ -933,6 +934,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of “🎲” or “🎯”. Defauts to “🎲” + :type emoji: :obj:`typing.Union[base.String, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Union[base.Boolean, None]` :param reply_to_message_id: If the message is a reply, ID of the original message From cc650047dffd5c6a9f62539ed9e9203f0776007a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 17:12:03 +0300 Subject: [PATCH 129/165] i-AIOG-12: Add explanation and explanation_parse_mode in the sendPoll --- aiogram/bot/bot.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 0953edc0..2d0e3994 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -870,6 +870,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): type: typing.Optional[base.String] = None, allows_multiple_answers: typing.Optional[base.Boolean] = None, correct_option_id: typing.Optional[base.Integer] = None, + explanation: typing.Optional[base.String] = None, + explanation_parse_mode: typing.Optional[base.String] = None, is_closed: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -888,17 +890,21 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param question: Poll question, 1-255 characters :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]` + :type 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]` + :type is_anonymous: :obj:`typing.Optional[base.Boolean]` :param type: Poll type, “quiz” or “regular”, defaults to “regular” - :param type: :obj:`typing.Optional[base.String]` + :type 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]` + :type 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]` + :type correct_option_id: :obj:`typing.Optional[base.Integer]` + :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing + :type explanation: :obj:`typing.Optional[base.String]` + :param explanation_parse_mode: Mode for parsing entities in the explanation. See formatting options for more details. + :type explanation_parse_mode: :obj:`typing.Optional[base.String]` :param is_closed: Pass True, if the poll needs to be immediately closed - :param is_closed: :obj:`typing.Optional[base.Boolean]` + :type 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 From b42ea0f2ceb7483fedf8d4f991203460539fdc22 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 17:22:13 +0300 Subject: [PATCH 130/165] i-AIOG-12: Add default parse mode --- aiogram/bot/bot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 2d0e3994..7b007b97 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -918,6 +918,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ options = prepare_arg(options) payload = generate_payload(**locals()) + if self.parse_mode: + payload.setdefault('explanation_parse_mode', self.parse_mode) result = await self.request(api.Methods.SEND_POLL, payload) return types.Message(**result) From 7e3f83ebfe6e92ba3a30656d741d812dbccbc340 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 17:30:44 +0300 Subject: [PATCH 131/165] i-AIOG-13: Add explanation and explanation_entities to the Poll object --- aiogram/types/poll.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index 86b41d7e..9e1315ca 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -2,7 +2,9 @@ import typing from ..utils import helper from . import base, fields +from .message_entity import MessageEntity from .user import User +from ..utils.text_decorations import html_decoration, markdown_decoration class PollOption(base.TelegramObject): @@ -44,6 +46,31 @@ class Poll(base.TelegramObject): type: base.String = fields.Field() allows_multiple_answers: base.Boolean = fields.Field() correct_option_id: base.Integer = fields.Field() + explanation: base.String = fields.Field() + explanation_entities: base.String = fields.ListField(base=MessageEntity) + + def parse_entities(self, as_html=True): + text_decorator = html_decoration if as_html else markdown_decoration + + return text_decorator.unparse(self.explanation or '', self.explanation_entities or []) + + @property + def md_explanation(self) -> str: + """ + Explanation formatted as markdown. + + :return: str + """ + return self.parse_entities(False) + + @property + def html_explanation(self) -> str: + """ + Explanation formatted as HTML + + :return: str + """ + return self.parse_entities() class PollType(helper.Helper): From 8906609f0e44d4f805071abbc19b288e833da01e Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 17:39:35 +0300 Subject: [PATCH 132/165] i-AIOG-14: Add open_period and close_date in the method sendPoll --- aiogram/bot/bot.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 7b007b97..384e9221 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -872,6 +872,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): correct_option_id: typing.Optional[base.Integer] = None, explanation: typing.Optional[base.String] = None, explanation_parse_mode: typing.Optional[base.String] = None, + open_period: typing.Union[ + base.Integer, datetime.datetime, datetime.timedelta, None] = None, + close_date: typing.Union[ + base.Integer, datetime.datetime, datetime.timedelta, None] = None, is_closed: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, @@ -903,6 +907,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type explanation: :obj:`typing.Optional[base.String]` :param explanation_parse_mode: Mode for parsing entities in the explanation. See formatting options for more details. :type explanation_parse_mode: :obj:`typing.Optional[base.String]` + :param open_period: Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with close_date. + :type open_period: :obj:`typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None]` + :param close_date: Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with open_period. + :type close_date: :obj:`typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None]` :param is_closed: Pass True, if the poll needs to be immediately closed :type is_closed: :obj:`typing.Optional[base.Boolean]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. @@ -917,6 +925,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ options = prepare_arg(options) + open_period = prepare_arg(open_period) + close_date = prepare_arg(close_date) payload = generate_payload(**locals()) if self.parse_mode: payload.setdefault('explanation_parse_mode', self.parse_mode) From f5e4016bf83100fa5018e84b9fdc0cf5ebb4eaf5 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 17:48:23 +0300 Subject: [PATCH 133/165] i-AIOG-15: Add open_period and close_date to the Poll object --- aiogram/bot/bot.py | 5 ++--- aiogram/types/poll.py | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 384e9221..41f30af1 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -872,8 +872,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): correct_option_id: typing.Optional[base.Integer] = None, explanation: typing.Optional[base.String] = None, explanation_parse_mode: typing.Optional[base.String] = None, - open_period: typing.Union[ - base.Integer, datetime.datetime, datetime.timedelta, None] = None, + open_period: typing.Union[base.Integer, None] = None, close_date: typing.Union[ base.Integer, datetime.datetime, datetime.timedelta, None] = None, is_closed: typing.Optional[base.Boolean] = None, @@ -908,7 +907,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param explanation_parse_mode: Mode for parsing entities in the explanation. See formatting options for more details. :type explanation_parse_mode: :obj:`typing.Optional[base.String]` :param open_period: Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with close_date. - :type open_period: :obj:`typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None]` + :type open_period: :obj:`typing.Union[base.Integer, None]` :param close_date: Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with open_period. :type close_date: :obj:`typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None]` :param is_closed: Pass True, if the poll needs to be immediately closed diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index 9e1315ca..a709edcd 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -1,9 +1,10 @@ +import datetime import typing -from ..utils import helper from . import base, fields from .message_entity import MessageEntity from .user import User +from ..utils import helper from ..utils.text_decorations import html_decoration, markdown_decoration @@ -48,6 +49,8 @@ class Poll(base.TelegramObject): correct_option_id: base.Integer = fields.Field() explanation: base.String = fields.Field() explanation_entities: base.String = fields.ListField(base=MessageEntity) + open_period: base.Integer = fields.Field() + close_date: datetime.datetime = fields.DateTimeField() def parse_entities(self, as_html=True): text_decorator = html_decoration if as_html else markdown_decoration From 233c6767557492e22c5abf86090f2c51b892d11e Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 17:54:15 +0300 Subject: [PATCH 134/165] i-AIOG-17 Add emoji to the Dice object --- aiogram/types/__init__.py | 3 ++- aiogram/types/dice.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 2cd15ac6..1221ec72 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -12,7 +12,7 @@ from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .chosen_inline_result import ChosenInlineResult from .contact import Contact -from .dice import Dice +from .dice import Dice, DiceEmoji from .document import Document from .encrypted_credentials import EncryptedCredentials from .encrypted_passport_element import EncryptedPassportElement @@ -85,6 +85,7 @@ __all__ = ( 'ContentType', 'ContentTypes', 'Dice', + 'DiceEmoji', 'Document', 'EncryptedCredentials', 'EncryptedPassportElement', diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py index 9d0e22d0..121b90b4 100644 --- a/aiogram/types/dice.py +++ b/aiogram/types/dice.py @@ -9,5 +9,10 @@ class Dice(base.TelegramObject): https://core.telegram.org/bots/api#dice """ - + emoji: base.String = fields.Field() value: base.Integer = fields.Field() + + +class DiceEmoji: + DICE = '🎲' + DARTS = '🎯' From f2c7236ec16f38739f7142a7f81baa482353b9c3 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 22:52:48 +0300 Subject: [PATCH 135/165] i-AIOG-17: Rename dart constant --- aiogram/types/dice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py index 121b90b4..6dfb190f 100644 --- a/aiogram/types/dice.py +++ b/aiogram/types/dice.py @@ -15,4 +15,4 @@ class Dice(base.TelegramObject): class DiceEmoji: DICE = '🎲' - DARTS = '🎯' + DART = '🎯' From f1f65d2e776e9263e97ab76a17cecb0400f113d6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 22:54:44 +0300 Subject: [PATCH 136/165] i-AIOG-18 Update versions (readme, setup, etc.) --- README.md | 2 +- README.rst | 2 +- aiogram/__init__.py | 4 ++-- aiogram/bot/api.py | 2 +- docs/source/index.rst | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a4c5b5fc..74de8a8d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.7-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index 39c5ac25..78dc071c 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 169eed5f..f06827cd 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.6.1' -__api_version__ = '4.6' +__version__ = '2.8' +__api_version__ = '4.8' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index f0553644..3b341ec9 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.6 + List is updated to Bot API 4.8 """ mode = HelperMode.lowerCamelCase diff --git a/docs/source/index.rst b/docs/source/index.rst index 71ed04d9..b18d386e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.6-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From b0870b3f93d208662e4da4c9f7b9e4dc3d9efdaf Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 25 Apr 2020 23:11:52 +0300 Subject: [PATCH 137/165] i-AIOG-16 update send dice aliases --- aiogram/types/message.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 4dd853ac..ddbccde6 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -150,7 +150,6 @@ class Message(base.TelegramObject): if self.passport_data: return ContentType.PASSPORT_DATA - return ContentType.UNKNOWN def is_command(self): @@ -810,7 +809,8 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def answer_dice(self, disable_notification: typing.Union[base.Boolean, None] = None, + async def answer_dice(self, emoji: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, @@ -824,6 +824,8 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#senddice + :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of “🎲” or “🎯”. Defauts to “🎲” + :type emoji: :obj:`typing.Union[base.String, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Union[base.Boolean, None]` :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, @@ -836,6 +838,7 @@ class Message(base.TelegramObject): """ return await self.bot.send_dice(chat_id=self.chat.id, disable_notification=disable_notification, + emoji=emoji, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) @@ -1381,7 +1384,8 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def reply_dice(self, disable_notification: typing.Union[base.Boolean, None] = None, + async def reply_dice(self, emoji: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, @@ -1395,6 +1399,8 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#senddice + :param emoji: Emoji on which the dice throw animation is based. Currently, must be one of “🎲” or “🎯”. Defauts to “🎲” + :type emoji: :obj:`typing.Union[base.String, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Union[base.Boolean, None]` :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, From 60bcd88b681c76b0345ed0b5a2a2f85f89301c83 Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 3 May 2020 17:26:08 +0300 Subject: [PATCH 138/165] Fix IDFilter behavior when single str id passed --- aiogram/dispatcher/filters/builtin.py | 34 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 0a81998a..2334b343 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -12,6 +12,19 @@ from aiogram.dispatcher.filters.filters import BoundFilter, Filter from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType +IDFilterArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] + + +def extract_filter_ids(id_filter_argument: IDFilterArgumentType) -> typing.List[int]: + # since "str" is also an "Iterable", we have to check for it first + if isinstance(id_filter_argument, str): + return [int(id_filter_argument)] + if isinstance(id_filter_argument, Iterable): + return [int(item) for (item) in id_filter_argument] + # the last possible type is a single "int" + return [id_filter_argument] + + class Command(Filter): """ You can handle commands by using this filter. @@ -543,12 +556,11 @@ class ExceptionsFilter(BoundFilter): except: return False - class IDFilter(Filter): def __init__(self, - user_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None, - chat_id: Optional[Union[Iterable[Union[int, str]], str, int]] = None, + user_id: Optional[IDFilterArgumentType] = None, + chat_id: Optional[IDFilterArgumentType] = None, ): """ :param user_id: @@ -557,18 +569,14 @@ class IDFilter(Filter): if user_id is None and chat_id is None: raise ValueError("Both user_id and chat_id can't be None") - self.user_id = None - self.chat_id = None + self.user_id: Optional[IDFilterArgumentType] = None + self.chat_id: Optional[IDFilterArgumentType] = None + if user_id: - if isinstance(user_id, Iterable): - self.user_id = list(map(int, user_id)) - else: - self.user_id = [int(user_id), ] + self.user_id = extract_filter_ids(user_id) + if chat_id: - if isinstance(chat_id, Iterable): - self.chat_id = list(map(int, chat_id)) - else: - self.chat_id = [int(chat_id), ] + self.chat_id = extract_filter_ids(chat_id) @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: From ac7758aeb03bf186a701fde0dd2bce4254165d82 Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 3 May 2020 18:00:35 +0300 Subject: [PATCH 139/165] Make self.user_id and self.chat_id Set[int] in IDFilter --- aiogram/dispatcher/filters/builtin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 2334b343..2c7bcaea 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -15,14 +15,14 @@ from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType IDFilterArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] -def extract_filter_ids(id_filter_argument: IDFilterArgumentType) -> typing.List[int]: +def extract_filter_ids(id_filter_argument: IDFilterArgumentType) -> typing.Set[int]: # since "str" is also an "Iterable", we have to check for it first if isinstance(id_filter_argument, str): - return [int(id_filter_argument)] + return {int(id_filter_argument),} if isinstance(id_filter_argument, Iterable): - return [int(item) for (item) in id_filter_argument] + return {int(item) for (item) in id_filter_argument} # the last possible type is a single "int" - return [id_filter_argument] + return {id_filter_argument,} class Command(Filter): @@ -569,8 +569,8 @@ class IDFilter(Filter): if user_id is None and chat_id is None: raise ValueError("Both user_id and chat_id can't be None") - self.user_id: Optional[IDFilterArgumentType] = None - self.chat_id: Optional[IDFilterArgumentType] = None + self.user_id: Optional[typing.Set[int]] = None + self.chat_id: Optional[typing.Set[int]] = None if user_id: self.user_id = extract_filter_ids(user_id) From 65184fb126d6f4f4f97a5291bdc2deaaa24625b4 Mon Sep 17 00:00:00 2001 From: uburuntu Date: Tue, 5 May 2020 11:50:41 +0300 Subject: [PATCH 140/165] fix: change user context at poll_answer --- aiogram/dispatcher/dispatcher.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 950ce60f..b485fa49 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -236,6 +236,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): if update.poll: return await self.poll_handlers.notify(update.poll) if update.poll_answer: + types.User.set_current(update.poll_answer.user) return await self.poll_answer_handlers.notify(update.poll_answer) except Exception as e: err = await self.errors_handlers.notify(update, e) From 689a6cef65bafdf0696325d2a04123714fee673a Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Sat, 9 May 2020 08:59:41 +0300 Subject: [PATCH 141/165] Bug fix There is no method `delete_chat_description` --- aiogram/types/chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 0d3947f6..4a7287d8 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -178,7 +178,7 @@ class Chat(base.TelegramObject): :return: Returns True on success. :rtype: :obj:`base.Boolean` """ - return await self.bot.delete_chat_description(self.id, description) + return await self.bot.set_chat_description(self.id, description) async def kick(self, user_id: base.Integer, until_date: typing.Union[base.Integer, datetime.datetime, datetime.timedelta, None] = None) -> base.Boolean: From 6508235d16413d6b35e7a082ac53d41c8d94a689 Mon Sep 17 00:00:00 2001 From: mpa Date: Sun, 10 May 2020 01:13:00 +0400 Subject: [PATCH 142/165] fix(BaseBot): remove __del__ method from BaseBot implement "lazy" session property getter and new get_new_session for BaseBot --- aiogram/bot/base.py | 43 ++++++++++++++---------- tests/test_bot/test_session.py | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 tests/test_bot/test_session.py diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index b7015881..86347e88 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -5,7 +5,7 @@ import ssl import typing import warnings from contextvars import ContextVar -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, Type import aiohttp import certifi @@ -74,6 +74,12 @@ class BaseBot: # aiohttp main session ssl_context = ssl.create_default_context(cafile=certifi.where()) + self._session: Optional[aiohttp.ClientSession] = None + self._connector_class: Type[aiohttp.TCPConnector] = aiohttp.TCPConnector + self._connector_init = dict( + limit=connections_limit, ssl=ssl_context, loop=self.loop + ) + if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')): from aiohttp_socks import SocksConnector from aiohttp_socks.utils import parse_proxy_url @@ -85,30 +91,31 @@ class BaseBot: if not password: password = proxy_auth.password - connector = SocksConnector(socks_ver=socks_ver, host=host, port=port, - username=username, password=password, - limit=connections_limit, ssl_context=ssl_context, - rdns=True, loop=self.loop) - + self._connector_class = SocksConnector + self._connector_init.update( + socks_ver=socks_ver, host=host, port=port, + username=username, password=password, rdns=True, + ) self.proxy = None self.proxy_auth = None - else: - connector = aiohttp.TCPConnector(limit=connections_limit, ssl=ssl_context, loop=self.loop) + self._timeout = None self.timeout = timeout - self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, json_serialize=json.dumps) - self.parse_mode = parse_mode - def __del__(self): - if not hasattr(self, 'loop') or not hasattr(self, 'session'): - return - if self.loop.is_running(): - self.loop.create_task(self.close()) - return - loop = asyncio.new_event_loop() - loop.run_until_complete(self.close()) + def get_new_session(self) -> aiohttp.ClientSession: + return aiohttp.ClientSession( + connector=self._connector_class(**self._connector_init), + loop=self.loop, + json_serialize=json.dumps + ) + + @property + def session(self) -> Optional[aiohttp.ClientSession]: + if self._session is None or self._session.closed: + self._session = self.get_new_session() + return self._session @staticmethod def _prepare_timeout( diff --git a/tests/test_bot/test_session.py b/tests/test_bot/test_session.py new file mode 100644 index 00000000..dec6379c --- /dev/null +++ b/tests/test_bot/test_session.py @@ -0,0 +1,61 @@ +import aiohttp +import aiohttp_socks +import pytest + +from aiogram.bot.base import BaseBot + +try: + from asynctest import CoroutineMock, patch +except ImportError: + from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore + + +class TestAiohttpSession: + @pytest.mark.asyncio + async def test_create_bot(self): + bot = BaseBot(token="42:correct") + + assert bot._session is None + assert isinstance(bot._connector_init, dict) + assert all(key in {"limit", "ssl", "loop"} for key in bot._connector_init) + assert isinstance(bot._connector_class, type) + assert issubclass(bot._connector_class, aiohttp.TCPConnector) + + assert bot._session is None + + assert isinstance(bot.session, aiohttp.ClientSession) + assert bot.session == bot._session + + @pytest.mark.asyncio + async def test_create_proxy_bot(self): + socks_ver, host, port, username, password = ( + "socks5", "124.90.90.90", 9999, "login", "password" + ) + + bot = BaseBot( + token="42:correct", + proxy=f"{socks_ver}://{host}:{port}/", + proxy_auth=aiohttp.BasicAuth(username, password, "encoding"), + ) + + assert bot._connector_class == aiohttp_socks.SocksConnector + + assert isinstance(bot._connector_init, dict) + + init_kwargs = bot._connector_init + assert init_kwargs["username"] == username + assert init_kwargs["password"] == password + assert init_kwargs["host"] == host + assert init_kwargs["port"] == port + + @pytest.mark.asyncio + async def test_close_session(self): + bot = BaseBot(token="42:correct",) + aiohttp_client_0 = bot.session + + with patch("aiohttp.ClientSession.close", new=CoroutineMock()) as mocked_close: + await aiohttp_client_0.close() + mocked_close.assert_called_once() + + await aiohttp_client_0.close() + assert aiohttp_client_0 != bot.session # will create new session From 1e7f07f44326850b4fd2c2658c87833c91a23107 Mon Sep 17 00:00:00 2001 From: Daniil Kovalenko <40635760+WhiteMemory99@users.noreply.github.com> Date: Sun, 10 May 2020 22:40:37 +0700 Subject: [PATCH 143/165] Fix escaping issues in markdown.py --- aiogram/utils/markdown.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index d3c8583b..b56e14b1 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -70,7 +70,7 @@ def bold(*content, sep=" "): :return: """ return markdown_decoration.bold.format( - value=html_decoration.quote(_join(*content, sep=sep)) + value=markdown_decoration.quote(_join(*content, sep=sep)) ) @@ -96,7 +96,7 @@ def italic(*content, sep=" "): :return: """ return markdown_decoration.italic.format( - value=html_decoration.quote(_join(*content, sep=sep)) + value=markdown_decoration.quote(_join(*content, sep=sep)) ) @@ -122,7 +122,7 @@ def code(*content, sep=" "): :return: """ return markdown_decoration.code.format( - value=html_decoration.quote(_join(*content, sep=sep)) + value=markdown_decoration.quote(_join(*content, sep=sep)) ) @@ -148,7 +148,7 @@ def pre(*content, sep="\n"): :return: """ return markdown_decoration.pre.format( - value=html_decoration.quote(_join(*content, sep=sep)) + value=markdown_decoration.quote(_join(*content, sep=sep)) ) @@ -225,7 +225,7 @@ def link(title: str, url: str) -> str: :param url: :return: """ - return markdown_decoration.link.format(value=html_decoration.quote(title), link=url) + return markdown_decoration.link.format(value=markdown_decoration.quote(title), link=url) def hlink(title: str, url: str) -> str: From 5db245f0a6f17cf20e4bb81ed4eb244ed133b875 Mon Sep 17 00:00:00 2001 From: Evgeniy Baryshkov Date: Sun, 10 May 2020 20:14:35 +0300 Subject: [PATCH 144/165] fix: python-asyncio version conflict --- dev_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 40a74f81..1cb36784 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -4,7 +4,7 @@ ujson>=1.35 python-rapidjson>=0.7.0 emoji>=0.5.2 pytest>=4.4.1,<4.6 -pytest-asyncio>=0.10.0 +pytest-asyncio==0.10.0 tox>=3.9.0 aresponses>=1.1.1 uvloop>=0.12.2 From 0c149d8b2650576820f1ed61e56b2cc3c5a3ae20 Mon Sep 17 00:00:00 2001 From: Evgeniy Baryshkov Date: Sun, 10 May 2020 23:10:41 +0300 Subject: [PATCH 145/165] Update pytest --- dev_requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 1cb36784..c0c2a39d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -3,8 +3,8 @@ ujson>=1.35 python-rapidjson>=0.7.0 emoji>=0.5.2 -pytest>=4.4.1,<4.6 -pytest-asyncio==0.10.0 +pytest>=5.4 +pytest-asyncio>=0.10.0 tox>=3.9.0 aresponses>=1.1.1 uvloop>=0.12.2 From 15861960f5510f84ad33b48f6caacf812c07d9e9 Mon Sep 17 00:00:00 2001 From: Igor Sereda Date: Tue, 12 May 2020 00:32:45 +0300 Subject: [PATCH 146/165] Fix missing InlineQueryResultPhoto parse_mode field --- aiogram/types/inline_query_result.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py index a80352d7..cf26f8a4 100644 --- a/aiogram/types/inline_query_result.py +++ b/aiogram/types/inline_query_result.py @@ -92,12 +92,13 @@ class InlineQueryResultPhoto(InlineQueryResult): title: typing.Optional[base.String] = None, description: typing.Optional[base.String] = None, caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, reply_markup: typing.Optional[InlineKeyboardMarkup] = None, input_message_content: typing.Optional[InputMessageContent] = None): super(InlineQueryResultPhoto, self).__init__(id=id, photo_url=photo_url, thumb_url=thumb_url, photo_width=photo_width, photo_height=photo_height, title=title, description=description, caption=caption, - reply_markup=reply_markup, + parse_mode=parse_mode, reply_markup=reply_markup, input_message_content=input_message_content) From eb48319f3a39bfdab395b186df4304d658d11e7e Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 17 May 2020 16:22:33 +0300 Subject: [PATCH 147/165] Change name for chat id type and helper to extract it --- aiogram/dispatcher/filters/builtin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 2c7bcaea..9a773e2d 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -12,10 +12,10 @@ from aiogram.dispatcher.filters.filters import BoundFilter, Filter from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType -IDFilterArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] +ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] -def extract_filter_ids(id_filter_argument: IDFilterArgumentType) -> typing.Set[int]: +def extract_chat_ids(id_filter_argument: ChatIDArgumentType) -> typing.Set[int]: # since "str" is also an "Iterable", we have to check for it first if isinstance(id_filter_argument, str): return {int(id_filter_argument),} @@ -559,8 +559,8 @@ class ExceptionsFilter(BoundFilter): class IDFilter(Filter): def __init__(self, - user_id: Optional[IDFilterArgumentType] = None, - chat_id: Optional[IDFilterArgumentType] = None, + user_id: Optional[ChatIDArgumentType] = None, + chat_id: Optional[ChatIDArgumentType] = None, ): """ :param user_id: @@ -573,10 +573,10 @@ class IDFilter(Filter): self.chat_id: Optional[typing.Set[int]] = None if user_id: - self.user_id = extract_filter_ids(user_id) + self.user_id = extract_chat_ids(user_id) if chat_id: - self.chat_id = extract_filter_ids(chat_id) + self.chat_id = extract_chat_ids(chat_id) @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: From 6d53463880e6b1b8acf02f534b3eb926ac1d478f Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 17 May 2020 16:27:50 +0300 Subject: [PATCH 148/165] Refactor AdminFilter to use extract_chat_ids helper as in IDFilter --- aiogram/dispatcher/filters/builtin.py | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 9a773e2d..a01bac7b 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -15,14 +15,14 @@ from aiogram.types import CallbackQuery, Message, InlineQuery, Poll, ChatType ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] -def extract_chat_ids(id_filter_argument: ChatIDArgumentType) -> typing.Set[int]: +def extract_chat_ids(chat_id: ChatIDArgumentType) -> typing.Set[int]: # since "str" is also an "Iterable", we have to check for it first - if isinstance(id_filter_argument, str): - return {int(id_filter_argument),} - if isinstance(id_filter_argument, Iterable): - return {int(item) for (item) in id_filter_argument} + if isinstance(chat_id, str): + return {int(chat_id), } + if isinstance(chat_id, Iterable): + return {int(item) for (item) in chat_id} # the last possible type is a single "int" - return {id_filter_argument,} + return {chat_id, } class Command(Filter): @@ -622,22 +622,20 @@ class AdminFilter(Filter): is_chat_admin is required for InlineQuery. """ - def __init__(self, is_chat_admin: Optional[Union[Iterable[Union[int, str]], str, int, bool]] = None): + def __init__(self, is_chat_admin: Optional[Union[ChatIDArgumentType, bool]] = None): self._check_current = False self._chat_ids = None if is_chat_admin is False: raise ValueError("is_chat_admin cannot be False") - if is_chat_admin: - if isinstance(is_chat_admin, bool): - self._check_current = is_chat_admin - if isinstance(is_chat_admin, Iterable): - self._chat_ids = list(is_chat_admin) - else: - self._chat_ids = [is_chat_admin] - else: + if not is_chat_admin: self._check_current = True + return + + if isinstance(is_chat_admin, bool): + self._check_current = is_chat_admin + self._chat_ids = extract_chat_ids(is_chat_admin) @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: From 6b1c7d3b36a09990b3cb01175b42b1b388ecf015 Mon Sep 17 00:00:00 2001 From: Senpos Date: Sun, 17 May 2020 16:56:11 +0300 Subject: [PATCH 149/165] Add tests for extract_chat_ids --- .../test_filters/test_builtin.py | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py index 86344cec..a26fc139 100644 --- a/tests/test_dispatcher/test_filters/test_builtin.py +++ b/tests/test_dispatcher/test_filters/test_builtin.py @@ -1,6 +1,12 @@ +from typing import Set + import pytest -from aiogram.dispatcher.filters.builtin import Text +from aiogram.dispatcher.filters.builtin import ( + Text, + extract_chat_ids, + ChatIDArgumentType, +) class TestText: @@ -16,3 +22,50 @@ class TestText: config = {param: value} res = Text.validate(config) assert res == {key: value} + + +@pytest.mark.parametrize( + ('chat_id', 'expected'), + ( + pytest.param('-64856280', {-64856280,}, id='single negative int as string'), + pytest.param('64856280', {64856280,}, id='single positive int as string'), + pytest.param(-64856280, {-64856280,}, id='single negative int'), + pytest.param(64856280, {64856280,}, id='single positive negative int'), + pytest.param( + ['-64856280'], {-64856280,}, id='list of single negative int as string' + ), + pytest.param([-64856280], {-64856280,}, id='list of single negative int'), + pytest.param( + ['-64856280', '-64856280'], + {-64856280,}, + id='list of two duplicated negative ints as strings', + ), + pytest.param( + ['-64856280', -64856280], + {-64856280,}, + id='list of one negative int as string and one negative int', + ), + pytest.param( + [-64856280, -64856280], + {-64856280,}, + id='list of two duplicated negative ints', + ), + pytest.param( + iter(['-64856280']), + {-64856280,}, + id='iterator from a list of single negative int as string', + ), + pytest.param( + [10000000, 20000000, 30000000], + {10000000, 20000000, 30000000}, + id='list of several positive ints', + ), + pytest.param( + [10000000, '20000000', -30000000], + {10000000, 20000000, -30000000}, + id='list of positive int, positive int as string, negative int', + ), + ), +) +def test_extract_chat_ids(chat_id: ChatIDArgumentType, expected: Set[int]): + assert extract_chat_ids(chat_id) == expected From 5bbb36372afa93ed3f2acdd4980e0e0f8396179f Mon Sep 17 00:00:00 2001 From: evgfilim1 Date: Sun, 17 May 2020 19:34:24 +0500 Subject: [PATCH 150/165] Small whitespace fix --- aiogram/dispatcher/filters/builtin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index a01bac7b..5fe01dde 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -556,8 +556,8 @@ class ExceptionsFilter(BoundFilter): except: return False -class IDFilter(Filter): +class IDFilter(Filter): def __init__(self, user_id: Optional[ChatIDArgumentType] = None, chat_id: Optional[ChatIDArgumentType] = None, From 70767111c4ca74961103eae0b39f09f64dd62026 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 31 May 2020 17:49:33 +0300 Subject: [PATCH 151/165] fix: add support init fields from parent object in KeyboardButton (#344) * fix: add support init fields from parent object in KeyboardButton * fix: add tests --- aiogram/types/reply_keyboard.py | 6 ++++-- tests/types/dataset.py | 5 +++++ tests/types/test_reply_keyboard.py | 12 ++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 tests/types/test_reply_keyboard.py diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index ced20417..ffe07ae1 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -111,11 +111,13 @@ class KeyboardButton(base.TelegramObject): def __init__(self, text: base.String, request_contact: base.Boolean = None, request_location: base.Boolean = None, - request_poll: KeyboardButtonPollType = None): + request_poll: KeyboardButtonPollType = None, + **kwargs): super(KeyboardButton, self).__init__(text=text, request_contact=request_contact, request_location=request_location, - request_poll=request_poll) + request_poll=request_poll, + **kwargs) class ReplyKeyboardRemove(base.TelegramObject): diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 310024cb..739e8e2c 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -457,3 +457,8 @@ WEBHOOK_INFO = { "has_custom_certificate": False, "pending_update_count": 0, } + +REPLY_KEYBOARD_MARKUP = { + "keyboard": [[{"text": "something here"}]], + "resize_keyboard": True, +} diff --git a/tests/types/test_reply_keyboard.py b/tests/types/test_reply_keyboard.py new file mode 100644 index 00000000..ae0b6d9e --- /dev/null +++ b/tests/types/test_reply_keyboard.py @@ -0,0 +1,12 @@ +from aiogram import types +from .dataset import REPLY_KEYBOARD_MARKUP + +reply_keyboard = types.ReplyKeyboardMarkup(**REPLY_KEYBOARD_MARKUP) + + +def test_serialize(): + assert reply_keyboard.to_python() == REPLY_KEYBOARD_MARKUP + + +def test_deserialize(): + assert reply_keyboard.to_object(reply_keyboard.to_python()) == reply_keyboard From de13dbf454826f5e2b552db3730605c2f10dc489 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 8 Jun 2020 20:19:05 +0300 Subject: [PATCH 152/165] AIOG-T-61 Telegram Bot API 4.9 --- aiogram/types/dice.py | 1 + aiogram/types/inline_query_result.py | 2 ++ aiogram/types/message.py | 1 + 3 files changed, 4 insertions(+) diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py index 6dfb190f..7b3f1727 100644 --- a/aiogram/types/dice.py +++ b/aiogram/types/dice.py @@ -16,3 +16,4 @@ class Dice(base.TelegramObject): class DiceEmoji: DICE = '🎲' DART = '🎯' + BASKETBALL = '🏀' diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py index cf26f8a4..fccaa2a1 100644 --- a/aiogram/types/inline_query_result.py +++ b/aiogram/types/inline_query_result.py @@ -118,6 +118,7 @@ class InlineQueryResultGif(InlineQueryResult): gif_height: base.Integer = fields.Field() gif_duration: base.Integer = fields.Field() thumb_url: base.String = fields.Field() + thumb_mime_type: base.String = fields.Field() title: base.String = fields.Field() caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) @@ -157,6 +158,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): mpeg4_height: base.Integer = fields.Field() mpeg4_duration: base.Integer = fields.Field() thumb_url: base.String = fields.Field() + thumb_mime_type: base.String = fields.Field() title: base.String = fields.Field() caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index ddbccde6..9b9c0f82 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -51,6 +51,7 @@ class Message(base.TelegramObject): forward_signature: base.String = fields.Field() forward_date: datetime.datetime = fields.DateTimeField() reply_to_message: Message = fields.Field(base='Message') + via_bot: User = fields.Field(base=User) edit_date: datetime.datetime = fields.DateTimeField() media_group_id: base.String = fields.Field() author_signature: base.String = fields.Field() From 50b5768759102d847efe9381dcd358de30d49cc2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 8 Jun 2020 20:20:43 +0300 Subject: [PATCH 153/165] AIOG-T-61 Bump version --- README.md | 2 +- README.rst | 2 +- aiogram/__init__.py | 4 ++-- aiogram/bot/api.py | 2 +- docs/source/index.rst | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 74de8a8d..dfef918f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) -[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/aiogram?style=flat-square)](http://docs.aiogram.dev/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index 78dc071c..1cf2765d 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/aiogram/__init__.py b/aiogram/__init__.py index cb339a25..b077ae36 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = [ 'utils' ] -__version__ = '2.8' -__api_version__ = '4.8' +__version__ = '2.9' +__api_version__ = '4.9' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 3b341ec9..1d0c4f7b 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -153,7 +153,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 4.8 + List is updated to Bot API 4.9 """ mode = HelperMode.lowerCamelCase diff --git a/docs/source/index.rst b/docs/source/index.rst index b18d386e..0ac6eccd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,7 +22,7 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.8-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.9-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From 027139f1b2b7dc553012a4fe2d39f16b0f354d0b Mon Sep 17 00:00:00 2001 From: George Imedashvili Date: Mon, 8 Jun 2020 18:21:15 +0100 Subject: [PATCH 154/165] fix get_full_command (#348) The reason is that .partition() doesn't have a default param as .split has, and default split param gives possibility to split not only by whitespaces, but also whitespace consequences (so the .strip() in get_args() not needed) and newlines. It's called "fix", because without it, commands like this: '''/command arg arg1''' are resulting with ('/command\narg\narg1', '', '') --- aiogram/types/message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 9b9c0f82..b9452967 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -168,7 +168,7 @@ class Message(base.TelegramObject): :return: tuple of (command, args) """ if self.is_command(): - command, _, args = self.text.partition(' ') + command, args = self.text.split(maxsplit=1) return command, args def get_command(self, pure=False): @@ -192,7 +192,7 @@ class Message(base.TelegramObject): """ command = self.get_full_command() if command: - return command[1].strip() + return command[1] def parse_entities(self, as_html=True): """ From d5169a294f03c63e62589de2566725b8b7fcc08d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 8 Jun 2020 20:42:38 +0300 Subject: [PATCH 155/165] AIOG-T-23 Backport text_decorations from 3.0a --- aiogram/utils/text_decorations.py | 192 +++++++++++++++++++----------- 1 file changed, 123 insertions(+), 69 deletions(-) diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index ad52c9d7..77dc0ff3 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -1,33 +1,23 @@ from __future__ import annotations + import html import re -import struct -from dataclasses import dataclass -from typing import TYPE_CHECKING, AnyStr, Callable, Generator, Iterable, List, Optional +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Generator, List, Optional, Pattern, cast -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from aiogram.types import MessageEntity __all__ = ( "TextDecoration", + "HtmlDecoration", + "MarkdownDecoration", "html_decoration", "markdown_decoration", - "add_surrogate", - "remove_surrogate", ) -@dataclass -class TextDecoration: - link: str - bold: str - italic: str - code: str - pre: str - underline: str - strikethrough: str - quote: Callable[[AnyStr], AnyStr] - +class TextDecoration(ABC): def apply_entity(self, entity: MessageEntity, text: str) -> str: """ Apply single entity to text @@ -36,24 +26,28 @@ class TextDecoration: :param text: :return: """ - if entity.type in ( - "bold", - "italic", - "code", - "pre", - "underline", - "strikethrough", - ): - return getattr(self, entity.type).format(value=text) - elif entity.type == "text_mention": - return self.link.format(value=text, link=f"tg://user?id={entity.user.id}") - elif entity.type == "text_link": - return self.link.format(value=text, link=entity.url) - elif entity.type == "url": + if entity.type in {"bot_command", "url", "mention", "phone_number"}: + # This entities should not be changed return text + if entity.type in {"bold", "italic", "code", "underline", "strikethrough"}: + return cast(str, getattr(self, entity.type)(value=text)) + if entity.type == "pre": + return ( + self.pre_language(value=text, language=entity.language) + if entity.language + else self.pre(value=text) + ) + if entity.type == "text_mention": + from aiogram.types import User + + user = cast(User, entity.user) + return self.link(value=text, link=f"tg://user?id={user.id}") + if entity.type == "text_link": + return self.link(value=text, link=cast(str, entity.url)) + return self.quote(text) - def unparse(self, text, entities: Optional[List[MessageEntity]] = None) -> str: + def unparse(self, text: str, entities: Optional[List[MessageEntity]] = None) -> str: """ Unparse message entities @@ -61,22 +55,22 @@ class TextDecoration: :param entities: Array of MessageEntities :return: """ - text = add_surrogate(text) result = "".join( self._unparse_entities( text, sorted(entities, key=lambda item: item.offset) if entities else [] ) ) - return remove_surrogate(result) + return result def _unparse_entities( self, text: str, - entities: Iterable[MessageEntity], + entities: List[MessageEntity], offset: Optional[int] = None, length: Optional[int] = None, ) -> Generator[str, None, None]: - offset = offset or 0 + if offset is None: + offset = 0 length = length or len(text) for index, entity in enumerate(entities): @@ -88,7 +82,7 @@ class TextDecoration: offset = entity.offset + entity.length sub_entities = list( - filter(lambda e: e.offset < offset, entities[index + 1 :]) + filter(lambda e: e.offset < (offset or 0), entities[index + 1 :]) ) yield self.apply_entity( entity, @@ -102,42 +96,102 @@ class TextDecoration: if offset < length: yield self.quote(text[offset:length]) + @abstractmethod + def link(self, value: str, link: str) -> str: # pragma: no cover + pass -html_decoration = TextDecoration( - link='{value}', - bold="{value}", - italic="{value}", - code="{value}", - pre="
{value}
", - underline="{value}", - strikethrough="{value}", - quote=html.escape, -) + @abstractmethod + def bold(self, value: str) -> str: # pragma: no cover + pass -MARKDOWN_QUOTE_PATTERN = re.compile(r"([_*\[\]()~`>#+\-=|{}.!])") + @abstractmethod + def italic(self, value: str) -> str: # pragma: no cover + pass -markdown_decoration = TextDecoration( - link="[{value}]({link})", - bold="*{value}*", - italic="_{value}_\r", - code="`{value}`", - pre="```{value}```", - underline="__{value}__", - strikethrough="~{value}~", - quote=lambda text: re.sub( - pattern=MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=text - ), -) + @abstractmethod + def code(self, value: str) -> str: # pragma: no cover + pass + + @abstractmethod + def pre(self, value: str) -> str: # pragma: no cover + pass + + @abstractmethod + def pre_language(self, value: str, language: str) -> str: # pragma: no cover + pass + + @abstractmethod + def underline(self, value: str) -> str: # pragma: no cover + pass + + @abstractmethod + def strikethrough(self, value: str) -> str: # pragma: no cover + pass + + @abstractmethod + def quote(self, value: str) -> str: # pragma: no cover + pass -def add_surrogate(text: str) -> str: - return "".join( - "".join(chr(d) for d in struct.unpack(" str: + return f'{value}' + + def bold(self, value: str) -> str: + return f"{value}" + + def italic(self, value: str) -> str: + return f"{value}" + + def code(self, value: str) -> str: + return f"{value}" + + def pre(self, value: str) -> str: + return f"
{value}
" + + def pre_language(self, value: str, language: str) -> str: + return f'
{value}
' + + def underline(self, value: str) -> str: + return f"{value}" + + def strikethrough(self, value: str) -> str: + return f"{value}" + + def quote(self, value: str) -> str: + return html.escape(value) -def remove_surrogate(text: str) -> str: - return text.encode("utf-16", "surrogatepass").decode("utf-16") +class MarkdownDecoration(TextDecoration): + MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-|{}.!])") + + def link(self, value: str, link: str) -> str: + return f"[{value}]({link})" + + def bold(self, value: str) -> str: + return f"*{value}*" + + def italic(self, value: str) -> str: + return f"_{value}_\r" + + def code(self, value: str) -> str: + return f"`{value}`" + + def pre(self, value: str) -> str: + return f"```{value}```" + + def pre_language(self, value: str, language: str) -> str: + return f"```{language}\n{value}\n```" + + def underline(self, value: str) -> str: + return f"__{value}__" + + def strikethrough(self, value: str) -> str: + return f"~{value}~" + + def quote(self, value: str) -> str: + return re.sub(pattern=self.MARKDOWN_QUOTE_PATTERN, repl=r"\\\1", string=value) + + +html_decoration = HtmlDecoration() +markdown_decoration = MarkdownDecoration() From 557147ad8d39ec6d90f36a840c09b458a063e48f Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 9 Jun 2020 16:55:13 +0300 Subject: [PATCH 156/165] fix: markdown helper methods work correctly (#353) * fix: methods in markdown helper work now * chore: add return type annotations --- aiogram/utils/markdown.py | 56 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index b56e14b1..da27bc39 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -18,7 +18,7 @@ HTML_QUOTES_MAP = {"<": "<", ">": ">", "&": "&", '"': """} _HQS = HTML_QUOTES_MAP.keys() # HQS for HTML QUOTES SYMBOLS -def quote_html(*content, sep=" "): +def quote_html(*content, sep=" ") -> str: """ Quote HTML symbols @@ -33,7 +33,7 @@ def quote_html(*content, sep=" "): return html_decoration.quote(_join(*content, sep=sep)) -def escape_md(*content, sep=" "): +def escape_md(*content, sep=" ") -> str: """ Escape markdown text @@ -61,7 +61,7 @@ def text(*content, sep=" "): return _join(*content, sep=sep) -def bold(*content, sep=" "): +def bold(*content, sep=" ") -> str: """ Make bold text (Markdown) @@ -69,12 +69,12 @@ def bold(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.bold.format( + return markdown_decoration.bold( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hbold(*content, sep=" "): +def hbold(*content, sep=" ") -> str: """ Make bold text (HTML) @@ -82,12 +82,12 @@ def hbold(*content, sep=" "): :param sep: :return: """ - return html_decoration.bold.format( + return html_decoration.bold( value=html_decoration.quote(_join(*content, sep=sep)) ) -def italic(*content, sep=" "): +def italic(*content, sep=" ") -> str: """ Make italic text (Markdown) @@ -95,12 +95,12 @@ def italic(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.italic.format( + return markdown_decoration.italic( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hitalic(*content, sep=" "): +def hitalic(*content, sep=" ") -> str: """ Make italic text (HTML) @@ -108,12 +108,12 @@ def hitalic(*content, sep=" "): :param sep: :return: """ - return html_decoration.italic.format( + return html_decoration.italic( value=html_decoration.quote(_join(*content, sep=sep)) ) -def code(*content, sep=" "): +def code(*content, sep=" ") -> str: """ Make mono-width text (Markdown) @@ -121,12 +121,12 @@ def code(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.code.format( + return markdown_decoration.code( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hcode(*content, sep=" "): +def hcode(*content, sep=" ") -> str: """ Make mono-width text (HTML) @@ -134,12 +134,12 @@ def hcode(*content, sep=" "): :param sep: :return: """ - return html_decoration.code.format( + return html_decoration.code( value=html_decoration.quote(_join(*content, sep=sep)) ) -def pre(*content, sep="\n"): +def pre(*content, sep="\n") -> str: """ Make mono-width text block (Markdown) @@ -147,12 +147,12 @@ def pre(*content, sep="\n"): :param sep: :return: """ - return markdown_decoration.pre.format( + return markdown_decoration.pre( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hpre(*content, sep="\n"): +def hpre(*content, sep="\n") -> str: """ Make mono-width text block (HTML) @@ -160,12 +160,12 @@ def hpre(*content, sep="\n"): :param sep: :return: """ - return html_decoration.pre.format( + return html_decoration.pre( value=html_decoration.quote(_join(*content, sep=sep)) ) -def underline(*content, sep=" "): +def underline(*content, sep=" ") -> str: """ Make underlined text (Markdown) @@ -173,12 +173,12 @@ def underline(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.underline.format( + return markdown_decoration.underline( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hunderline(*content, sep=" "): +def hunderline(*content, sep=" ") -> str: """ Make underlined text (HTML) @@ -186,12 +186,12 @@ def hunderline(*content, sep=" "): :param sep: :return: """ - return html_decoration.underline.format( + return html_decoration.underline( value=html_decoration.quote(_join(*content, sep=sep)) ) -def strikethrough(*content, sep=" "): +def strikethrough(*content, sep=" ") -> str: """ Make strikethrough text (Markdown) @@ -199,12 +199,12 @@ def strikethrough(*content, sep=" "): :param sep: :return: """ - return markdown_decoration.strikethrough.format( + return markdown_decoration.strikethrough( value=markdown_decoration.quote(_join(*content, sep=sep)) ) -def hstrikethrough(*content, sep=" "): +def hstrikethrough(*content, sep=" ") -> str: """ Make strikethrough text (HTML) @@ -212,7 +212,7 @@ def hstrikethrough(*content, sep=" "): :param sep: :return: """ - return html_decoration.strikethrough.format( + return html_decoration.strikethrough( value=html_decoration.quote(_join(*content, sep=sep)) ) @@ -225,7 +225,7 @@ def link(title: str, url: str) -> str: :param url: :return: """ - return markdown_decoration.link.format(value=markdown_decoration.quote(title), link=url) + return markdown_decoration.link(value=markdown_decoration.quote(title), link=url) def hlink(title: str, url: str) -> str: @@ -236,7 +236,7 @@ def hlink(title: str, url: str) -> str: :param url: :return: """ - return html_decoration.link.format(value=html_decoration.quote(title), link=url) + return html_decoration.link(value=html_decoration.quote(title), link=url) def hide_link(url: str) -> str: From a8dfe86358837e909b1e192d7ba6c0ef57541a65 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 10 Jun 2020 23:07:55 +0300 Subject: [PATCH 157/165] feat: ForwardedMessage filter (#355) * feat: ForwardedMessage filter * fix: add tests * fix: attr name --- aiogram/dispatcher/dispatcher.py | 8 +++++- aiogram/dispatcher/filters/__init__.py | 3 ++- aiogram/dispatcher/filters/builtin.py | 10 +++++++ docs/source/dispatcher/filters.rst | 8 ++++++ .../test_filters/test_builtin.py | 27 ++++++++++++++++++- 5 files changed, 53 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index b485fa49..164d6aad 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -10,7 +10,7 @@ from aiohttp.helpers import sentinel from aiogram.utils.deprecated import renamed_argument from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \ - RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter + RegexpCommandsFilter, StateFilter, Text, IDFilter, AdminFilter, IsReplyFilter, ForwardedMessageFilter from .filters.builtin import IsSenderContact from .handler import Handler from .middlewares import MiddlewareManager @@ -160,6 +160,12 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.channel_post_handlers, self.edited_channel_post_handlers, ]) + filters_factory.bind(ForwardedMessageFilter, event_handlers=[ + self.message_handlers, + self.edited_channel_post_handlers, + self.channel_post_handlers, + self.edited_channel_post_handlers + ]) def __del__(self): self.stop_polling() diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 6de3cc7a..edd1959a 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,6 +1,6 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \ - Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact + Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact, ForwardedMessageFilter from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -32,4 +32,5 @@ __all__ = [ 'get_filters_spec', 'execute_filter', 'check_filters', + 'ForwardedMessageFilter', ] diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 5fe01dde..c59d9b0d 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -681,3 +681,13 @@ class IsReplyFilter(BoundFilter): return {'reply': msg.reply_to_message} elif not msg.reply_to_message and not self.is_reply: return True + + +class ForwardedMessageFilter(BoundFilter): + key = 'is_forwarded' + + def __init__(self, is_forwarded: bool): + self.is_forwarded = is_forwarded + + async def check(self, message: Message): + return bool(getattr(message, "forward_date")) is self.is_forwarded diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index af06b73e..3681dfcb 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -141,6 +141,14 @@ IsReplyFilter :show-inheritance: +ForwardedMessageFilter +------------- + +.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter + :members: + :show-inheritance: + + Making own filters (Custom filters) =================================== diff --git a/tests/test_dispatcher/test_filters/test_builtin.py b/tests/test_dispatcher/test_filters/test_builtin.py index a26fc139..4cfce465 100644 --- a/tests/test_dispatcher/test_filters/test_builtin.py +++ b/tests/test_dispatcher/test_filters/test_builtin.py @@ -1,12 +1,15 @@ from typing import Set +from datetime import datetime import pytest from aiogram.dispatcher.filters.builtin import ( Text, extract_chat_ids, - ChatIDArgumentType, + ChatIDArgumentType, ForwardedMessageFilter, ) +from aiogram.types import Message +from tests.types.dataset import MESSAGE class TestText: @@ -69,3 +72,25 @@ class TestText: ) def test_extract_chat_ids(chat_id: ChatIDArgumentType, expected: Set[int]): assert extract_chat_ids(chat_id) == expected + + +class TestForwardedMessageFilter: + async def test_filter_forwarded_messages(self): + filter = ForwardedMessageFilter(is_forwarded=True) + + forwarded_message = Message(forward_date=round(datetime(2020, 5, 21, 5, 1).timestamp()), **MESSAGE) + + not_forwarded_message = Message(**MESSAGE) + + assert await filter.check(forwarded_message) + assert not await filter.check(not_forwarded_message) + + async def test_filter_not_forwarded_messages(self): + filter = ForwardedMessageFilter(is_forwarded=False) + + forwarded_message = Message(forward_date=round(datetime(2020, 5, 21, 5, 1).timestamp()), **MESSAGE) + + not_forwarded_message = Message(**MESSAGE) + + assert await filter.check(not_forwarded_message) + assert not await filter.check(forwarded_message) From 1389ca587401fd46961397def659f72a4dc6bffd Mon Sep 17 00:00:00 2001 From: Denis Belavin <41421345+LuckyDenis@users.noreply.github.com> Date: Wed, 10 Jun 2020 23:08:44 +0300 Subject: [PATCH 158/165] #320 - Fix: Class InputMediaAudio contains some fields from other class. (#354) Co-authored-by: Belavin Denis --- aiogram/types/input_media.py | 7 ++---- tests/types/test_input_media.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 tests/types/test_input_media.py diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 952e7a55..d42fac99 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -137,8 +137,6 @@ class InputMediaAudio(InputMedia): https://core.telegram.org/bots/api#inputmediaanimation """ - width: base.Integer = fields.Field() - height: base.Integer = fields.Field() duration: base.Integer = fields.Field() performer: base.String = fields.Field() title: base.String = fields.Field() @@ -146,13 +144,12 @@ class InputMediaAudio(InputMedia): def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, caption: base.String = None, - width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None, performer: base.String = None, title: base.String = None, parse_mode: base.String = None, **kwargs): - super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption, - width=width, height=height, duration=duration, + super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, + caption=caption, duration=duration, performer=performer, title=title, parse_mode=parse_mode, conf=kwargs) diff --git a/tests/types/test_input_media.py b/tests/types/test_input_media.py new file mode 100644 index 00000000..953197c9 --- /dev/null +++ b/tests/types/test_input_media.py @@ -0,0 +1,42 @@ +from aiogram import types +from .dataset import AUDIO, ANIMATION, \ + DOCUMENT, PHOTO, VIDEO + + +WIDTH = 'width' +HEIGHT = 'height' + +input_media_audio = types.InputMediaAudio( + types.Audio(**AUDIO)) +input_media_animation = types.InputMediaAnimation( + types.Animation(**ANIMATION)) +input_media_document = types.InputMediaDocument( + types.Document(**DOCUMENT)) +input_media_video = types.InputMediaVideo( + types.Video(**VIDEO)) +input_media_photo = types.InputMediaPhoto( + types.PhotoSize(**PHOTO)) + + +def test_field_width(): + """ + https://core.telegram.org/bots/api#inputmedia + """ + assert not hasattr(input_media_audio, WIDTH) + assert not hasattr(input_media_document, WIDTH) + assert not hasattr(input_media_photo, WIDTH) + + assert hasattr(input_media_animation, WIDTH) + assert hasattr(input_media_video, WIDTH) + + +def test_field_height(): + """ + https://core.telegram.org/bots/api#inputmedia + """ + assert not hasattr(input_media_audio, HEIGHT) + assert not hasattr(input_media_document, HEIGHT) + assert not hasattr(input_media_photo, HEIGHT) + + assert hasattr(input_media_animation, HEIGHT) + assert hasattr(input_media_video, HEIGHT) From 8d30c1dc1b9a494178835ec09e389444cd6fcbbd Mon Sep 17 00:00:00 2001 From: Egor Dementyev Date: Sat, 13 Jun 2020 19:18:30 +0300 Subject: [PATCH 159/165] Fix message.get_full_command() (#352) * Fix message.get_full_command() * Fix message.get_full_command() by JrooTJunior Co-authored-by: Alex Root Junior * fix typos Co-authored-by: Alex Root Junior --- aiogram/types/message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index b9452967..2ea65bfb 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -168,7 +168,8 @@ class Message(base.TelegramObject): :return: tuple of (command, args) """ if self.is_command(): - command, args = self.text.split(maxsplit=1) + command, *args = self.text.split(maxsplit=1) + args = args[0] if args else None return command, args def get_command(self, pure=False): From 21b4b64db1ba5ed8465a32279cd2efd4165b2b44 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 13 Jun 2020 21:30:24 +0300 Subject: [PATCH 160/165] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index b077ae36..0dba109a 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = [ 'utils' ] -__version__ = '2.9' +__version__ = '2.9.1' __api_version__ = '4.9' From 09f3c35aec02a20669ead5144b21536fee4cfb99 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 13 Jun 2020 21:53:31 +0300 Subject: [PATCH 161/165] Hotfix get_full_command --- aiogram/types/message.py | 1327 +++++++++++++++++++++++--------------- 1 file changed, 797 insertions(+), 530 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 2ea65bfb..c56a143a 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -4,8 +4,10 @@ import datetime import functools import typing -from . import base -from . import fields +from ..utils import helper +from ..utils import markdown as md +from ..utils.text_decorations import html_decoration, markdown_decoration +from . import base, fields from .animation import Animation from .audio import Audio from .chat import Chat, ChatType @@ -15,14 +17,14 @@ from .document import Document from .force_reply import ForceReply from .game import Game from .inline_keyboard import InlineKeyboardMarkup -from .input_media import MediaGroup, InputMedia +from .input_media import InputMedia, MediaGroup from .invoice import Invoice from .location import Location from .message_entity import MessageEntity from .passport_data import PassportData from .photo_size import PhotoSize from .poll import Poll -from .reply_keyboard import ReplyKeyboardRemove, ReplyKeyboardMarkup +from .reply_keyboard import ReplyKeyboardMarkup, ReplyKeyboardRemove from .sticker import Sticker from .successful_payment import SuccessfulPayment from .user import User @@ -30,9 +32,6 @@ from .venue import Venue from .video import Video 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): @@ -41,8 +40,9 @@ class Message(base.TelegramObject): https://core.telegram.org/bots/api#message """ + message_id: base.Integer = fields.Field() - from_user: User = fields.Field(alias='from', base=User) + from_user: User = fields.Field(alias="from", base=User) date: datetime.datetime = fields.DateTimeField() chat: Chat = fields.Field(base=Chat) forward_from: User = fields.Field(base=User) @@ -50,7 +50,7 @@ class Message(base.TelegramObject): forward_from_message_id: base.Integer = fields.Field() forward_signature: base.String = fields.Field() forward_date: datetime.datetime = fields.DateTimeField() - reply_to_message: Message = fields.Field(base='Message') + reply_to_message: Message = fields.Field(base="Message") via_bot: User = fields.Field(base=User) edit_date: datetime.datetime = fields.DateTimeField() media_group_id: base.String = fields.Field() @@ -84,7 +84,7 @@ class Message(base.TelegramObject): channel_chat_created: base.Boolean = fields.Field() migrate_to_chat_id: base.Integer = fields.Field() migrate_from_chat_id: base.Integer = fields.Field() - pinned_message: Message = fields.Field(base='Message') + pinned_message: Message = fields.Field(base="Message") invoice: Invoice = fields.Field(base=Invoice) successful_payment: SuccessfulPayment = fields.Field(base=SuccessfulPayment) connected_website: base.String = fields.Field() @@ -159,7 +159,7 @@ class Message(base.TelegramObject): :return: bool """ - return self.text and self.text.startswith('/') + return self.text and self.text.startswith("/") def get_full_command(self): """ @@ -169,7 +169,7 @@ class Message(base.TelegramObject): """ if self.is_command(): command, *args = self.text.split(maxsplit=1) - args = args[0] if args else None + args = args[-1] if args else "" return command, args def get_command(self, pure=False): @@ -182,7 +182,7 @@ class Message(base.TelegramObject): if command: command = command[0] if pure: - command, _, _ = command[1:].partition('@') + command, _, _ = command[1:].partition("@") return command def get_args(self): @@ -237,16 +237,16 @@ class Message(base.TelegramObject): :return: str """ if ChatType.is_private(self.chat): - raise TypeError('Invalid chat type!') + raise TypeError("Invalid chat type!") - url = 'https://t.me/' + url = "https://t.me/" if self.chat.username: # Generates public link - url += f'{self.chat.username}/' + url += f"{self.chat.username}/" else: # Generates private link available for chat members - url += f'c/{self.chat.shifted_id}/' - url += f'{self.message_id}' + url += f"c/{self.chat.shifted_id}/" + url += f"{self.message_id}" return url @@ -269,15 +269,21 @@ class Message(base.TelegramObject): return md.hlink(text, url) return md.link(text, url) - async def answer(self, text: base.String, - parse_mode: typing.Union[base.String, None] = None, - disable_web_page_preview: typing.Union[base.Boolean, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer( + self, + text: base.String, + parse_mode: typing.Union[base.String, None] = None, + disable_web_page_preview: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Answer to this message @@ -299,23 +305,31 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_message(chat_id=self.chat.id, - text=text, - parse_mode=parse_mode, - disable_web_page_preview=disable_web_page_preview, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_message( + chat_id=self.chat.id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_photo(self, photo: typing.Union[base.InputFile, base.String], - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_photo( + self, + photo: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send photos. @@ -339,26 +353,34 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_photo(chat_id=self.chat.id, - photo=photo, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_photo( + chat_id=self.chat.id, + photo=photo, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_audio(self, audio: typing.Union[base.InputFile, base.String], - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - duration: typing.Union[base.Integer, None] = None, - performer: typing.Union[base.String, None] = None, - title: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_audio( + self, + audio: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + duration: typing.Union[base.Integer, None] = None, + performer: typing.Union[base.String, None] = None, + title: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .mp3 format. @@ -390,30 +412,38 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_audio(chat_id=self.chat.id, - audio=audio, - caption=caption, - parse_mode=parse_mode, - duration=duration, - performer=performer, - title=title, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_audio( + chat_id=self.chat.id, + audio=audio, + caption=caption, + parse_mode=parse_mode, + duration=duration, + performer=performer, + title=title, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_animation(self, animation: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - width: typing.Union[base.Integer, None] = None, - height: typing.Union[base.Integer, None] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_animation( + self, + animation: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -450,27 +480,35 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_animation(self.chat.id, - animation=animation, - duration=duration, - width=width, - height=height, - thumb=thumb, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_animation( + self.chat.id, + animation=animation, + duration=duration, + width=width, + height=height, + thumb=thumb, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_document(self, document: typing.Union[base.InputFile, base.String], - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_document( + self, + document: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send general files. @@ -495,26 +533,34 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_document(chat_id=self.chat.id, - document=document, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_document( + chat_id=self.chat.id, + document=document, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_video(self, video: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - width: typing.Union[base.Integer, None] = None, - height: typing.Union[base.Integer, None] = None, - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_video( + self, + video: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). @@ -544,27 +590,35 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_video(chat_id=self.chat.id, - video=video, - duration=duration, - width=width, - height=height, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_video( + chat_id=self.chat.id, + video=video, + duration=duration, + width=width, + height=height, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_voice(self, voice: typing.Union[base.InputFile, base.String], - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - duration: typing.Union[base.Integer, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_voice( + self, + voice: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + duration: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. @@ -593,24 +647,32 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_voice(chat_id=self.chat.id, - voice=voice, - caption=caption, - parse_mode=parse_mode, - duration=duration, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_voice( + chat_id=self.chat.id, + voice=voice, + caption=caption, + parse_mode=parse_mode, + duration=duration, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_video_note(self, video_note: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - length: typing.Union[base.Integer, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_video_note( + self, + video_note: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + length: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send video messages. @@ -633,17 +695,22 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_video_note(chat_id=self.chat.id, - video_note=video_note, - duration=duration, - length=length, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_video_note( + chat_id=self.chat.id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_media_group(self, media: typing.Union[MediaGroup, typing.List], - disable_notification: typing.Union[base.Boolean, None] = None, - reply: base.Boolean = False) -> typing.List[Message]: + async def answer_media_group( + self, + media: typing.Union[MediaGroup, typing.List], + disable_notification: typing.Union[base.Boolean, None] = None, + reply: base.Boolean = False, + ) -> typing.List[Message]: """ Use this method to send a group of photos or videos as an album. @@ -657,20 +724,28 @@ class Message(base.TelegramObject): :return: On success, an array of the sent Messages is returned. :rtype: typing.List[types.Message] """ - return await self.bot.send_media_group(self.chat.id, - media=media, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None) + return await self.bot.send_media_group( + self.chat.id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + ) - async def answer_location(self, - latitude: base.Float, longitude: base.Float, - live_period: typing.Union[base.Integer, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_location( + self, + latitude: base.Float, + longitude: base.Float, + live_period: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send point on the map. @@ -692,24 +767,33 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_location(chat_id=self.chat.id, - latitude=latitude, - longitude=longitude, - live_period=live_period, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_location( + chat_id=self.chat.id, + latitude=latitude, + longitude=longitude, + live_period=live_period, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_venue(self, - latitude: base.Float, longitude: base.Float, - title: base.String, address: base.String, - foursquare_id: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_venue( + self, + latitude: base.Float, + longitude: base.Float, + title: base.String, + address: base.String, + foursquare_id: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send information about a venue. @@ -735,24 +819,33 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_venue(chat_id=self.chat.id, - latitude=latitude, - longitude=longitude, - title=title, - address=address, - foursquare_id=foursquare_id, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_venue( + chat_id=self.chat.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_contact(self, phone_number: base.String, - first_name: base.String, last_name: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_contact( + self, + phone_number: base.String, + first_name: base.String, + last_name: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send phone contacts. @@ -774,20 +867,29 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_contact(chat_id=self.chat.id, - phone_number=phone_number, - first_name=first_name, last_name=last_name, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_contact( + chat_id=self.chat.id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_sticker(self, sticker: typing.Union[base.InputFile, base.String], - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_sticker( + self, + sticker: typing.Union[base.InputFile, base.String], + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send .webp stickers. @@ -805,19 +907,27 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_sticker(chat_id=self.chat.id, - sticker=sticker, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_sticker( + chat_id=self.chat.id, + sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def answer_dice(self, emoji: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = False) -> Message: + async def answer_dice( + self, + emoji: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = False, + ) -> Message: """ Use this method to send a dice, which will have a random value from 1 to 6. On success, the sent Message is returned. @@ -838,21 +948,29 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_dice(chat_id=self.chat.id, - disable_notification=disable_notification, - emoji=emoji, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_dice( + chat_id=self.chat.id, + disable_notification=disable_notification, + emoji=emoji, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply(self, text: base.String, - parse_mode: typing.Union[base.String, None] = None, - disable_web_page_preview: typing.Union[base.Boolean, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply( + self, + text: base.String, + parse_mode: typing.Union[base.String, None] = None, + disable_web_page_preview: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Reply to this message @@ -874,23 +992,31 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_message(chat_id=self.chat.id, - text=text, - parse_mode=parse_mode, - disable_web_page_preview=disable_web_page_preview, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_message( + chat_id=self.chat.id, + text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_photo(self, photo: typing.Union[base.InputFile, base.String], - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_photo( + self, + photo: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send photos. @@ -914,26 +1040,34 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_photo(chat_id=self.chat.id, - photo=photo, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_photo( + chat_id=self.chat.id, + photo=photo, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_audio(self, audio: typing.Union[base.InputFile, base.String], - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - duration: typing.Union[base.Integer, None] = None, - performer: typing.Union[base.String, None] = None, - title: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_audio( + self, + audio: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + duration: typing.Union[base.Integer, None] = None, + performer: typing.Union[base.String, None] = None, + title: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .mp3 format. @@ -965,30 +1099,38 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_audio(chat_id=self.chat.id, - audio=audio, - caption=caption, - parse_mode=parse_mode, - duration=duration, - performer=performer, - title=title, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_audio( + chat_id=self.chat.id, + audio=audio, + caption=caption, + parse_mode=parse_mode, + duration=duration, + performer=performer, + title=title, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_animation(self, animation: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - width: typing.Union[base.Integer, None] = None, - height: typing.Union[base.Integer, None] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_animation( + self, + animation: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -1025,27 +1167,35 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_animation(self.chat.id, - animation=animation, - duration=duration, - width=width, - height=height, - thumb=thumb, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_animation( + self.chat.id, + animation=animation, + duration=duration, + width=width, + height=height, + thumb=thumb, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_document(self, document: typing.Union[base.InputFile, base.String], - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_document( + self, + document: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send general files. @@ -1070,26 +1220,34 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_document(chat_id=self.chat.id, - document=document, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_document( + chat_id=self.chat.id, + document=document, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_video(self, video: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - width: typing.Union[base.Integer, None] = None, - height: typing.Union[base.Integer, None] = None, - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_video( + self, + video: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). @@ -1119,27 +1277,35 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_video(chat_id=self.chat.id, - video=video, - duration=duration, - width=width, - height=height, - caption=caption, - parse_mode=parse_mode, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_video( + chat_id=self.chat.id, + video=video, + duration=duration, + width=width, + height=height, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_voice(self, voice: typing.Union[base.InputFile, base.String], - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - duration: typing.Union[base.Integer, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_voice( + self, + voice: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + duration: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. @@ -1168,24 +1334,32 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_voice(chat_id=self.chat.id, - voice=voice, - caption=caption, - parse_mode=parse_mode, - duration=duration, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_voice( + chat_id=self.chat.id, + voice=voice, + caption=caption, + parse_mode=parse_mode, + duration=duration, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_video_note(self, video_note: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - length: typing.Union[base.Integer, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_video_note( + self, + video_note: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + length: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send video messages. @@ -1208,17 +1382,22 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_video_note(chat_id=self.chat.id, - video_note=video_note, - duration=duration, - length=length, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_video_note( + chat_id=self.chat.id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_media_group(self, media: typing.Union[MediaGroup, typing.List], - disable_notification: typing.Union[base.Boolean, None] = None, - reply: base.Boolean = True) -> typing.List[Message]: + async def reply_media_group( + self, + media: typing.Union[MediaGroup, typing.List], + disable_notification: typing.Union[base.Boolean, None] = None, + reply: base.Boolean = True, + ) -> typing.List[Message]: """ Use this method to send a group of photos or videos as an album. @@ -1232,20 +1411,28 @@ class Message(base.TelegramObject): :return: On success, an array of the sent Messages is returned. :rtype: typing.List[types.Message] """ - return await self.bot.send_media_group(self.chat.id, - media=media, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None) + return await self.bot.send_media_group( + self.chat.id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + ) - async def reply_location(self, - latitude: base.Float, longitude: base.Float, - live_period: typing.Union[base.Integer, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_location( + self, + latitude: base.Float, + longitude: base.Float, + live_period: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send point on the map. @@ -1267,24 +1454,33 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_location(chat_id=self.chat.id, - latitude=latitude, - longitude=longitude, - live_period=live_period, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_location( + chat_id=self.chat.id, + latitude=latitude, + longitude=longitude, + live_period=live_period, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_venue(self, - latitude: base.Float, longitude: base.Float, - title: base.String, address: base.String, - foursquare_id: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_venue( + self, + latitude: base.Float, + longitude: base.Float, + title: base.String, + address: base.String, + foursquare_id: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send information about a venue. @@ -1310,24 +1506,33 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_venue(chat_id=self.chat.id, - latitude=latitude, - longitude=longitude, - title=title, - address=address, - foursquare_id=foursquare_id, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_venue( + chat_id=self.chat.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_contact(self, phone_number: base.String, - first_name: base.String, last_name: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_contact( + self, + phone_number: base.String, + first_name: base.String, + last_name: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send phone contacts. @@ -1349,20 +1554,29 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_contact(chat_id=self.chat.id, - phone_number=phone_number, - first_name=first_name, last_name=last_name, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_contact( + chat_id=self.chat.id, + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_sticker(self, sticker: typing.Union[base.InputFile, base.String], - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_sticker( + self, + sticker: typing.Union[base.InputFile, base.String], + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send .webp stickers. @@ -1380,19 +1594,27 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_sticker(chat_id=self.chat.id, - sticker=sticker, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_sticker( + chat_id=self.chat.id, + sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def reply_dice(self, emoji: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, - ForceReply, None] = None, - reply: base.Boolean = True) -> Message: + async def reply_dice( + self, + emoji: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[ + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, + None, + ] = None, + reply: base.Boolean = True, + ) -> Message: """ Use this method to send a dice, which will have a random value from 1 to 6. On success, the sent Message is returned. @@ -1413,13 +1635,18 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_dice(chat_id=self.chat.id, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) + return await self.bot.send_dice( + chat_id=self.chat.id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup, + ) - async def forward(self, chat_id: typing.Union[base.Integer, base.String], - disable_notification: typing.Union[base.Boolean, None] = None) -> Message: + async def forward( + self, + chat_id: typing.Union[base.Integer, base.String], + disable_notification: typing.Union[base.Boolean, None] = None, + ) -> Message: """ Forward this message @@ -1432,13 +1659,17 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.forward_message(chat_id, self.chat.id, self.message_id, disable_notification) + return await self.bot.forward_message( + chat_id, self.chat.id, self.message_id, disable_notification + ) - async def edit_text(self, text: base.String, - parse_mode: typing.Union[base.String, None] = None, - disable_web_page_preview: typing.Union[base.Boolean, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - None] = None) -> typing.Union[Message, base.Boolean]: + async def edit_text( + self, + text: base.String, + parse_mode: typing.Union[base.String, None] = None, + disable_web_page_preview: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[InlineKeyboardMarkup, None] = None, + ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit text and game messages sent by the bot or via the bot (for inline bots). @@ -1457,16 +1688,21 @@ class Message(base.TelegramObject): the edited Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ - return await self.bot.edit_message_text(text=text, - chat_id=self.chat.id, message_id=self.message_id, - parse_mode=parse_mode, - disable_web_page_preview=disable_web_page_preview, - reply_markup=reply_markup) + return await self.bot.edit_message_text( + text=text, + chat_id=self.chat.id, + message_id=self.message_id, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + reply_markup=reply_markup, + ) - async def edit_caption(self, caption: base.String, - parse_mode: typing.Union[base.String, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, - None] = None) -> typing.Union[Message, base.Boolean]: + async def edit_caption( + self, + caption: base.String, + parse_mode: typing.Union[base.String, None] = None, + reply_markup: typing.Union[InlineKeyboardMarkup, None] = None, + ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit captions of messages sent by the bot or via the bot (for inline bots). @@ -1483,12 +1719,19 @@ class Message(base.TelegramObject): otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ - return await self.bot.edit_message_caption(chat_id=self.chat.id, message_id=self.message_id, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup) + return await self.bot.edit_message_caption( + chat_id=self.chat.id, + message_id=self.message_id, + caption=caption, + parse_mode=parse_mode, + reply_markup=reply_markup, + ) - async def edit_media(self, media: InputMedia, - reply_markup: typing.Union[InlineKeyboardMarkup, - None] = None) -> typing.Union[Message, base.Boolean]: + async def edit_media( + self, + media: InputMedia, + reply_markup: typing.Union[InlineKeyboardMarkup, None] = None, + ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. @@ -1509,12 +1752,16 @@ class Message(base.TelegramObject): otherwise True is returned :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ - return await self.bot.edit_message_media(media=media, chat_id=self.chat.id, message_id=self.message_id, - reply_markup=reply_markup) + return await self.bot.edit_message_media( + media=media, + chat_id=self.chat.id, + message_id=self.message_id, + reply_markup=reply_markup, + ) - async def edit_reply_markup(self, - reply_markup: typing.Union[InlineKeyboardMarkup, - None] = None) -> typing.Union[Message, base.Boolean]: + async def edit_reply_markup( + self, reply_markup: typing.Union[InlineKeyboardMarkup, None] = None + ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). @@ -1526,8 +1773,9 @@ class Message(base.TelegramObject): otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ - return await self.bot.edit_message_reply_markup(chat_id=self.chat.id, message_id=self.message_id, - reply_markup=reply_markup) + return await self.bot.edit_message_reply_markup( + chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup + ) async def delete_reply_markup(self) -> typing.Union[Message, base.Boolean]: """ @@ -1537,12 +1785,16 @@ class Message(base.TelegramObject): otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ - return await self.bot.edit_message_reply_markup(chat_id=self.chat.id, message_id=self.message_id) + return await self.bot.edit_message_reply_markup( + chat_id=self.chat.id, message_id=self.message_id + ) - async def edit_live_location(self, latitude: base.Float, - longitude: base.Float, - reply_markup: typing.Union[InlineKeyboardMarkup, - None] = None) -> typing.Union[Message, base.Boolean]: + async def edit_live_location( + self, + latitude: base.Float, + longitude: base.Float, + reply_markup: typing.Union[InlineKeyboardMarkup, None] = None, + ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit live location messages sent by the bot or via the bot (for inline bots). A location can be edited until its live_period expires or editing is explicitly disabled by a call @@ -1560,13 +1812,17 @@ class Message(base.TelegramObject): otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ - return await self.bot.edit_message_live_location(latitude=latitude, longitude=longitude, - chat_id=self.chat.id, message_id=self.message_id, - reply_markup=reply_markup) + return await self.bot.edit_message_live_location( + latitude=latitude, + longitude=longitude, + chat_id=self.chat.id, + message_id=self.message_id, + reply_markup=reply_markup, + ) - async def stop_live_location(self, - reply_markup: typing.Union[InlineKeyboardMarkup, - None] = None) -> typing.Union[Message, base.Boolean]: + async def stop_live_location( + self, reply_markup: typing.Union[InlineKeyboardMarkup, None] = None + ) -> typing.Union[Message, base.Boolean]: """ Use this method to stop updating a live location message sent by the bot or via the bot (for inline bots) before live_period expires. @@ -1579,8 +1835,9 @@ class Message(base.TelegramObject): otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ - return await self.bot.stop_message_live_location(chat_id=self.chat.id, message_id=self.message_id, - reply_markup=reply_markup) + return await self.bot.stop_message_live_location( + chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup + ) async def delete(self) -> base.Boolean: """ @@ -1599,7 +1856,9 @@ class Message(base.TelegramObject): """ return await self.bot.delete_message(self.chat.id, self.message_id) - async def pin(self, disable_notification: typing.Union[base.Boolean, None] = None) -> base.Boolean: + async def pin( + self, disable_notification: typing.Union[base.Boolean, None] = None + ) -> 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. @@ -1615,11 +1874,13 @@ class Message(base.TelegramObject): return await self.chat.pin_message(self.message_id, disable_notification) async def send_copy( - self: Message, - chat_id: typing.Union[str, int], - disable_notification: typing.Optional[bool] = None, - reply_to_message_id: typing.Optional[int] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None, + self: Message, + 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 + ] = None, ) -> Message: """ Send copy of current message @@ -1648,7 +1909,7 @@ class Message(base.TelegramObject): title=self.audio.title, performer=self.audio.performer, duration=self.audio.duration, - **kwargs + **kwargs, ) elif self.animation: return await self.bot.send_animation( @@ -1683,7 +1944,7 @@ class Message(base.TelegramObject): first_name=self.contact.first_name, last_name=self.contact.last_name, vcard=self.contact.vcard, - **kwargs + **kwargs, ) elif self.venue: kwargs.pop("parse_mode") @@ -1694,17 +1955,21 @@ class Message(base.TelegramObject): address=self.venue.address, foursquare_id=self.venue.foursquare_id, foursquare_type=self.venue.foursquare_type, - **kwargs + **kwargs, ) elif self.location: kwargs.pop("parse_mode") return await self.bot.send_location( - latitude=self.location.latitude, longitude=self.location.longitude, **kwargs + latitude=self.location.latitude, + longitude=self.location.longitude, + **kwargs, ) elif self.poll: kwargs.pop("parse_mode") return await self.bot.send_poll( - question=self.poll.question, options=[option.text for option in self.poll.options], **kwargs + question=self.poll.question, + options=[option.text for option in self.poll.options], + **kwargs, ) else: raise TypeError("This type of message can't be copied.") @@ -1741,6 +2006,7 @@ class ContentType(helper.Helper): :key: UNKNOWN :key: ANY """ + mode = helper.HelperMode.snake_case TEXT = helper.Item() # text @@ -1804,6 +2070,7 @@ class ContentTypes(helper.Helper): :key: UNKNOWN :key: ANY """ + mode = helper.HelperMode.snake_case TEXT = helper.ListItem() # text From 4647b17c3cb2d2c8909d9753bfdaa8ec1d0edbbd Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 13 Jun 2020 21:54:25 +0300 Subject: [PATCH 162/165] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 0dba109a..bebafcec 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = [ 'utils' ] -__version__ = '2.9.1' +__version__ = '2.9.2' __api_version__ = '4.9' From b004c8af199aa942a6c14fade64f267d6c7f7579 Mon Sep 17 00:00:00 2001 From: Yyonging Date: Sat, 27 Jun 2020 21:10:09 +0800 Subject: [PATCH 163/165] fix the #307 (#371) --- aiogram/types/fields.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index e0d5b892..022b9b72 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -1,5 +1,6 @@ import abc import datetime +import weakref __all__ = ('BaseField', 'Field', 'ListField', 'DateTimeField', 'TextField', 'ListOfLists') @@ -109,7 +110,9 @@ class Field(BaseField): and self.base_object is not None \ and not hasattr(value, 'base_object') \ and not hasattr(value, 'to_python'): - return self.base_object(conf={'parent': parent}, **value) + if not isinstance(parent, weakref.ReferenceType): + parent = weakref.ref(parent) + return self.base_object(conf={'parent':parent}, **value) return value From c9cbde4595448c3bed4e2232197e2d8df088759b Mon Sep 17 00:00:00 2001 From: Abstract-X <44748702+Abstract-X@users.noreply.github.com> Date: Sun, 28 Jun 2020 00:15:20 +1100 Subject: [PATCH 164/165] Add setting current context of types (#369) --- aiogram/dispatcher/dispatcher.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 164d6aad..a236df57 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -209,39 +209,50 @@ class Dispatcher(DataMixin, ContextInstanceMixin): try: if update.message: + types.Message.set_current(update.message) types.User.set_current(update.message.from_user) types.Chat.set_current(update.message.chat) return await self.message_handlers.notify(update.message) if update.edited_message: + types.Message.set_current(update.edited_message) types.User.set_current(update.edited_message.from_user) types.Chat.set_current(update.edited_message.chat) return await self.edited_message_handlers.notify(update.edited_message) if update.channel_post: + types.Message.set_current(update.channel_post) types.Chat.set_current(update.channel_post.chat) return await self.channel_post_handlers.notify(update.channel_post) if update.edited_channel_post: + types.Message.set_current(update.edited_channel_post) types.Chat.set_current(update.edited_channel_post.chat) return await self.edited_channel_post_handlers.notify(update.edited_channel_post) if update.inline_query: + types.InlineQuery.set_current(update.inline_query) types.User.set_current(update.inline_query.from_user) return await self.inline_query_handlers.notify(update.inline_query) if update.chosen_inline_result: + types.ChosenInlineResult.set_current(update.chosen_inline_result) types.User.set_current(update.chosen_inline_result.from_user) return await self.chosen_inline_result_handlers.notify(update.chosen_inline_result) if update.callback_query: + types.CallbackQuery.set_current(update.callback_query) if update.callback_query.message: types.Chat.set_current(update.callback_query.message.chat) types.User.set_current(update.callback_query.from_user) return await self.callback_query_handlers.notify(update.callback_query) if update.shipping_query: + types.ShippingQuery.set_current(update.shipping_query) types.User.set_current(update.shipping_query.from_user) return await self.shipping_query_handlers.notify(update.shipping_query) if update.pre_checkout_query: + types.PreCheckoutQuery.set_current(update.pre_checkout_query) types.User.set_current(update.pre_checkout_query.from_user) return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query) if update.poll: + types.Poll.set_current(update.poll) return await self.poll_handlers.notify(update.poll) if update.poll_answer: + types.PollAnswer.set_current(update.poll_answer) types.User.set_current(update.poll_answer.user) return await self.poll_answer_handlers.notify(update.poll_answer) except Exception as e: From f9c367548fbeac1fc1a487abfa13fb59b25aefec Mon Sep 17 00:00:00 2001 From: unintended Date: Sat, 27 Jun 2020 16:17:38 +0300 Subject: [PATCH 165/165] Fix markdown escaping issues (#363) * #360 - Fix: escape '=' sign in markdown * fix more escaping issues * Rename test suite Co-authored-by: Alex Root Junior --- aiogram/utils/text_decorations.py | 2 +- tests/test_utils/test_markdown.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/test_utils/test_markdown.py diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 77dc0ff3..3d22f637 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -163,7 +163,7 @@ class HtmlDecoration(TextDecoration): class MarkdownDecoration(TextDecoration): - MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-|{}.!])") + MARKDOWN_QUOTE_PATTERN: Pattern[str] = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])") def link(self, value: str, link: str) -> str: return f"[{value}]({link})" diff --git a/tests/test_utils/test_markdown.py b/tests/test_utils/test_markdown.py new file mode 100644 index 00000000..02faea2a --- /dev/null +++ b/tests/test_utils/test_markdown.py @@ -0,0 +1,11 @@ +import pytest + +from aiogram.utils import markdown + + +class TestMarkdownEscape: + def test_equality_sign_is_escaped(self): + assert markdown.escape_md(r"e = mc2") == r"e \= mc2" + + def test_pre_escaped(self): + assert markdown.escape_md(r"hello\.") == r"hello\\\."