From 22094eb477711e644924eaeb2424c8cef20e637a Mon Sep 17 00:00:00 2001 From: Forden Date: Wed, 5 Aug 2020 15:06:02 +0300 Subject: [PATCH 001/118] Patch 1 (#398) * Update callback_data_factory.py * Update callback_data_factory_simple.py --- examples/callback_data_factory.py | 2 +- examples/callback_data_factory_simple.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index 9a8affe9..2335ea95 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -112,7 +112,7 @@ async def query_post_vote(query: types.CallbackQuery, callback_data: dict): @dp.errors_handler(exception=MessageNotModified) async def message_not_modified_handler(update, error): - return True + return True # errors_handler must return True if error was handled correctly if __name__ == '__main__': diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py index 5fc9c548..a6d246d5 100644 --- a/examples/callback_data_factory_simple.py +++ b/examples/callback_data_factory_simple.py @@ -61,7 +61,7 @@ async def callback_vote_action(query: types.CallbackQuery, callback_data: dict): @dp.errors_handler(exception=MessageNotModified) # handle the cases when this exception raises async def message_not_modified_handler(update, error): - return True + return True # errors_handler must return True if error was handled correctly if __name__ == '__main__': From 4863675d28e841fb0ec8b85e77940599d7b5f013 Mon Sep 17 00:00:00 2001 From: Forden Date: Fri, 14 Aug 2020 17:09:45 +0300 Subject: [PATCH 002/118] Add exception MessageToPinNotFound (#404) --- aiogram/utils/exceptions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index cee2820a..a289be25 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -7,6 +7,7 @@ - MessageNotModified - MessageToForwardNotFound - MessageToDeleteNotFound + - MessageToPinNotFound - MessageIdentifierNotSpecified - MessageTextIsEmpty - MessageCantBeEdited @@ -182,6 +183,13 @@ class MessageToDeleteNotFound(MessageError): match = 'message to delete not found' +class MessageToPinNotFound(MessageError): + """ + Will be raised when you try to pin deleted or unknown message. + """ + match = 'message to pin not found' + + class MessageToReplyNotFound(MessageError): """ Will be raised when you try to reply to very old or deleted or unknown message. From 00cff4acf5af422cb2f1c5c34c0833d8d021301a Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Sat, 22 Aug 2020 01:07:03 +0400 Subject: [PATCH 003/118] fix(handlerObj): fix parameter-spec solving (#408) --- aiogram/dispatcher/handler.py | 2 +- tests/test_dispatcher/test_handler.py | 66 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/test_dispatcher/test_handler.py diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index cd5e9b50..38219012 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -33,7 +33,7 @@ def _check_spec(spec: inspect.FullArgSpec, kwargs: dict): if spec.varkw: return kwargs - return {k: v for k, v in kwargs.items() if k in spec.args} + return {k: v for k, v in kwargs.items() if k in set(spec.args + spec.kwonlyargs)} class Handler: diff --git a/tests/test_dispatcher/test_handler.py b/tests/test_dispatcher/test_handler.py new file mode 100644 index 00000000..b823c8f8 --- /dev/null +++ b/tests/test_dispatcher/test_handler.py @@ -0,0 +1,66 @@ +import functools + +import pytest + +from aiogram.dispatcher.handler import Handler, _check_spec, _get_spec + + +def callback1(foo: int, bar: int, baz: int): + return locals() + + +async def callback2(foo: int, bar: int, baz: int): + return locals() + + +async def callback3(foo: int, **kwargs): + return locals() + + +class TestHandlerObj: + def test_init_decorated(self): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + @decorator + def callback1(foo, bar, baz): + pass + + @decorator + @decorator + def callback2(foo, bar, baz): + pass + + obj1 = Handler.HandlerObj(callback1, _get_spec(callback1)) + obj2 = Handler.HandlerObj(callback2, _get_spec(callback2)) + + assert set(obj1.spec.args) == {"foo", "bar", "baz"} + assert obj1.handler == callback1 + assert set(obj2.spec.args) == {"foo", "bar", "baz"} + assert obj2.handler == callback2 + + @pytest.mark.parametrize( + "callback,kwargs,result", + [ + pytest.param( + callback1, {"foo": 42, "spam": True, "baz": "fuz"}, {"foo": 42, "baz": "fuz"} + ), + pytest.param( + callback2, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + {"foo": 42, "baz": "fuz", "bar": "test"}, + ), + pytest.param( + callback3, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + {"foo": 42, "spam": True, "baz": "fuz", "bar": "test"}, + ), + ], + ) + def test__check_spec(self, callback, kwargs, result): + spec = _get_spec(callback) + assert _check_spec(spec, kwargs) == result From d8c6214170a87cb46048f18623e6bbb96dc1b37c Mon Sep 17 00:00:00 2001 From: Daniil Kovalenko <40635760+WhiteMemory99@users.noreply.github.com> Date: Sun, 30 Aug 2020 05:06:48 +0700 Subject: [PATCH 004/118] Fix HTML characters escaping (#409) html.escape replaces " and ' characters by default, but it's not required by Telegram and causes unexpected behavior, quote=False argument fixes that. --- 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 3d22f637..4b3109af 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -159,7 +159,7 @@ class HtmlDecoration(TextDecoration): return f"{value}" def quote(self, value: str) -> str: - return html.escape(value) + return html.escape(value, quote=False) class MarkdownDecoration(TextDecoration): From 17eb8a56d06954a1540ebef0bdca80c8dd9cff4c Mon Sep 17 00:00:00 2001 From: unintended Date: Fri, 4 Sep 2020 18:08:15 +0300 Subject: [PATCH 005/118] Fix #413 parse entities positioning (#414) * fix entity positioning in parse_entities() #413 * add tests and small fixes --- aiogram/utils/text_decorations.py | 26 +++++++++++++++-------- tests/test_utils/test_text_decorations.py | 25 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 tests/test_utils/test_text_decorations.py diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 4b3109af..81592465 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -57,14 +57,14 @@ class TextDecoration(ABC): """ result = "".join( self._unparse_entities( - text, sorted(entities, key=lambda item: item.offset) if entities else [] + self._add_surrogates(text), sorted(entities, key=lambda item: item.offset) if entities else [] ) ) return result def _unparse_entities( self, - text: str, + text: bytes, entities: List[MessageEntity], offset: Optional[int] = None, length: Optional[int] = None, @@ -74,15 +74,15 @@ class TextDecoration(ABC): length = length or len(text) for index, entity in enumerate(entities): - if entity.offset < offset: + if entity.offset * 2 < offset: continue - if entity.offset > offset: - yield self.quote(text[offset : entity.offset]) - start = entity.offset - offset = entity.offset + entity.length + if entity.offset * 2 > offset: + yield self.quote(self._remove_surrogates(text[offset : entity.offset * 2])) + start = entity.offset * 2 + offset = entity.offset * 2 + entity.length * 2 sub_entities = list( - filter(lambda e: e.offset < (offset or 0), entities[index + 1 :]) + filter(lambda e: e.offset * 2 < (offset or 0), entities[index + 1 :]) ) yield self.apply_entity( entity, @@ -94,7 +94,15 @@ class TextDecoration(ABC): ) if offset < length: - yield self.quote(text[offset:length]) + yield self.quote(self._remove_surrogates(text[offset:length])) + + @staticmethod + def _add_surrogates(text: str): + return text.encode('utf-16-le') + + @staticmethod + def _remove_surrogates(text: bytes): + return text.decode('utf-16-le') @abstractmethod def link(self, value: str, link: str) -> str: # pragma: no cover diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py new file mode 100644 index 00000000..dd0e595d --- /dev/null +++ b/tests/test_utils/test_text_decorations.py @@ -0,0 +1,25 @@ +from aiogram.types import MessageEntity, MessageEntityType +from aiogram.utils import text_decorations + + +class TestTextDecorations: + def test_unparse_entities_normal_text(self): + assert text_decorations.markdown_decoration.unparse( + "hi i'm bold and italic and still bold", + entities=[ + MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), + MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), + ] + ) == "hi *i'm bold _and italic_\r and still bold*" + + def test_unparse_entities_emoji_text(self): + """ + emoji is encoded as two chars in json + """ + assert text_decorations.markdown_decoration.unparse( + "๐Ÿš€ i'm bold and italic and still bold", + entities=[ + MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), + MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), + ] + ) == "๐Ÿš€ *i'm bold _and italic_\r and still bold*" From 9ab2945267aab4a9567fca6086d12fc21774e1a0 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 10 Sep 2020 22:31:54 +0300 Subject: [PATCH 006/118] fixed CallbackQuery.answer() returns nothing (#420) --- aiogram/types/callback_query.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aiogram/types/callback_query.py b/aiogram/types/callback_query.py index 51ba1f17..e847bff8 100644 --- a/aiogram/types/callback_query.py +++ b/aiogram/types/callback_query.py @@ -54,8 +54,11 @@ class CallbackQuery(base.TelegramObject): :type cache_time: :obj:`typing.Union[base.Integer, None]` :return: On success, True is returned. :rtype: :obj:`base.Boolean`""" - await self.bot.answer_callback_query(callback_query_id=self.id, text=text, - show_alert=show_alert, url=url, cache_time=cache_time) + return await self.bot.answer_callback_query(callback_query_id=self.id, + text=text, + show_alert=show_alert, + url=url, + cache_time=cache_time) def __hash__(self): return hash(self.id) From 56ffa00c8ca8e80040fe9c58cf271d080fc46957 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 10 Sep 2020 22:32:56 +0300 Subject: [PATCH 007/118] I18n example patch (added docs and fixed typos) (#419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update i18n example 1. Replaced one file translation to all project folder translation. It's more usable case. 2. `For e.g.` --> `E.g.`. E.g. is short for `exempli gratia` which means simply โ€œfor example.โ€ So if you write for e.g., you are in effect writing `for for example`. 3. `xargs` replased with serveral lines, `xargs` is not appliable for Microsoft Windows users. 4. Added info about translation tools. 5. Minor edits. * i18n middlaware typo fix * i18n example header typo fix --- aiogram/contrib/middlewares/i18n.py | 4 ++-- examples/i18n_example.py | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 63f54510..bb6d8003 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -59,13 +59,13 @@ class I18nMiddleware(BaseMiddleware): with open(mo_path, 'rb') as fp: translations[name] = gettext.GNUTranslations(fp) elif os.path.exists(mo_path[:-2] + 'po'): - raise RuntimeError(f"Found locale '{name} but this language is not compiled!") + raise RuntimeError(f"Found locale '{name}' but this language is not compiled!") return translations def reload(self): """ - Hot reload locles + Hot reload locales """ self.locales = self.find_locales() diff --git a/examples/i18n_example.py b/examples/i18n_example.py index b626d048..29b43210 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -1,8 +1,8 @@ """ -Internalize your bot +Internationalize your bot Step 1: extract texts - # pybabel extract i18n_example.py -o locales/mybot.pot + # pybabel extract --input-dirs=. -o locales/mybot.pot Some useful options: - Extract texts with pluralization support @@ -16,9 +16,14 @@ Step 1: extract texts - Set version # --version=2.2 -Step 2: create *.po files. For e.g. create en, ru, uk locales. - # echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l -Step 3: translate texts +Step 2: create *.po files. E.g. create en, ru, uk locales. + # pybabel init -i locales/mybot.pot -d locales -D mybot -l en + # pybabel init -i locales/mybot.pot -d locales -D mybot -l ru + # pybabel init -i locales/mybot.pot -d locales -D mybot -l uk + +Step 3: translate texts located in locales/{language}/LC_MESSAGES/mybot.po + To open .po file you can use basic text editor or any PO editor, e.g. https://poedit.net/ + Step 4: compile translations # pybabel compile -d locales -D mybot @@ -27,7 +32,8 @@ Step 5: When you change the code of your bot you need to update po & mo files. command from step 1 Step 5.2: update po files # pybabel update -d locales -D mybot -i locales/mybot.pot - Step 5.3: update your translations + Step 5.3: update your translations + location and tools you know from step 3 Step 5.4: compile mo files command from step 4 """ @@ -92,5 +98,6 @@ async def cmd_like(message: types.Message, locale): # NOTE: This is comment for a translator await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', likes).format(number=likes)) + if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) From fc5ccc9d5a04981d5debfb2af00d20e00339e293 Mon Sep 17 00:00:00 2001 From: Lamroy95 <50185460+Lamroy95@users.noreply.github.com> Date: Thu, 10 Sep 2020 22:33:25 +0300 Subject: [PATCH 008/118] Fixed docs Quick start page code blocks (#417) --- docs/source/quick_start.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index b0724a78..319886ce 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -28,13 +28,13 @@ If you want to handle all messages in the chat simply add handler without filter .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 35-37 + :lines: 44-49 Last step: run long polling. .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 40-41 + :lines: 52-53 Summary ------- @@ -42,4 +42,4 @@ Summary .. literalinclude:: ../../examples/echo_bot.py :language: python :linenos: - :lines: -19,27- + :lines: -27,43- From a529619d7999af98d157fdeaa46dc8eee77948e1 Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Thu, 10 Sep 2020 23:34:20 +0400 Subject: [PATCH 009/118] hotfix(updates): CHOSEN_INLINE_RESULT is a correct API-term (#415) * hotfix(updates): CHOSEN_INLINE_RESULT is a correct API-term * feat(utils): deprecated descriptor deprecate CHOSEN_INLINE_QUERY and always return CHOSEN_INLINE_RESULT instead of incorrect value * fix(tests): remove example from test * fix(utils): use stacklevel=3 level on which descriptor is being called --- aiogram/types/update.py | 10 +++++++-- aiogram/utils/deprecated.py | 35 ++++++++++++++++++++++++++++- tests/test_utils/test_deprecated.py | 14 ++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 tests/test_utils/test_deprecated.py diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 2146cb9d..9d1afacc 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -9,7 +9,7 @@ from .message import Message from .poll import Poll, PollAnswer from .pre_checkout_query import PreCheckoutQuery from .shipping_query import ShippingQuery -from ..utils import helper +from ..utils import helper, deprecated class Update(base.TelegramObject): @@ -55,9 +55,15 @@ class AllowedUpdates(helper.Helper): CHANNEL_POST = helper.ListItem() # channel_post EDITED_CHANNEL_POST = helper.ListItem() # edited_channel_post INLINE_QUERY = helper.ListItem() # inline_query - CHOSEN_INLINE_QUERY = helper.ListItem() # chosen_inline_result + CHOSEN_INLINE_RESULT = helper.ListItem() # chosen_inline_result 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 + + CHOSEN_INLINE_QUERY = deprecated.DeprecatedReadOnlyClassVar( + "`CHOSEN_INLINE_QUERY` is a deprecated value for allowed update. " + "Use `CHOSEN_INLINE_RESULT`", + new_value_getter=lambda cls: cls.CHOSEN_INLINE_RESULT, + ) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index 83a9034c..6d0d7ee3 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -2,7 +2,7 @@ import asyncio import inspect import warnings import functools -from typing import Callable +from typing import Callable, Generic, TypeVar, Type, Optional def deprecated(reason, stacklevel=2) -> Callable: @@ -129,3 +129,36 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve return wrapped return decorator + + +_VT = TypeVar("_VT") +_OwnerCls = TypeVar("_OwnerCls") + + +class DeprecatedReadOnlyClassVar(Generic[_OwnerCls, _VT]): + """ + DeprecatedReadOnlyClassVar[Owner, ValueType] + + :param warning_message: Warning message when getter gets called + :param new_value_getter: Any callable with (owner_class: Type[Owner]) -> ValueType + signature that will be executed + + Usage example: + + >>> class MyClass: + ... some_attribute: DeprecatedReadOnlyClassVar[MyClass, int] = \ + ... DeprecatedReadOnlyClassVar( + ... "Warning message.", lambda owner: 15) + ... + >>> MyClass.some_attribute # does warning.warn with `Warning message` and returns 15 in the current case + """ + + __slots__ = "_new_value_getter", "_warning_message" + + def __init__(self, warning_message: str, new_value_getter: Callable[[_OwnerCls], _VT]): + self._warning_message = warning_message + self._new_value_getter = new_value_getter + + def __get__(self, instance: Optional[_OwnerCls], owner: Type[_OwnerCls]): + warn_deprecated(self._warning_message, stacklevel=3) + return self._new_value_getter(owner) diff --git a/tests/test_utils/test_deprecated.py b/tests/test_utils/test_deprecated.py new file mode 100644 index 00000000..114d6810 --- /dev/null +++ b/tests/test_utils/test_deprecated.py @@ -0,0 +1,14 @@ +import pytest + +from aiogram.utils.deprecated import DeprecatedReadOnlyClassVar + + +def test_DeprecatedReadOnlyClassVarCD(): + assert DeprecatedReadOnlyClassVar.__slots__ == ("_new_value_getter", "_warning_message") + + new_value_of_deprecated_cls_cd = "mpa" + pseudo_owner_cls = type("OpekaCla$$", (), {}) + deprecated_cd = DeprecatedReadOnlyClassVar("mopekaa", lambda owner: new_value_of_deprecated_cls_cd) + + with pytest.warns(DeprecationWarning): + assert deprecated_cd.__get__(None, pseudo_owner_cls) == new_value_of_deprecated_cls_cd From fce48e3127c645fbfe7b771d34c9175f3c41d2cf Mon Sep 17 00:00:00 2001 From: Daneel L <53001370+lovkiymusic@users.noreply.github.com> Date: Sun, 13 Sep 2020 22:07:14 +0300 Subject: [PATCH 010/118] comment of RedisStorage2 fix (#423) --- aiogram/contrib/fsm_storage/redis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index bf88eff7..74dd736c 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -208,7 +208,7 @@ class RedisStorage2(BaseStorage): .. code-block:: python3 - storage = RedisStorage('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key') + storage = RedisStorage2('localhost', 6379, db=5, pool_size=10, prefix='my_fsm_key') dp = Dispatcher(bot, storage=storage) And need to close Redis connection when shutdown From a936465f427b25c149e4e508c666fb5ed3de91d5 Mon Sep 17 00:00:00 2001 From: Gabben <43146729+gabbhack@users.noreply.github.com> Date: Mon, 14 Sep 2020 00:07:29 +0500 Subject: [PATCH 011/118] add missing attributes (#422) https://core.telegram.org/bots/api#animation --- aiogram/types/animation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/types/animation.py b/aiogram/types/animation.py index 78f5235a..b08089c1 100644 --- a/aiogram/types/animation.py +++ b/aiogram/types/animation.py @@ -15,6 +15,9 @@ class Animation(base.TelegramObject, mixins.Downloadable): 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() thumb: PhotoSize = fields.Field(base=PhotoSize) file_name: base.String = fields.Field() mime_type: base.String = fields.Field() From 51547f974511dc909bc2697c36c7b3b2681335c8 Mon Sep 17 00:00:00 2001 From: lyteloli Date: Sun, 13 Sep 2020 22:08:55 +0300 Subject: [PATCH 012/118] Fixed usage of deprecated is_private function (#421) Co-authored-by: Alex Root Junior --- aiogram/dispatcher/filters/builtin.py | 2 +- aiogram/types/message.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index fcaaf786..20317f57 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -673,7 +673,7 @@ class AdminFilter(Filter): message = obj.message else: return False - if ChatType.is_private(message): # there is no admin in private chats + if message.chat.type == ChatType.PRIVATE: # there is no admin in private chats return False chat_ids = [message.chat.id] else: diff --git a/aiogram/types/message.py b/aiogram/types/message.py index fedd656e..52c2b3a8 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -236,9 +236,8 @@ class Message(base.TelegramObject): :return: str """ - if ChatType.is_private(self.chat): - raise TypeError("Invalid chat type!") - + if self.chat.type == ChatType.PRIVATE: + raise TypeError('Invalid chat type url = "https://t.me/" if self.chat.username: # Generates public link From 00202565e4941166a468f9b9b16d5b8b7502ecd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9?= Date: Sun, 13 Sep 2020 22:09:43 +0300 Subject: [PATCH 013/118] fixed type hints of callback_data (#400) its impotant to remeber all data saved in callback_data is text even if you pass to it integer insofar as newbies often copy examples and modyfy this typing may help them make no mistake --- examples/callback_data_factory.py | 7 ++++--- examples/callback_data_factory_simple.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index 2335ea95..c95860f1 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -1,6 +1,7 @@ import logging import random import uuid +import typing from aiogram import Bot, Dispatcher, executor, md, types from aiogram.contrib.fsm_storage.memory import MemoryStorage @@ -52,7 +53,7 @@ def format_post(post_id: str, post: dict) -> (str, types.InlineKeyboardMarkup): md.quote_html(post['body']), '', # just new empty line f"Votes: {post['votes']}", - sep = '\n', + sep='\n', ) markup = types.InlineKeyboardMarkup() @@ -75,7 +76,7 @@ async def query_show_list(query: types.CallbackQuery): @dp.callback_query_handler(posts_cb.filter(action='view')) -async def query_view(query: types.CallbackQuery, callback_data: dict): +async def query_view(query: types.CallbackQuery, callback_data: typing.Dict[str, str]): post_id = callback_data['id'] post = POSTS.get(post_id, None) @@ -87,7 +88,7 @@ async def query_view(query: types.CallbackQuery, callback_data: dict): @dp.callback_query_handler(posts_cb.filter(action=['like', 'dislike'])) -async def query_post_vote(query: types.CallbackQuery, callback_data: dict): +async def query_post_vote(query: types.CallbackQuery, callback_data: typing.Dict[str, str]): try: await dp.throttle('vote', rate=1) except Throttled: diff --git a/examples/callback_data_factory_simple.py b/examples/callback_data_factory_simple.py index a6d246d5..d8c1a327 100644 --- a/examples/callback_data_factory_simple.py +++ b/examples/callback_data_factory_simple.py @@ -4,6 +4,7 @@ For more comprehensive example see callback_data_factory.py """ import logging +import typing from aiogram import Bot, Dispatcher, executor, types from aiogram.contrib.middlewares.logging import LoggingMiddleware @@ -38,7 +39,7 @@ async def cmd_start(message: types.Message): @dp.callback_query_handler(vote_cb.filter(action=['up', 'down'])) -async def callback_vote_action(query: types.CallbackQuery, callback_data: dict): +async def callback_vote_action(query: types.CallbackQuery, callback_data: typing.Dict[str, str]): logging.info('Got this callback data: %r', callback_data) # callback_data contains all info from callback data await query.answer() # don't forget to answer callback query as soon as possible callback_data_action = callback_data['action'] From 60fe0931a72b4b02588d5d153d1903a05be81496 Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Sun, 13 Sep 2020 23:11:27 +0400 Subject: [PATCH 014/118] docs(readme): prettify readme, update downloads stats badge (#406) * docs(readme): prettify readme, update downloads stats badge * chore(readme): describe steps for minimal code * chore(rme): remove comma etc Co-authored-by: evgfilim1 * chore(rme): add heading for details/summary blocks * Update README.md Co-authored-by: evgfilim1 Co-authored-by: Alex Root Junior --- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dfef918f..5cc22e92 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,72 @@ **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://docs.aiogram.dev/en/latest/). + +## Examples +
+ ๐Ÿ“š Click to see some basic examples + + +**Few steps before getting started...** +- First, you should obtain token for your bot from [BotFather](https://t.me/BotFather). +- Install latest stable version of aiogram, simply running `pip install aiogram` + +### Simple [`getMe`](https://core.telegram.org/bots/api#getme) request + +```python +import asyncio +from aiogram import Bot + + +async def main(): + bot = Bot(token=BOT-TOKEN) + + try: + me = await bot.get_me() + print(f"๐Ÿค– Hello, I'm {me.first_name}.\nHave a nice Day!") + finally: + await bot.close() + +asyncio.run(main()) +``` + +### Poll BotAPI for updates and process updates + +```python +import asyncio +from aiogram import Bot, Dispatcher, types + +async def start_handler(event: types.Message): + await event.answer( + f"Hello, {event.from_user.get_mention(as_html=True)} ๐Ÿ‘‹!", + parse_mode=types.ParseMode.HTML, + ) + +async def main(): + bot = Bot(token=BOT-TOKEN) + try: + disp = Dispatcher(bot=bot) + disp.register_message_handler(start_handler, commands={"start", "restart"}) + await disp.start_polling() + finally: + await bot.close() + +asyncio.run(main()) +``` + +### Moar! + +You can find more examples in [`examples/`](https://github.com/aiogram/aiogram/tree/dev-2.x/examples) directory + +
## Official aiogram resources: - News: [@aiogram_live](https://t.me/aiogram_live) - 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 site](https://docs.aiogram.dev/) + - PyPI: [aiogram](https://pypi.python.org/pypi/aiogram) + - Documentation: [aiogram site](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 f926d80ba2ffe3a62cdd906a2094693a025ed2f0 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 13 Sep 2020 22:13:15 +0300 Subject: [PATCH 015/118] Add missed emoji argument to reply_dice (#395) * fix: add missed emoji argument to reply_dice * ref: order arguments for send_dice --- 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 52c2b3a8..3540a736 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1048,8 +1048,8 @@ class Message(base.TelegramObject): """ return await self.bot.send_dice( chat_id=self.chat.id, - disable_notification=disable_notification, emoji=emoji, + disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup, ) @@ -1834,6 +1834,7 @@ class Message(base.TelegramObject): """ return await self.bot.send_dice( chat_id=self.chat.id, + emoji=emoji, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup, From 7f053412bf5c3f4803b13ea6a028961bb55caef1 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 13 Sep 2020 22:14:01 +0300 Subject: [PATCH 016/118] Add is_chat_creator method to ChatMemberStatus (#394) * new: add is_chat_creator method to ChatMemberStatus * enh: use tuples instead of lists for some checks --- aiogram/bot/api.py | 2 +- aiogram/bot/bot.py | 4 ++-- aiogram/types/chat_member.py | 11 +++++++++-- aiogram/types/input_media.py | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 1d0c4f7b..cb258cdf 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -80,7 +80,7 @@ def check_result(method_name: str, content_type: str, status_code: int, body: st exceptions.NotFound.detect(description) elif status_code == HTTPStatus.CONFLICT: exceptions.ConflictError.detect(description) - elif status_code in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]: + elif status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): exceptions.Unauthorized.detect(description) elif status_code == HTTPStatus.REQUEST_ENTITY_TOO_LARGE: raise exceptions.NetworkError('File too large for uploading. ' diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index f427fa8c..c5678a37 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1135,10 +1135,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): permissions = prepare_arg(permissions) payload = generate_payload(**locals()) - for permission in ['can_send_messages', + for permission in ('can_send_messages', 'can_send_media_messages', 'can_send_other_messages', - 'can_add_web_page_previews']: + 'can_add_web_page_previews'): if permission in payload: warnings.warn(f"The method `restrict_chat_member` now takes the new user permissions " f"in a single argument of the type ChatPermissions instead of " diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 347b2750..274c8a26 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -34,6 +34,9 @@ class ChatMember(base.TelegramObject): can_send_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() + def is_chat_creator(self) -> bool: + return ChatMemberStatus.is_chat_creator(self.status) + def is_chat_admin(self) -> bool: return ChatMemberStatus.is_chat_admin(self.status) @@ -57,10 +60,14 @@ class ChatMemberStatus(helper.Helper): LEFT = helper.Item() # left KICKED = helper.Item() # kicked + @classmethod + def is_chat_creator(cls, role: str) -> bool: + return role == cls.CREATOR + @classmethod def is_chat_admin(cls, role: str) -> bool: - return role in [cls.ADMINISTRATOR, cls.CREATOR] + return role in (cls.ADMINISTRATOR, cls.CREATOR) @classmethod def is_chat_member(cls, role: str) -> bool: - return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED] + return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 9a77658f..25422df1 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -239,7 +239,7 @@ class MediaGroup(base.TelegramObject): elif not isinstance(media, InputMedia): raise TypeError(f"Media must be an instance of InputMedia or dict, not {type(media).__name__}") - elif media.type in ['document', 'audio', 'animation']: + elif media.type in ('document', 'audio', 'animation'): raise ValueError(f"This type of media is not supported by media groups!") self.media.append(media) From 5b40a2b8cfee24bfef997bfcede70b2c784b03ca Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 13 Sep 2020 22:14:33 +0300 Subject: [PATCH 017/118] Add missed ChatPermissions to __all__ (#393) * new: add missed ChatPermissions to __all__ * ref: standardize all __all__ --- aiogram/__init__.py | 6 ++--- aiogram/bot/__init__.py | 6 ++--- aiogram/contrib/fsm_storage/rethinkdb.py | 2 +- aiogram/dispatcher/__init__.py | 4 ++-- aiogram/dispatcher/filters/__init__.py | 30 ++++++++++++------------ aiogram/types/__init__.py | 1 + aiogram/utils/text_decorations.py | 10 ++++---- 7 files changed, 30 insertions(+), 29 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index bebafcec..74dc49ab 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -25,7 +25,7 @@ else: if 'DISABLE_UVLOOP' not in os.environ: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -__all__ = [ +__all__ = ( 'Bot', 'Dispatcher', '__api_version__', @@ -40,8 +40,8 @@ __all__ = [ 'md', 'middlewares', 'types', - 'utils' -] + 'utils', +) __version__ = '2.9.2' __api_version__ = '4.9' diff --git a/aiogram/bot/__init__.py b/aiogram/bot/__init__.py index 252c465b..19c051bf 100644 --- a/aiogram/bot/__init__.py +++ b/aiogram/bot/__init__.py @@ -2,8 +2,8 @@ from . import api from .base import BaseBot from .bot import Bot -__all__ = [ +__all__ = ( 'BaseBot', 'Bot', - 'api' -] + 'api', +) diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index 38d24efa..a9d822d5 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -7,7 +7,7 @@ from rethinkdb.asyncio_net.net_asyncio import Connection from ...dispatcher.storage import BaseStorage -__all__ = ['RethinkDBStorage'] +__all__ = ('RethinkDBStorage',) r = rethinkdb.RethinkDB() r.set_loop_type('asyncio') diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index 6ad43bbe..e412dd36 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -5,7 +5,7 @@ from . import storage from . import webhook from .dispatcher import Dispatcher, FSMContext, DEFAULT_RATE_LIMIT -__all__ = [ +__all__ = ( 'DEFAULT_RATE_LIMIT', 'Dispatcher', 'FSMContext', @@ -14,4 +14,4 @@ __all__ = [ 'middlewares', 'storage', 'webhook' -] +) diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 5f839662..d64a2667 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -6,33 +6,33 @@ from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec -__all__ = [ - 'AbstractFilter', - 'BoundFilter', +__all__ = ( 'Command', - 'CommandStart', 'CommandHelp', 'CommandPrivacy', 'CommandSettings', + 'CommandStart', 'ContentTypeFilter', 'ExceptionsFilter', 'HashTag', - 'Filter', - 'FilterNotPassed', - 'FilterRecord', - 'FiltersFactory', - 'RegexpCommandsFilter', 'Regexp', + 'RegexpCommandsFilter', 'StateFilter', 'Text', 'IDFilter', + 'AdminFilter', 'IsReplyFilter', 'IsSenderContact', - 'AdminFilter', - 'get_filter_spec', - 'get_filters_spec', - 'execute_filter', - 'check_filters', 'ForwardedMessageFilter', 'ChatTypeFilter', -] + 'FiltersFactory', + 'AbstractFilter', + 'BoundFilter', + 'Filter', + 'FilterNotPassed', + 'FilterRecord', + 'execute_filter', + 'check_filters', + 'get_filter_spec', + 'get_filters_spec', +) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 1221ec72..d46f24da 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -78,6 +78,7 @@ __all__ = ( 'ChatActions', 'ChatMember', 'ChatMemberStatus', + 'ChatPermissions', 'ChatPhoto', 'ChatType', 'ChosenInlineResult', diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 81592465..0fed91e8 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -9,11 +9,11 @@ if TYPE_CHECKING: # pragma: no cover from aiogram.types import MessageEntity __all__ = ( - "TextDecoration", - "HtmlDecoration", - "MarkdownDecoration", - "html_decoration", - "markdown_decoration", + 'HtmlDecoration', + 'MarkdownDecoration', + 'TextDecoration', + 'html_decoration', + 'markdown_decoration', ) From 0cc6a2c8f6e77b1a653d4fd6151164ee43892bd8 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 13 Sep 2020 22:15:07 +0300 Subject: [PATCH 018/118] Add is_forward method to Message (#390) * new: add is_forward method to Message * enh: add return types to Message methods * fix: docs typo * ref: make command methods closer --- aiogram/types/message.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 3540a736..f867a44a 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -153,7 +153,16 @@ class Message(base.TelegramObject): return ContentType.UNKNOWN - def is_command(self): + def is_forward(self) -> bool: + """ + Check that the message is forwarded. + Only `forward_date` is required to be in forwarded message. + + :return: bool + """ + return bool(self.forward_date) + + def is_command(self) -> bool: """ Check message text is command @@ -161,7 +170,7 @@ class Message(base.TelegramObject): """ return self.text and self.text.startswith("/") - def get_full_command(self): + def get_full_command(self) -> typing.Optional[typing.Tuple[str, str]]: """ Split command and args @@ -172,7 +181,7 @@ class Message(base.TelegramObject): args = args[-1] if args else "" return command, args - def get_command(self, pure=False): + def get_command(self, pure=False) -> typing.Optional[str]: """ Get command from message @@ -185,7 +194,7 @@ class Message(base.TelegramObject): command, _, _ = command[1:].partition("@") return command - def get_args(self): + def get_args(self) -> typing.Optional[str]: """ Get arguments @@ -195,7 +204,7 @@ class Message(base.TelegramObject): if command: return command[1] - def parse_entities(self, as_html=True): + def parse_entities(self, as_html=True) -> str: """ Text or caption formatted as HTML or Markdown. From 7d1c8c42d3cdd333e3a2933eb09fcd9523810852 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 13 Sep 2020 22:27:50 +0300 Subject: [PATCH 019/118] Fix syntax error --- 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 f867a44a..9d2ad505 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -245,8 +245,8 @@ class Message(base.TelegramObject): :return: str """ - if self.chat.type == ChatType.PRIVATE: - raise TypeError('Invalid chat type + if ChatType.is_private(self.chat): + raise TypeError("Invalid chat type!") url = "https://t.me/" if self.chat.username: # Generates public link From c99b165668d305c534c56f6345960c1461219adf Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Sun, 13 Sep 2020 23:42:21 +0400 Subject: [PATCH 020/118] fix(bot,dispatcher): do not use _MainThread event loop (#397) * fix(bot,dispatcher): do not use _MainThread event loop on ::Bot, ::Dispatcher initializations * fix: use more generic get approach * docs: comments * chore(task): asyncio.create_task comes with py3.7 * fix(dispatcher): todo --- aiogram/bot/base.py | 19 ++++++------ aiogram/dispatcher/dispatcher.py | 49 +++++++++++++++++++++++++------ aiogram/dispatcher/middlewares.py | 5 +++- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 86347e88..f45546c3 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -56,6 +56,8 @@ class BaseBot: :type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]` :raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError` """ + self._main_loop = loop + # Authentication if validate_token: api.check_token(token) @@ -66,19 +68,12 @@ class BaseBot: self.proxy = proxy self.proxy_auth = proxy_auth - # Asyncio loop instance - if loop is None: - loop = asyncio.get_event_loop() - self.loop = loop - # 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 - ) + self._connector_init = dict(limit=connections_limit, ssl=ssl_context) if isinstance(proxy, str) and (proxy.startswith('socks5://') or proxy.startswith('socks4://')): from aiohttp_socks import SocksConnector @@ -106,11 +101,15 @@ class BaseBot: def get_new_session(self) -> aiohttp.ClientSession: return aiohttp.ClientSession( - connector=self._connector_class(**self._connector_init), - loop=self.loop, + connector=self._connector_class(**self._connector_init, loop=self._main_loop), + loop=self._main_loop, json_serialize=json.dumps ) + @property + def loop(self) -> Optional[asyncio.AbstractEventLoop]: + return self._main_loop + @property def session(self) -> Optional[aiohttp.ClientSession]: if self._session is None or self._session.closed: diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 7a3aa5b3..b38d3af1 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -27,6 +27,13 @@ log = logging.getLogger(__name__) DEFAULT_RATE_LIMIT = .1 +def _ensure_loop(x: "asyncio.AbstractEventLoop"): + assert isinstance( + x, asyncio.AbstractEventLoop + ), f"Loop must be the implementation of {asyncio.AbstractEventLoop!r}, " \ + f"not {type(x)!r}" + + class Dispatcher(DataMixin, ContextInstanceMixin): """ Simple Updates dispatcher @@ -43,15 +50,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin): if not isinstance(bot, Bot): raise TypeError(f"Argument 'bot' must be an instance of Bot, not '{type(bot).__name__}'") - if loop is None: - loop = bot.loop if storage is None: storage = DisabledStorage() if filters_factory is None: filters_factory = FiltersFactory(self) self.bot: Bot = bot - self.loop = loop + if loop is not None: + _ensure_loop(loop) + self._main_loop = loop self.storage = storage self.run_tasks_by_default = run_tasks_by_default @@ -79,10 +86,27 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self._polling = False self._closed = True - self._close_waiter = loop.create_future() + self._dispatcher_close_waiter = None self._setup_filters() + @property + def loop(self) -> typing.Optional[asyncio.AbstractEventLoop]: + # for the sake of backward compatibility + # lib internally must delegate tasks with respect to _main_loop attribute + # however should never be used by the library itself + # use more generic approaches from asyncio's namespace + return self._main_loop + + @property + def _close_waiter(self) -> "asyncio.Future": + if self._dispatcher_close_waiter is None: + if self._main_loop is not None: + self._dispatcher_close_waiter = self._main_loop.create_future() + else: + self._dispatcher_close_waiter = asyncio.get_event_loop().create_future() + return self._dispatcher_close_waiter + def _setup_filters(self): filters_factory = self.filters_factory @@ -282,6 +306,13 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return await self.bot.delete_webhook() + def _loop_create_task(self, coro): + if self._main_loop is None: + return asyncio.create_task(coro) + else: + _ensure_loop(self._main_loop) + return self._main_loop.create_task(coro) + async def start_polling(self, timeout=20, relax=0.1, @@ -337,7 +368,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): log.debug(f"Received {len(updates)} updates.") offset = updates[-1].update_id + 1 - self.loop.create_task(self._process_polling_updates(updates, fast)) + self._loop_create_task(self._process_polling_updates(updates, fast)) if relax: await asyncio.sleep(relax) @@ -381,7 +412,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): :return: """ - await asyncio.shield(self._close_waiter, loop=self.loop) + await asyncio.shield(self._close_waiter) def is_polling(self): """ @@ -1158,15 +1189,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin): try: response = task.result() except Exception as e: - self.loop.create_task( + self._loop_create_task( self.errors_handlers.notify(types.Update.get_current(), e)) else: if isinstance(response, BaseResponse): - self.loop.create_task(response.execute_response(self.bot)) + self._loop_create_task(response.execute_response(self.bot)) @functools.wraps(func) async def wrapper(*args, **kwargs): - task = self.loop.create_task(func(*args, **kwargs)) + task = self._loop_create_task(func(*args, **kwargs)) task.add_done_callback(process_response) return wrapper diff --git a/aiogram/dispatcher/middlewares.py b/aiogram/dispatcher/middlewares.py index dba3db4c..5fa09830 100644 --- a/aiogram/dispatcher/middlewares.py +++ b/aiogram/dispatcher/middlewares.py @@ -16,11 +16,14 @@ class MiddlewareManager: :param dispatcher: instance of Dispatcher """ self.dispatcher = dispatcher - self.loop = dispatcher.loop self.bot = dispatcher.bot self.storage = dispatcher.storage self.applications = [] + @property + def loop(self): + return self.dispatcher.loop + def setup(self, middleware): """ Setup middleware From 84785e48807fbcbb224889524a4e3d3544d5b12c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 13 Sep 2020 22:56:32 +0300 Subject: [PATCH 021/118] Fix italic and underline decorations --- Makefile | 2 +- aiogram/utils/text_decorations.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index f21ec8ae..da6493d9 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ test: tox summary: - cloc aiogram/ tests/ examples/ setup.py + cloc aiogram/ tests/ examples/ docs/ setup.py docs: docs/source/* cd docs && $(MAKE) html diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 0fed91e8..4de8d69a 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -180,7 +180,7 @@ class MarkdownDecoration(TextDecoration): return f"*{value}*" def italic(self, value: str) -> str: - return f"_{value}_\r" + return f"_\r{value}_\r" def code(self, value: str) -> str: return f"`{value}`" @@ -192,7 +192,7 @@ class MarkdownDecoration(TextDecoration): return f"```{language}\n{value}\n```" def underline(self, value: str) -> str: - return f"__{value}__" + return f"__\r{value}__\r" def strikethrough(self, value: str) -> str: return f"~{value}~" From 369bff422a326d4f47c63f8719e7f1b7d4aeb11a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 13 Sep 2020 23:13:27 +0300 Subject: [PATCH 022/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 74dc49ab..9a11b7a2 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.9.2' +__version__ = '2.10' __api_version__ = '4.9' From 703dd7fabdf1dd2072ebbd311437dc5615b83cd2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 13 Sep 2020 23:25:27 +0300 Subject: [PATCH 023/118] Fix tests and bump dependencies --- setup.py | 2 +- tests/test_utils/test_text_decorations.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index b21b4e57..00f617ee 100755 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( ], extras_require={ 'proxy': [ - 'aiohttp-socks>=0.3.4,<0.4.0', + 'aiohttp-socks>=0.5.3,<0.6.0', ], 'fast': [ 'uvloop>=0.14.0,<0.15.0', diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py index dd0e595d..cc724dd7 100644 --- a/tests/test_utils/test_text_decorations.py +++ b/tests/test_utils/test_text_decorations.py @@ -10,7 +10,7 @@ class TestTextDecorations: MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), ] - ) == "hi *i'm bold _and italic_\r and still bold*" + ) == "hi *i'm bold _\rand italic_\r and still bold*" def test_unparse_entities_emoji_text(self): """ @@ -22,4 +22,4 @@ class TestTextDecorations: MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), ] - ) == "๐Ÿš€ *i'm bold _and italic_\r and still bold*" + ) == "๐Ÿš€ *i'm bold _\rand italic_\r and still bold*" From 7c65344dcdd88bb1a852dacd946c82b47ed30509 Mon Sep 17 00:00:00 2001 From: Martin Winks <50446230+uwinx@users.noreply.github.com> Date: Mon, 14 Sep 2020 15:44:18 +0400 Subject: [PATCH 024/118] hotfix(dispatcher): prop DP.loop may be None, executor and webhook view should respect that (#424) --- aiogram/contrib/middlewares/environment.py | 4 +++- aiogram/dispatcher/webhook.py | 4 ++-- aiogram/utils/executor.py | 10 +++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/aiogram/contrib/middlewares/environment.py b/aiogram/contrib/middlewares/environment.py index 0427a739..f6ad56dd 100644 --- a/aiogram/contrib/middlewares/environment.py +++ b/aiogram/contrib/middlewares/environment.py @@ -1,3 +1,5 @@ +import asyncio + from aiogram.dispatcher.middlewares import BaseMiddleware @@ -14,7 +16,7 @@ class EnvironmentMiddleware(BaseMiddleware): data.update( bot=dp.bot, dispatcher=dp, - loop=dp.loop + loop=dp.loop or asyncio.get_event_loop() ) if self.context: data.update(self.context) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index ed2ebf99..5199d591 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -169,7 +169,7 @@ class WebhookRequestHandler(web.View): :return: """ dispatcher = self.get_dispatcher() - loop = dispatcher.loop + loop = dispatcher.loop or asyncio.get_event_loop() # Analog of `asyncio.wait_for` but without cancelling task waiter = loop.create_future() @@ -209,7 +209,7 @@ class WebhookRequestHandler(web.View): TimeoutWarning) dispatcher = self.get_dispatcher() - loop = dispatcher.loop + loop = dispatcher.loop or asyncio.get_event_loop() try: results = task.result() diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index fe3483f6..fe4837e3 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -123,13 +123,13 @@ class Executor: """ def __init__(self, dispatcher, skip_updates=None, check_ip=False, retry_after=None, loop=None): - if loop is None: - loop = dispatcher.loop + if loop is not None: + self._loop = loop + self.dispatcher = dispatcher self.skip_updates = skip_updates self.check_ip = check_ip self.retry_after = retry_after - self.loop = loop self._identity = secrets.token_urlsafe(16) self._web_app = None @@ -145,6 +145,10 @@ class Executor: Bot.set_current(dispatcher.bot) Dispatcher.set_current(dispatcher) + @property + def loop(self) -> asyncio.AbstractEventLoop: + return getattr(self, "_loop", asyncio.get_event_loop()) + @property def frozen(self): return self._freeze From 8de05b4b865cfa149879f08e7a1e80700b28da59 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 14 Sep 2020 22:10:51 +0300 Subject: [PATCH 025/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 9a11b7a2..e9ba5b53 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.10' +__version__ = '2.10.1' __api_version__ = '4.9' From 7b33e5c68a5cdf2de54e6e27c8bf65a20716ceb2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 14 Sep 2020 22:56:11 +0300 Subject: [PATCH 026/118] Update docs --- aiogram/contrib/fsm_storage/mongo.py | 4 +- aiogram/contrib/fsm_storage/rethinkdb.py | 5 ++- dev_requirements.txt | 1 + docs/source/dispatcher/filters.rst | 50 +++++++++++----------- docs/source/dispatcher/fsm.rst | 20 +++++++-- docs/source/examples/throtling_example.rst | 4 +- docs/source/examples/webhook_example_2.rst | 8 ++-- docs/source/utils/index.rst | 1 + 8 files changed, 55 insertions(+), 38 deletions(-) diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index a7601cc4..f810a3eb 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -5,9 +5,9 @@ This module has mongo storage for finite-state machine from typing import Union, Dict, Optional, List, Tuple, AnyStr -import pymongo try: + import pymongo import motor from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase except ModuleNotFoundError as e: @@ -26,6 +26,7 @@ COLLECTIONS = (STATE, DATA, BUCKET) class MongoStorage(BaseStorage): """ Mongo-based storage for FSM. + Usage: .. code-block:: python3 @@ -39,7 +40,6 @@ class MongoStorage(BaseStorage): await dp.storage.close() await dp.storage.wait_closed() - """ def __init__(self, host='localhost', port=27017, db_name='aiogram_fsm', uri=None, diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index a9d822d5..b19327ca 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -19,16 +19,17 @@ class RethinkDBStorage(BaseStorage): Usage: - ..code-block:: python3 + .. code-block:: python3 storage = RethinkDBStorage(db='aiogram', table='aiogram', user='aiogram', password='aiogram_secret') dispatcher = Dispatcher(bot, storage=storage) And need to close connection when shutdown - ..code-clock:: python3 + .. code-block:: python3 await storage.close() + await storage.wait_closed() """ diff --git a/dev_requirements.txt b/dev_requirements.txt index c0c2a39d..0252e7e1 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -16,3 +16,4 @@ sphinxcontrib-programoutput>=0.14 aiohttp-socks>=0.3.4 rethinkdb>=2.4.1 coverage==4.5.3 +motor>=2.2.0 diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index f53a4c95..b8f4962e 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -10,7 +10,7 @@ Filter factory greatly simplifies the reuse of filters when registering handlers Filters factory =============== -.. autoclass:: aiogram.dispatcher.filters.factory.FiltersFactory +.. autoclass:: aiogram.dispatcher.filters.FiltersFactory :members: :show-inheritance: @@ -21,28 +21,28 @@ Builtin filters Command ------- -.. autoclass:: aiogram.dispatcher.filters.builtin.Command +.. autoclass:: aiogram.dispatcher.filters.Command :members: :show-inheritance: CommandStart ------------ -.. autoclass:: aiogram.dispatcher.filters.builtin.CommandStart +.. autoclass:: aiogram.dispatcher.filters.CommandStart :members: :show-inheritance: CommandHelp ----------- -.. autoclass:: aiogram.dispatcher.filters.builtin.CommandHelp +.. autoclass:: aiogram.dispatcher.filters.CommandHelp :members: :show-inheritance: CommandSettings --------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.CommandSettings +.. autoclass:: aiogram.dispatcher.filters.CommandSettings :members: :show-inheritance: @@ -50,7 +50,7 @@ CommandSettings CommandPrivacy -------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.CommandPrivacy +.. autoclass:: aiogram.dispatcher.filters.CommandPrivacy :members: :show-inheritance: @@ -58,7 +58,7 @@ CommandPrivacy Text ---- -.. autoclass:: aiogram.dispatcher.filters.builtin.Text +.. autoclass:: aiogram.dispatcher.filters.Text :members: :show-inheritance: @@ -66,7 +66,7 @@ Text HashTag ------- -.. autoclass:: aiogram.dispatcher.filters.builtin.HashTag +.. autoclass:: aiogram.dispatcher.filters.HashTag :members: :show-inheritance: @@ -74,7 +74,7 @@ HashTag Regexp ------ -.. autoclass:: aiogram.dispatcher.filters.builtin.Regexp +.. autoclass:: aiogram.dispatcher.filters.Regexp :members: :show-inheritance: @@ -82,7 +82,7 @@ Regexp RegexpCommandsFilter -------------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.RegexpCommandsFilter +.. autoclass:: aiogram.dispatcher.filters.RegexpCommandsFilter :members: :show-inheritance: @@ -90,21 +90,21 @@ RegexpCommandsFilter ContentTypeFilter ----------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.ContentTypeFilter +.. autoclass:: aiogram.dispatcher.filters.ContentTypeFilter :members: :show-inheritance: IsSenderContact --------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.IsSenderContact +.. autoclass:: aiogram.dispatcher.filters.IsSenderContact :members: :show-inheritance: StateFilter ----------- -.. autoclass:: aiogram.dispatcher.filters.builtin.StateFilter +.. autoclass:: aiogram.dispatcher.filters.StateFilter :members: :show-inheritance: @@ -112,13 +112,13 @@ StateFilter ExceptionsFilter ---------------- -.. autoclass:: aiogram.dispatcher.filters.builtin.ExceptionsFilter +.. autoclass:: aiogram.dispatcher.filters.ExceptionsFilter :members: :show-inheritance: IDFilter ----------------- +-------- .. autoclass:: aiogram.dispatcher.filters.builtin.IDFilter :members: @@ -126,9 +126,9 @@ IDFilter AdminFilter ----------------- +----------- -.. autoclass:: aiogram.dispatcher.filters.builtin.AdminFilter +.. autoclass:: aiogram.dispatcher.filters.AdminFilter :members: :show-inheritance: @@ -136,23 +136,23 @@ AdminFilter IsReplyFilter ------------- -.. autoclass:: aiogram.dispatcher.filters.filters.IsReplyFilter +.. autoclass:: aiogram.dispatcher.filters.IsReplyFilter :members: :show-inheritance: ForwardedMessageFilter -------------- +---------------------- -.. autoclass:: aiogram.dispatcher.filters.filters.ForwardedMessageFilter +.. autoclass:: aiogram.dispatcher.filters.ForwardedMessageFilter :members: :show-inheritance: ChatTypeFilter -------------- +-------------- -.. autoclass:: aiogram.dispatcher.filters.filters.ChatTypeFilter +.. autoclass:: aiogram.dispatcher.filters.ChatTypeFilter :members: :show-inheritance: @@ -170,19 +170,19 @@ Own filter can be: AbstractFilter -------------- -.. autoclass:: aiogram.dispatcher.filters.filters.AbstractFilter +.. autoclass:: aiogram.dispatcher.filters.AbstractFilter :members: :show-inheritance: Filter ------ -.. autoclass:: aiogram.dispatcher.filters.filters.Filter +.. autoclass:: aiogram.dispatcher.filters.Filter :members: :show-inheritance: BoundFilter ----------- -.. autoclass:: aiogram.dispatcher.filters.filters.BoundFilter +.. autoclass:: aiogram.dispatcher.filters.BoundFilter :members: :show-inheritance: diff --git a/docs/source/dispatcher/fsm.rst b/docs/source/dispatcher/fsm.rst index 93c94aa6..1b00e81e 100644 --- a/docs/source/dispatcher/fsm.rst +++ b/docs/source/dispatcher/fsm.rst @@ -12,15 +12,29 @@ Coming soon... Memory storage ~~~~~~~~~~~~~~ -Coming soon... + +.. autoclass:: aiogram.contrib.fsm_storage.memory.MemoryStorage + :show-inheritance: Redis storage ~~~~~~~~~~~~~ -Coming soon... + +.. autoclass:: aiogram.contrib.fsm_storage.redis.RedisStorage + :show-inheritance: + +Mongo storage +~~~~~~~~~~~~~ + +.. autoclass:: aiogram.contrib.fsm_storage.mongo.MongoStorage + :show-inheritance: + Rethink DB storage ~~~~~~~~~~~~~~~~~~ -Coming soon... + +.. autoclass:: aiogram.contrib.fsm_storage.rethinkdb.RethinkDBStorage + :show-inheritance: + Making own storage's ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/examples/throtling_example.rst b/docs/source/examples/throtling_example.rst index eaeb337e..f64eaccc 100644 --- a/docs/source/examples/throtling_example.rst +++ b/docs/source/examples/throtling_example.rst @@ -7,8 +7,8 @@ Throtling example Example for throttling manager. You can use that for flood controlling. -.. literalinclude:: ../../../examples/throtling_example.py - :caption: throtling_example.py +.. literalinclude:: ../../../examples/throttling_example.py + :caption: throttling_example.py :language: python :linenos: :lines: 7- diff --git a/docs/source/examples/webhook_example_2.rst b/docs/source/examples/webhook_example_2.rst index 2ffdfed9..025264e4 100644 --- a/docs/source/examples/webhook_example_2.rst +++ b/docs/source/examples/webhook_example_2.rst @@ -1,10 +1,10 @@ .. Autogenerated file at 2018-09-08 02:07:37.576034 -================= -Webhook example 2 -================= +=================== +Webhook example old +=================== -.. literalinclude:: ../../../examples/webhook_example_2.py +.. literalinclude:: ../../../examples/webhook_example_old.py :caption: webhook_example_2.py :language: python :linenos: diff --git a/docs/source/utils/index.rst b/docs/source/utils/index.rst index 1ac3777c..4865518e 100644 --- a/docs/source/utils/index.rst +++ b/docs/source/utils/index.rst @@ -13,3 +13,4 @@ Utils parts json emoji + deep_linking From 41c104bb5705cf93fce91a82e773a80feef88a28 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sat, 26 Sep 2020 15:22:23 +0300 Subject: [PATCH 027/118] new: add exceptions (#427) * new: add FileIsTooBig exception * new: add MessageCantBeForwarded exception --- aiogram/utils/exceptions.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index a289be25..58e757e9 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -12,6 +12,7 @@ - MessageTextIsEmpty - MessageCantBeEdited - MessageCantBeDeleted + - MessageCantBeForwarded - MessageToEditNotFound - MessageToReplyNotFound - ToMuchMessages @@ -38,6 +39,7 @@ - URLHostIsEmpty - StartParamInvalid - ButtonDataInvalid + - FileIsTooBig - WrongFileIdentifier - GroupDeactivated - BadWebhook @@ -213,6 +215,10 @@ class MessageCantBeDeleted(MessageError): match = 'message can\'t be deleted' +class MessageCantBeForwarded(MessageError): + match = 'message can\'t be forwarded' + + class MessageToEditNotFound(MessageError): match = 'message to edit not found' @@ -347,6 +353,10 @@ class ButtonDataInvalid(BadRequest): text = 'Button data invalid' +class FileIsTooBig(BadRequest): + match = 'File is too big' + + class WrongFileIdentifier(BadRequest): match = 'wrong file identifier/HTTP URL specified' From 1e2fe72aca12a3fc6f2d1f66c71539af5a84ea00 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 26 Sep 2020 15:22:52 +0300 Subject: [PATCH 028/118] #429 added disable_web_page_preview for Message.send_copy(...) (#430) --- aiogram/types/message.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 9d2ad505..fc6bc77b 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -2084,6 +2084,7 @@ class Message(base.TelegramObject): self: Message, chat_id: typing.Union[str, int], disable_notification: typing.Optional[bool] = None, + disable_web_page_preview: typing.Optional[bool] = None, reply_to_message_id: typing.Optional[int] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, None @@ -2094,6 +2095,7 @@ class Message(base.TelegramObject): :param chat_id: :param disable_notification: + :param disable_web_page_preview: for text messages only :param reply_to_message_id: :param reply_markup: :return: @@ -2108,6 +2110,7 @@ class Message(base.TelegramObject): text = self.html_text if (self.text or self.caption) else None if self.text: + kwargs["disable_web_page_preview"] = disable_web_page_preview return await self.bot.send_message(text=text, **kwargs) elif self.audio: return await self.bot.send_audio( From bd5e4d8149274f399cdfc9cef6708928a2aaf6ec Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 28 Oct 2020 19:11:20 +0300 Subject: [PATCH 029/118] MessageToReplyNotFound new text (#447) --- aiogram/utils/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 58e757e9..fe2cc144 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -196,7 +196,7 @@ class MessageToReplyNotFound(MessageError): """ Will be raised when you try to reply to very old or deleted or unknown message. """ - match = 'message to reply not found' + match = 'Reply message not found' class MessageIdentifierNotSpecified(MessageError): From ae67814abbd0f7a34b1364dd91546e526ab680cf Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 28 Oct 2020 19:11:43 +0300 Subject: [PATCH 030/118] fixed BotKicked match (#445) --- aiogram/utils/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index fe2cc144..cc1885dd 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -515,7 +515,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin): class BotKicked(Unauthorized): - match = 'bot was kicked from a chat' + match = 'bot was kicked from' class BotBlocked(Unauthorized): From 4783c4f268eaed456cca930309341a1a1abe5342 Mon Sep 17 00:00:00 2001 From: khamidov <63729380+amalkhamidov@users.noreply.github.com> Date: Sun, 8 Nov 2020 02:22:36 +0500 Subject: [PATCH 031/118] feat(InputFile): adding Pathlib to supporting types for files (#442) * feat(InputFile): adding Pathlib to supporting types for files * Resolve filename via pathlib Co-authored-by: Khamidov Amal Co-authored-by: Alex Root Junior --- aiogram/types/input_file.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index 3c397395..3a78c499 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -4,6 +4,8 @@ import io import logging import os import secrets +from pathlib import Path +from typing import Union import aiohttp @@ -25,7 +27,7 @@ class InputFile(base.TelegramObject): https://core.telegram.org/bots/api#inputfile """ - def __init__(self, path_or_bytesio, filename=None, conf=None): + def __init__(self, path_or_bytesio: Union[str, io.IOBase, Path], filename=None, conf=None): """ :param path_or_bytesio: @@ -45,6 +47,12 @@ class InputFile(base.TelegramObject): elif isinstance(path_or_bytesio, _WebPipe): self._path = None self._file = path_or_bytesio + + elif isinstance(path_or_bytesio, Path): + self._file = path_or_bytesio.open("rb") + self._path = path_or_bytesio.resolve() + if filename is None: + filename = path_or_bytesio.name else: raise TypeError('Not supported file type.') From fc177b567fbb23c202643f377a20889291fa0758 Mon Sep 17 00:00:00 2001 From: Lamroy95 <50185460+Lamroy95@users.noreply.github.com> Date: Sun, 8 Nov 2020 18:45:39 +0300 Subject: [PATCH 032/118] Update quick_start.rst (#439) Handler without filters catches only text messages --- docs/source/quick_start.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index 319886ce..eb8551db 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -24,7 +24,7 @@ Next step: interaction with bots starts with one command. Register your first co :language: python :lines: 20-25 -If you want to handle all messages in the chat simply add handler without filters: +If you want to handle all text messages in the chat simply add handler without filters: .. literalinclude:: ../../examples/echo_bot.py :language: python From 8c43c209e07366dbe62e614d578458d7d7441dc5 Mon Sep 17 00:00:00 2001 From: Groosha Date: Sun, 8 Nov 2020 18:46:28 +0300 Subject: [PATCH 033/118] Added SUPERGROUP chat type (#438) * Added SUPERGROUP chat type, because SUPER_GROUP is incorrect and confusing. * Added deprecation warning to SUPER_GROUP value --- aiogram/types/chat.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 28cc5ed0..7fcea5ea 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -10,7 +10,7 @@ from .chat_member import ChatMember from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .input_file import InputFile -from ..utils.deprecated import deprecated +from ..utils.deprecated import deprecated, DeprecatedReadOnlyClassVar class Chat(base.TelegramObject): @@ -494,6 +494,7 @@ class ChatType(helper.Helper): :key: PRIVATE :key: GROUP :key: SUPER_GROUP + :key: SUPERGROUP :key: CHANNEL """ @@ -501,9 +502,14 @@ class ChatType(helper.Helper): PRIVATE = helper.Item() # private GROUP = helper.Item() # group - SUPER_GROUP = helper.Item() # supergroup + SUPERGROUP = helper.Item() # supergroup CHANNEL = helper.Item() # channel + SUPER_GROUP: DeprecatedReadOnlyClassVar[ChatType, helper.Item] \ + = DeprecatedReadOnlyClassVar( + "SUPER_GROUP chat type is deprecated, use SUPERGROUP instead.", + new_value_getter=lambda cls: cls.SUPERGROUP) + @staticmethod def _check(obj, chat_types) -> bool: if hasattr(obj, 'chat'): @@ -543,7 +549,7 @@ class ChatType(helper.Helper): :param obj: :return: """ - return cls._check(obj, [cls.SUPER_GROUP]) + return cls._check(obj, [cls.SUPER_GROUP, cls.SUPERGROUP]) @classmethod @deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0") @@ -554,7 +560,7 @@ class ChatType(helper.Helper): :param obj: :return: """ - return cls._check(obj, [cls.GROUP, cls.SUPER_GROUP]) + return cls._check(obj, [cls.GROUP, cls.SUPER_GROUP, cls.SUPERGROUP]) @classmethod @deprecated("This filter was moved to ChatTypeFilter, and will be removed in aiogram v3.0") From 2b060d9ad4e7e072fc1e6ed301867e90c6b4cdbc Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 8 Nov 2020 18:51:39 +0300 Subject: [PATCH 034/118] Bot API 5.0 (#454) * increased Telegram Bot API version * AIOG-T-64 added logOut method * AIOG-T-64 added logOut method test * AIOG-T-64 logOut type annotation fix * AIOG-T-65 added close (close_bot) method * AIOG-T-65 old `close` method deprecation warn * AIOG-T-65 `close_bot` test added * AIOG-T-67 added ip_address param to set_webhook, updated docs * updated deprecation text Co-authored-by: Martin Winks * AIOG-T-69 param `drop_pending_updates` added in methods `setWebhook` and `deleteWebhook` * AIOG-T-71 new `ChatLocation` class * AIOG-T-70 updated `Chat` class: bio, linked chats, location * AIOG-T-68 field `ip_address` added to class `WebhookInfo` * AIOG-T-72 param `only_if_banned` added to `unbanChatMember` method * AIOG-T-72 updated Chat.unban shortcut * AIOG-T-73 field `file_name` added to `Audio` and `Video` classes * AIOG-T-74 param `disable_content_type_detection` added in `sendDocument` method and `InputMediaDocument` class * AIOG-T-75 Added the ability to pin messages in private chats (docs update) * AIOG-T-76 Added the parameter message_id to the method unpinChatMessage to allow unpinning of the specific pinned message * AIOG-T-77 Added the method unpinAllChatMessages, which can be used to unpin all pinned messages in a chat. * AIOG-T-78 updated send_media_group description; added media qty check * AIOG-T-80 field `live_period` added to `Location` class * AIOG-T-81 Added support for live location heading * AIOG-T-82 added the field proximity_alert_distance to the classes Location, InlineQueryResultLocation, InputLocationMessageContent; fixed heading in InputLocationMessageContent * AIOG-T-82 added parameter proximity_alert_distance to the methods sendLocation and editMessageLiveLocation * AIOG-T-83 Added the type ProximityAlertTriggered * AIOG-T-83 Added field proximity_alert_triggered to the class Message * AIOG-T-84 Added the field horizontal_accuracy to the classes Location, InlineQueryResultLocation, InputLocationMessageContent * AIOG-T-84 Added the parameter horizontal_accuracy to the methods sendLocation and editMessageLiveLocation. * Added live_period to InputLocationMessageContent (missed?) * AIOG-T-85 Added the field sender_chat to the class Message * AIOG-T-86 Added `is_anonymous` field to `chatMember` class * AIOG-T-87 Added the parameter is_anonymous to the method promoteChatMember * AIOG-T-89 Added the method `copyMessage` * AIOG-T-90 Poll docs update * AIOG-T-91 ability to manually specify text entities * AIOG-T-92 Google Places as a venue API provider * AIOG-T-93 Added the field allow_sending_without_reply to the methods * AIOG-T-94 football and slot machine dice * removed Optional Co-authored-by: Ramzan Bekbulatov * Apply suggestions from code review removed Optional Co-authored-by: Ramzan Bekbulatov * Don't use deprecated Bot.close method from dispatcher (Replaced by session.close) * Fix copyMessage method, update alias (with deprecation) Fix imports * AIOG-T-79: Easy way to use custom API server * Update docs * Bump requirements * Rollback email * AIOG-T-93 allow_sending_without_reply to send_message shortcuts * AIOG-T-93 added allow_sending_without_reply to send_photo shortcuts * AIOG-T-93 added allow_sending_without_reply to send_video shortcuts * Union[type, None] -> Optional[type] refactoring * AIOG-T-93 added allow_sending_without_reply to send_animation shortcuts * added type hint to reply field * AIOG-T-93 added allow_sending_without_reply to send_audio shortcuts * AIOG-T-93 added allow_sending_without_reply to send_document shortcuts * AIOG-T-93 added allow_sending_without_reply to send_sticker shortcuts * AIOG-T-93 added allow_sending_without_reply to send_video_note shortcuts * AIOG-T-93 added allow_sending_without_reply to send_voice shortcuts * AIOG-T-93 added allow_sending_without_reply to send_location shortcuts * AIOG-T-93 added allow_sending_without_reply to send_venue shortcuts * AIOG-T-93 added allow_sending_without_reply to send_contact shortcuts * AIOG-T-93 added allow_sending_without_reply to send_poll shortcuts * AIOG-T-93 added allow_sending_without_reply to send_dice shortcuts * AIOG-T-93 added allow_sending_without_reply to send_media_group shortcuts * AIOG-T-92 added google_place_ to send_venue shortcuts * AIOG-T-91 added entities to send_message shortcuts * AIOG-T-91 added caption_entities to send_photo shortcuts * AIOG-T-91 added caption_entities to send_video shortcuts * AIOG-T-91 added caption_entities to send_animation shortcuts * AIOG-T-91 added caption_entities to send_audio shortcuts * AIOG-T-91 added caption_entities to send_document shortcuts * AIOG-T-91 added caption_entities to send_voice shortcuts * AIOG-T-91 added explanation_parse_mode to send_poll shortcuts * AIOG-T-91 added entities to edit_message_text shortcuts * AIOG-T-91 added caption_entities to edit_message_caption shortcuts * fixed types.MessageEntity -> MessageEntity in docs Co-authored-by: Martin Winks Co-authored-by: Ramzan Bekbulatov Co-authored-by: Alex Root Junior --- README.rst | 2 +- aiogram/__init__.py | 2 +- aiogram/bot/api.py | 80 +- aiogram/bot/base.py | 14 +- aiogram/bot/bot.py | 1455 ++++++++++++----- aiogram/contrib/fsm_storage/rethinkdb.py | 2 +- aiogram/dispatcher/storage.py | 2 +- aiogram/dispatcher/webhook.py | 8 +- aiogram/types/__init__.py | 6 + aiogram/types/audio.py | 1 + aiogram/types/callback_query.py | 16 +- aiogram/types/chat.py | 154 +- aiogram/types/chat_location.py | 16 + aiogram/types/chat_member.py | 3 +- aiogram/types/dice.py | 2 + aiogram/types/inline_query.py | 20 +- aiogram/types/inline_query_result.py | 571 ++++--- aiogram/types/input_media.py | 123 +- aiogram/types/input_message_content.py | 71 +- aiogram/types/location.py | 6 + aiogram/types/message.py | 1308 +++++++++++---- aiogram/types/message_id.py | 10 + aiogram/types/proximity_alert_triggered.py | 15 + aiogram/types/venue.py | 2 + aiogram/types/video.py | 1 + aiogram/types/webhook_info.py | 1 + aiogram/utils/executor.py | 2 +- .../examples/advanced_executor_example.rst | 20 - docs/source/examples/broadcast_example.rst | 2 - docs/source/examples/check_user_language.rst | 3 - docs/source/examples/echo_bot.rst | 3 +- .../examples/finite_state_machine_example.rst | 2 - docs/source/examples/i18n_example.rst | 20 - docs/source/examples/index.rst | 1 + docs/source/examples/inline_bot.rst | 2 - docs/source/examples/local_server.rst | 8 + docs/source/examples/media_group.rst | 2 - .../examples/middleware_and_antiflood.rst | 2 - docs/source/examples/payments.rst | 2 - docs/source/examples/proxy_and_emojize.rst | 2 - .../regexp_commands_filter_example.rst | 2 - docs/source/examples/throtling_example.rst | 12 +- docs/source/examples/webhook_example.rst | 5 - docs/source/examples/webhook_example_2.rst | 2 - docs/source/index.rst | 2 +- examples/local_server.py | 27 + requirements.txt | 6 +- setup.py | 7 +- tests/test_bot.py | 16 + 49 files changed, 2847 insertions(+), 1194 deletions(-) create mode 100644 aiogram/types/chat_location.py create mode 100644 aiogram/types/message_id.py create mode 100644 aiogram/types/proximity_alert_triggered.py create mode 100644 docs/source/examples/local_server.rst create mode 100644 examples/local_server.py diff --git a/README.rst b/README.rst index 1cf2765d..09072c81 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.9-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.0-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 9a11b7a2..1c66de7e 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -44,4 +44,4 @@ __all__ = ( ) __version__ = '2.10' -__api_version__ = '4.9' +__api_version__ = '5.0' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index cb258cdf..03c7e209 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -1,20 +1,57 @@ import logging import os +from dataclasses import dataclass from http import HTTPStatus import aiohttp from .. import types -from ..utils import exceptions -from ..utils import json +from ..utils import exceptions, json from ..utils.helper import Helper, HelperMode, Item # Main aiogram logger log = logging.getLogger('aiogram') -# API Url's -API_URL = "https://api.telegram.org/bot{token}/{method}" -FILE_URL = "https://api.telegram.org/file/bot{token}/{path}" + +@dataclass(frozen=True) +class TelegramAPIServer: + """ + Base config for API Endpoints + """ + + base: str + file: str + + def api_url(self, token: str, method: str) -> str: + """ + Generate URL for API methods + + :param token: Bot token + :param method: API method name (case insensitive) + :return: URL + """ + return self.base.format(token=token, method=method) + + def file_url(self, token: str, path: str) -> str: + """ + Generate URL for downloading files + + :param token: Bot token + :param path: file path + :return: URL + """ + return self.file.format(token=token, path=path) + + @classmethod + def from_base(cls, base: str) -> 'TelegramAPIServer': + base = base.rstrip("/") + return cls( + base=f"{base}/bot{{token}}/{{method}}", + file=f"{base}/file/bot{{token}}/{{method}}", + ) + + +TELEGRAM_PRODUCTION = TelegramAPIServer.from_base("https://api.telegram.org") def check_token(token: str) -> bool: @@ -92,11 +129,10 @@ def check_result(method_name: str, content_type: str, status_code: int, body: st raise exceptions.TelegramAPIError(f"{description} [{status_code}]") -async def make_request(session, token, method, data=None, files=None, **kwargs): - # log.debug(f"Make request: '{method}' with data: {data} and files {files}") +async def make_request(session, server, token, method, data=None, files=None, **kwargs): log.debug('Make request: "%s" with data: "%r" and files "%r"', method, data, files) - url = Methods.api_url(token=token, method=method) + url = server.api_url(token=token, method=method) req = compose_data(data, files) try: @@ -153,7 +189,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 4.9 + List is updated to Bot API 5.0 """ mode = HelperMode.lowerCamelCase @@ -165,8 +201,11 @@ class Methods(Helper): # Available methods GET_ME = Item() # getMe + LOG_OUT = Item() # logOut + CLOSE = Item() # close SEND_MESSAGE = Item() # sendMessage FORWARD_MESSAGE = Item() # forwardMessage + COPY_MESSAGE = Item() # copyMessage SEND_PHOTO = Item() # sendPhoto SEND_AUDIO = Item() # sendAudio SEND_DOCUMENT = Item() # sendDocument @@ -198,6 +237,7 @@ class Methods(Helper): SET_CHAT_DESCRIPTION = Item() # setChatDescription PIN_CHAT_MESSAGE = Item() # pinChatMessage UNPIN_CHAT_MESSAGE = Item() # unpinChatMessage + UNPIN_ALL_CHAT_MESSAGES = Item() # unpinAllChatMessages LEAVE_CHAT = Item() # leaveChat GET_CHAT = Item() # getChat GET_CHAT_ADMINISTRATORS = Item() # getChatAdministrators @@ -242,25 +282,3 @@ class Methods(Helper): SEND_GAME = Item() # sendGame SET_GAME_SCORE = Item() # setGameScore GET_GAME_HIGH_SCORES = Item() # getGameHighScores - - @staticmethod - def api_url(token, method): - """ - Generate API URL with included token and method name - - :param token: - :param method: - :return: - """ - return API_URL.format(token=token, method=method) - - @staticmethod - def file_url(token, path): - """ - Generate File URL with included token and file path - - :param token: - :param path: - :return: - """ - return FILE_URL.format(token=token, path=path) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index f45546c3..07e44c1c 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -12,9 +12,11 @@ import certifi from aiohttp.helpers import sentinel from . import api +from .api import TelegramAPIServer, TELEGRAM_PRODUCTION from ..types import ParseMode, base from ..utils import json from ..utils.auth_widget import check_integrity +from ..utils.deprecated import deprecated class BaseBot: @@ -33,7 +35,8 @@ class BaseBot: proxy_auth: Optional[aiohttp.BasicAuth] = None, validate_token: Optional[base.Boolean] = True, parse_mode: typing.Optional[base.String] = None, - timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None + timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None, + server: TelegramAPIServer = TELEGRAM_PRODUCTION ): """ Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot @@ -54,6 +57,8 @@ class BaseBot: :type parse_mode: :obj:`str` :param timeout: Request timeout :type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]` + :param server: Telegram Bot API Server endpoint. + :type server: :obj:`TelegramAPIServer` :raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError` """ self._main_loop = loop @@ -64,6 +69,7 @@ class BaseBot: self._token = None self.__token = token self.id = int(token.split(sep=':')[0]) + self.server = server self.proxy = proxy self.proxy_auth = proxy_auth @@ -173,6 +179,8 @@ class BaseBot: finally: self._ctx_token.reset(token) + @deprecated("This method's behavior will be changed in aiogram v3.0. " + "More info: https://core.telegram.org/bots/api#close", stacklevel=3) async def close(self): """ Close all client sessions @@ -197,7 +205,7 @@ class BaseBot: :rtype: Union[List, Dict] :raise: :obj:`aiogram.exceptions.TelegramApiError` """ - return await api.make_request(self.session, self.__token, method, data, files, + return await api.make_request(self.session, self.server, self.__token, method, data, files, proxy=self.proxy, proxy_auth=self.proxy_auth, timeout=self.timeout, **kwargs) async def download_file(self, file_path: base.String, @@ -237,7 +245,7 @@ class BaseBot: return dest def get_file_url(self, file_path): - return api.Methods.file_url(token=self.__token, path=file_path) + return self.server.file_url(token=self.__token, path=file_path) async def send_file(self, file_type, method, file, payload) -> Union[Dict, base.Boolean]: """ diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index c5678a37..0ac13702 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -7,6 +7,8 @@ import warnings from .base import BaseBot, api from .. import types from ..types import base +from ..utils.deprecated import deprecated +from ..utils.exceptions import ValidationError from ..utils.mixins import DataMixin, ContextInstanceMixin from ..utils.payload import generate_payload, prepare_arg, prepare_attachment, prepare_file @@ -64,9 +66,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): # === Getting updates === # https://core.telegram.org/bots/api#getting-updates - async def get_updates(self, offset: typing.Union[base.Integer, None] = None, - limit: typing.Union[base.Integer, None] = None, - timeout: typing.Union[base.Integer, None] = None, + async def get_updates(self, offset: typing.Optional[base.Integer] = None, + limit: typing.Optional[base.Integer] = None, + timeout: typing.Optional[base.Integer] = None, allowed_updates: typing.Union[typing.List[base.String], None] = None) -> typing.List[types.Update]: """ @@ -79,11 +81,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): Source: https://core.telegram.org/bots/api#getupdates :param offset: Identifier of the first update to be returned - :type offset: :obj:`typing.Union[base.Integer, None]` + :type offset: :obj:`typing.Optional[base.Integer]` :param limit: Limits the number of updates to be retrieved - :type limit: :obj:`typing.Union[base.Integer, None]` + :type limit: :obj:`typing.Optional[base.Integer]` :param timeout: Timeout in seconds for long polling - :type timeout: :obj:`typing.Union[base.Integer, None]` + :type timeout: :obj:`typing.Optional[base.Integer]` :param allowed_updates: List the types of updates you want your bot to receive :type allowed_updates: :obj:`typing.Union[typing.List[base.String], None]` :return: An Array of Update objects is returned @@ -95,27 +97,61 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_UPDATES, payload) return [types.Update(**update) for update in result] - async def set_webhook(self, url: base.String, - certificate: typing.Union[base.InputFile, None] = None, - max_connections: typing.Union[base.Integer, None] = None, - allowed_updates: typing.Union[typing.List[base.String], None] = None) -> base.Boolean: + async def set_webhook(self, + url: base.String, + certificate: typing.Optional[base.InputFile] = None, + ip_address: typing.Optional[base.String] = None, + max_connections: typing.Optional[base.Integer] = None, + allowed_updates: typing.Optional[typing.List[base.String]] = None, + drop_pending_updates: typing.Optional[base.Boolean] = None, + ) -> base.Boolean: """ - Use this method to specify a url and receive incoming updates via an outgoing webhook. - Whenever there is an update for the bot, we will send an HTTPS POST request to the specified url, - containing a JSON-serialized Update. In case of an unsuccessful request, - we will give up after a reasonable amount of attempts. + Use this method to specify a url and receive incoming updates via an outgoing + webhook. Whenever there is an update for the bot, we will send an HTTPS POST + request to the specified url, containing a JSON-serialized Update. In case + of an unsuccessful request, we will give up after a reasonable amount of + attempts. Returns True on success. + + If you'd like to make sure that the Webhook request comes from Telegram, + we recommend using a secret path in the URL, e.g. + `https://www.example.com/`. + Since nobody else knows your bot's token, you can be pretty sure it's us. Source: https://core.telegram.org/bots/api#setwebhook - :param url: HTTPS url to send updates to. Use an empty string to remove webhook integration + :param url: HTTPS url to send updates to. Use an empty string to remove + webhook integration :type url: :obj:`base.String` - :param certificate: Upload your public key certificate so that the root certificate in use can be checked - :type certificate: :obj:`typing.Union[base.InputFile, None]` - :param max_connections: Maximum allowed number of simultaneous HTTPS connections to the webhook - for update delivery, 1-100. - :type max_connections: :obj:`typing.Union[base.Integer, None]` - :param allowed_updates: List the types of updates you want your bot to receive - :type allowed_updates: :obj:`typing.Union[typing.List[base.String], None]` + + :param certificate: Upload your public key certificate so that the root + certificate in use can be checked. See our self-signed guide for details: + https://core.telegram.org/bots/self-signed + :type certificate: :obj:`typing.Optional[base.InputFile]` + + :param ip_address: The fixed IP address which will be used to send webhook + requests instead of the IP address resolved through DNS + :type ip_address: :obj:`typing.Optional[base.String]` + + :param max_connections: Maximum allowed number of simultaneous HTTPS + connections to the webhook for update delivery, 1-100. Defaults to 40. + Use lower values to limit the load on your bot's server, and higher + values to increase your bot's throughput. + :type max_connections: :obj:`typing.Optional[base.Integer]` + + :param allowed_updates: A list of the update types you want your bot to + receive. For example, specify [โ€œmessageโ€, โ€œedited_channel_postโ€, + โ€œcallback_queryโ€] to only receive updates of these types. See Update for + a complete list of available update types. Specify an empty list to + receive all updates regardless of type (default). If not specified, the + previous setting will be used. + Please note that this parameter doesn't affect updates created before the + call to the setWebhook, so unwanted updates may be received for a short + period of time. + :type allowed_updates: :obj:`typing.Optional[typing.List[base.String]]` + + :param drop_pending_updates: Pass True to drop all pending updates + :type drop_pending_updates: :obj:`typing.Optional[base.Boolean]` + :return: Returns true :rtype: :obj:`base.Boolean` """ @@ -128,13 +164,18 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SET_WEBHOOK, payload, files) return result - async def delete_webhook(self) -> base.Boolean: + async def delete_webhook(self, + drop_pending_updates: typing.Optional[base.Boolean] = None, + ) -> base.Boolean: """ - Use this method to remove webhook integration if you decide to switch back to getUpdates. - Returns True on success. Requires no parameters. + Use this method to remove webhook integration if you decide to switch back + to getUpdates. Returns True on success. Source: https://core.telegram.org/bots/api#deletewebhook + :param drop_pending_updates: Pass True to drop all pending updates + :type drop_pending_updates: :obj:`typing.Optional[base.Boolean]` + :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -176,15 +217,57 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_ME, payload) return types.User(**result) - async def send_message(self, chat_id: typing.Union[base.Integer, base.String], 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_to_message_id: typing.Union[base.Integer, None] = None, + async def log_out(self) -> base.Boolean: + """ + Use this method to log out from the cloud Bot API server before launching + the bot locally. You **must** log out the bot before running it locally, + otherwise there is no guarantee that the bot will receive updates. + After a successful call, you will not be able to log in again using the + same token for 10 minutes. Returns True on success. Requires no parameters. + + Source: https://core.telegram.org/bots/api#logout + + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.LOG_OUT, payload) + return result + + @deprecated("This method will be renamed to `close` in aiogram v3.0") + async def close_bot(self) -> base.Boolean: + """ + Use this method to close the bot instance before moving it from one local + server to another. You need to delete the webhook before calling this method + to ensure that the bot isn't launched again after server restart. The method + will return error 429 in the first 10 minutes after the bot is launched. + Returns True on success. Requires no parameters. + + Source: https://core.telegram.org/bots/api#close + + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.CLOSE, payload) + return result + + async def send_message(self, + chat_id: typing.Union[base.Integer, base.String], + text: base.String, + parse_mode: typing.Optional[base.String] = None, + entities: typing.Optional[typing.List[types.MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + ) -> types.Message: """ Use this method to send text messages. @@ -192,21 +275,36 @@ 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 text: Text of the message to be sent :type text: :obj:`base.String` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + :param disable_web_page_preview: Disables link previews for links in this message - :type disable_web_page_preview: :obj:`typing.Union[base.Boolean, None]` + :type disable_web_page_preview: :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.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -221,7 +319,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def forward_message(self, chat_id: typing.Union[base.Integer, base.String], from_chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer, - disable_notification: typing.Union[base.Boolean, None] = None) -> types.Message: + disable_notification: typing.Optional[base.Boolean] = None) -> types.Message: """ Use this method to forward messages of any kind. @@ -232,7 +330,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param from_chat_id: Unique identifier for the chat where the original message was sent :type from_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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` :param message_id: Message identifier in the chat specified in from_chat_id :type message_id: :obj:`base.Integer` :return: On success, the sent Message is returned @@ -243,16 +341,97 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.FORWARD_MESSAGE, payload) return types.Message(**result) - async def send_photo(self, chat_id: typing.Union[base.Integer, base.String], + async def copy_message(self, + chat_id: typing.Union[base.Integer, base.String], + from_chat_id: typing.Union[base.Integer, base.String], + message_id: base.Integer, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, + types.ReplyKeyboardRemove, + types.ForceReply, None] = None, + ) -> types.MessageId: + """ + Use this method to copy messages of any kind. The method is analogous to the + method forwardMessages, but the copied message doesn't have a link to the + original message. Returns the MessageId of the sent message on success. + + Source: https://core.telegram.org/bots/api#copymessage + + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + + :param from_chat_id: Unique identifier for the chat where the original + message was sent (or channel username in the format @channelusername) + :type from_chat_id: :obj:`typing.Union[base.Integer, base.String]` + + :param message_id: Message identifier in the chat specified in from_chat_id + :type message_id: :obj:`base.Integer` + + :param caption: New caption for media, 0-1024 characters after entities + parsing. If not specified, the original caption is kept + :type caption: :obj:`typing.Optional[base.String]` + + :param parse_mode: Mode for parsing entities in the new caption. See + formatting options for more details: + https://core.telegram.org/bots/api#formatting-options + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in the new + caption, which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param reply_to_message_id: If the message is a reply, ID of the original + message + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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()) + + if self.parse_mode: + payload.setdefault('parse_mode', self.parse_mode) + + result = await self.request(api.Methods.COPY_MESSAGE, payload) + return types.MessageId(**result) + + async def send_photo(self, + chat_id: typing.Union[base.Integer, base.String], 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_to_message_id: typing.Union[base.Integer, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + ) -> types.Message: """ Use this method to send photos. @@ -260,21 +439,36 @@ 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 photo: Photo to send :type photo: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Photo caption (may also be used when resending photos by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -289,20 +483,24 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_PHOTO, payload, files) return types.Message(**result) - async def send_audio(self, chat_id: typing.Union[base.Integer, base.String], + async def send_audio(self, + chat_id: typing.Union[base.Integer, base.String], 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + performer: typing.Optional[base.String] = None, + title: typing.Optional[base.String] = None, thumb: typing.Union[base.InputFile, base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_to_message_id: typing.Union[base.Integer, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + ) -> types.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. @@ -313,29 +511,48 @@ 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 audio: Audio file to send :type audio: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Audio caption, 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + :param duration: Duration of the audio in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param performer: Performer - :type performer: :obj:`typing.Union[base.String, None]` + :type performer: :obj:`typing.Optional[base.String]` + :param title: Track name - :type title: :obj:`typing.Union[base.String, None]` + :type title: :obj:`typing.Optional[base.String]` + :param thumb: Thumbnail of the file sent :type thumb: :obj:`typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -351,43 +568,75 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_AUDIO, payload, files) return types.Message(**result) - async def send_document(self, chat_id: typing.Union[base.Integer, base.String], + async def send_document(self, + chat_id: typing.Union[base.Integer, base.String], document: typing.Union[base.InputFile, base.String], thumb: 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_to_message_id: typing.Union[base.Integer, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + disable_content_type_detection: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, + None] = None, + ) -> types.Message: """ - Use this method to send general files. - - Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. + Use this method to send general files. On success, the sent Message is + returned. Bots can currently send files of any type of up to 50 MB in size, + this limit may be changed in the future. Source: https://core.telegram.org/bots/api#senddocument - :param chat_id: Unique identifier for the target chat or username of the target channel + :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 document: File to send :type document: :obj:`typing.Union[base.InputFile, base.String]` + :param thumb: Thumbnail of the file sent :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` - :param caption: Document caption (may also be used when resending documents by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` - :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, - fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_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]` + + :param caption: Document caption (may also be used when resending documents + by file_id), 0-1024 characters + :type caption: :obj:`typing.Optional[base.String]` + + :param disable_content_type_detection: Disables automatic server-side content + type detection for files uploaded using multipart/form-data + :type disable_content_type_detection: :obj:`typing.Optional[base.Boolean]` + + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in your bot's message. + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + + :param disable_notification: Sends the message silently. Users will receive a + notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param reply_to_message_id: If the message is a reply, ID of the original + message + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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` """ @@ -405,15 +654,17 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_video(self, chat_id: typing.Union[base.Integer, base.String], 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, + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = None, thumb: typing.Union[base.InputFile, base.String, None] = None, - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - supports_streaming: typing.Union[base.Boolean, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_to_message_id: typing.Union[base.Integer, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + supports_streaming: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, @@ -426,31 +677,51 @@ 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 video: Video to send :type video: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent video in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param width: Video width - :type width: :obj:`typing.Union[base.Integer, None]` + :type width: :obj:`typing.Optional[base.Integer]` + :param height: Video height - :type height: :obj:`typing.Union[base.Integer, None]` + :type height: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + :param supports_streaming: Pass True, if the uploaded video is suitable for streaming - :type supports_streaming: :obj:`typing.Union[base.Boolean, None]` + :type supports_streaming: :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.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -469,18 +740,20 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_animation(self, chat_id: typing.Union[base.Integer, base.String], 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, + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = 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_to_message_id: typing.Union[base.Integer, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply], None] = None + types.ForceReply], None] = None, ) -> types.Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -493,32 +766,51 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param animation: Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data :type animation: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent animation in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param width: Animation width - :type width: :obj:`typing.Union[base.Integer, None]` + :type width: :obj:`typing.Optional[base.Integer]` + :param height: Animation height - :type height: :obj:`typing.Union[base.Integer, None]` + :type height: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` + :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -534,17 +826,21 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_ANIMATION, payload, files) return types.Message(**result) - async def send_voice(self, chat_id: typing.Union[base.Integer, base.String], + async def send_voice(self, + chat_id: typing.Union[base.Integer, base.String], 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_to_message_id: typing.Union[base.Integer, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + ) -> types.Message: """ Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. @@ -556,23 +852,39 @@ 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 voice: Audio file to send :type voice: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Voice message caption, 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + :param duration: Duration of the voice message in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -589,11 +901,12 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_video_note(self, chat_id: typing.Union[base.Integer, base.String], video_note: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - length: typing.Union[base.Integer, None] = None, + duration: typing.Optional[base.Integer] = None, + length: typing.Optional[base.Integer] = None, thumb: typing.Union[base.InputFile, base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_to_message_id: typing.Union[base.Integer, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, @@ -606,22 +919,34 @@ 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 video_note: Video note to send :type video_note: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent video in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param length: Video width and height - :type length: :obj:`typing.Union[base.Integer, None]` + :type length: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent :type thumb: :obj:`typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -634,24 +959,41 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_VIDEO_NOTE, payload, files) return types.Message(**result) - async def send_media_group(self, chat_id: typing.Union[base.Integer, base.String], + async def send_media_group(self, + chat_id: typing.Union[base.Integer, base.String], media: typing.Union[types.MediaGroup, typing.List], - disable_notification: typing.Union[base.Boolean, None] = None, - reply_to_message_id: typing.Union[base.Integer, - None] = None) -> typing.List[types.Message]: + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + ) -> typing.List[types.Message]: """ - Use this method to send a group of photos or videos as an album. + Use this method to send a group of photos, videos, documents or audios as + an album. Documents and audio files can be only group in an album with + messages of the same type. On success, an array of Messages that were sent + is returned. Source: https://core.telegram.org/bots/api#sendmediagroup - :param chat_id: Unique identifier for the target chat or username of the target channel + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` - :param media: A JSON-serialized array describing photos and videos to be sent + + :param media: A JSON-serialized array describing messages to be sent, must + include 2-10 items :type media: :obj:`typing.Union[types.MediaGroup, typing.List]` - :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 disable_notification: Sends messages silently. Users will receive a + notification with no sound. + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param reply_to_message_id: If the messages are a reply, ID of the original + message + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :return: On success, an array of the sent Messages is returned :rtype: typing.List[types.Message] """ @@ -659,6 +1001,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): if isinstance(media, list): media = types.MediaGroup(media) + # check MediaGroup quantity + if 2 > len(media.media) > 10: + raise ValidationError("Media group must include 2-10 items") + files = dict(media.get_files()) media = prepare_arg(media) @@ -669,9 +1015,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_location(self, chat_id: typing.Union[base.Integer, base.String], latitude: base.Float, longitude: base.Float, - live_period: typing.Union[base.Integer, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_to_message_id: typing.Union[base.Integer, None] = None, + horizontal_accuracy: typing.Optional[base.Float] = None, + live_period: typing.Optional[base.Integer] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, @@ -683,20 +1033,44 @@ 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 latitude: Latitude of the location :type latitude: :obj:`base.Float` + :param longitude: Longitude of the location :type longitude: :obj:`base.Float` + + :param horizontal_accuracy: The radius of uncertainty for the location, + measured in meters; 0-1500 + :type horizontal_accuracy: :obj:`typing.Optional[base.Float]` + :param live_period: Period in seconds for which the location will be updated - :type live_period: :obj:`typing.Union[base.Integer, None]` + :type live_period: :obj:`typing.Optional[base.Integer]` + + :param heading: For live locations, a direction in which the user is moving, + in degrees. Must be between 1 and 360 if specified. + :type heading: :obj:`typing.Optional[base.Integer]` + + :param proximity_alert_radius: For live locations, a maximum distance for + proximity alerts about approaching another chat member, in meters. Must + be between 1 and 100000 if specified. + :type proximity_alert_radius: :obj:`typing.Optional[base.Integer]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -706,12 +1080,17 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_LOCATION, payload) return types.Message(**result) - async def edit_message_live_location(self, latitude: base.Float, longitude: base.Float, + async def edit_message_live_location(self, + latitude: base.Float, + longitude: base.Float, chat_id: typing.Union[base.Integer, base.String, None] = None, - message_id: typing.Union[base.Integer, None] = None, - inline_message_id: typing.Union[base.String, None] = None, - reply_markup: typing.Union[types.InlineKeyboardMarkup, - None] = None) -> types.Message or base.Boolean: + message_id: typing.Optional[base.Integer] = None, + inline_message_id: typing.Optional[base.String] = None, + horizontal_accuracy: typing.Optional[base.Float] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, + reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None, + ) -> types.Message or 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 @@ -721,16 +1100,35 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Required if inline_message_id is not specified :type chat_id: :obj:`typing.Union[base.Integer, base.String, None]` + :param message_id: Required if inline_message_id is not specified. Identifier of the sent message - :type message_id: :obj:`typing.Union[base.Integer, None]` + :type message_id: :obj:`typing.Optional[base.Integer]` + :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :type inline_message_id: :obj:`typing.Union[base.String, None]` + :type inline_message_id: :obj:`typing.Optional[base.String]` + :param latitude: Latitude of new location :type latitude: :obj:`base.Float` + :param longitude: Longitude of new location :type longitude: :obj:`base.Float` + + :param horizontal_accuracy: The radius of uncertainty for the location, + measured in meters; 0-1500 + :type horizontal_accuracy: :obj:`typing.Optional[base.Float]` + + :param heading: Direction in which the user is moving, in degrees. Must be + between 1 and 360 if specified. + :type heading: :obj:`typing.Optional[base.Integer]` + + :param proximity_alert_radius: For live locations, a maximum distance for + proximity alerts about approaching another chat member, in meters. Must + be between 1 and 100000 if specified. + :type proximity_alert_radius: :obj:`typing.Optional[base.Integer]` + :param reply_markup: A JSON-serialized object for a new inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + :return: On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -745,8 +1143,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def stop_message_live_location(self, chat_id: typing.Union[base.Integer, base.String, None] = None, - message_id: typing.Union[base.Integer, None] = None, - inline_message_id: typing.Union[base.String, None] = None, + message_id: typing.Optional[base.Integer] = None, + inline_message_id: typing.Optional[base.String] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, None] = None) -> types.Message or base.Boolean: """ @@ -758,11 +1156,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Required if inline_message_id is not specified :type chat_id: :obj:`typing.Union[base.Integer, base.String, None]` :param message_id: Required if inline_message_id is not specified. Identifier of the sent message - :type message_id: :obj:`typing.Union[base.Integer, None]` + :type message_id: :obj:`typing.Optional[base.Integer]` :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :type inline_message_id: :obj:`typing.Union[base.String, None]` + :type inline_message_id: :obj:`typing.Optional[base.String]` :param reply_markup: A JSON-serialized object for a new inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` :return: On success, if the message was sent by the bot, the sent Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -775,44 +1173,77 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return result return types.Message(**result) - async def send_venue(self, chat_id: typing.Union[base.Integer, base.String], - latitude: base.Float, longitude: base.Float, - title: base.String, address: base.String, - foursquare_id: typing.Union[base.String, None] = None, - foursquare_type: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_to_message_id: typing.Union[base.Integer, None] = None, + async def send_venue(self, + chat_id: typing.Union[base.Integer, base.String], + latitude: base.Float, + longitude: base.Float, + title: base.String, + address: base.String, + foursquare_id: typing.Optional[base.String] = None, + foursquare_type: typing.Optional[base.String] = None, + google_place_id: typing.Optional[base.String] = None, + google_place_type: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + ) -> types.Message: """ Use this method to send information about a venue. Source: https://core.telegram.org/bots/api#sendvenue - :param chat_id: Unique identifier for the target chat or username of the target channel + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param latitude: Latitude of the venue :type latitude: :obj:`base.Float` + :param longitude: Longitude of the venue :type longitude: :obj:`base.Float` + :param title: Name of the venue :type title: :obj:`base.String` + :param address: Address of the venue :type address: :obj:`base.String` + :param foursquare_id: Foursquare identifier of the venue - :type foursquare_id: :obj:`typing.Union[base.String, None]` + :type foursquare_id: :obj:`typing.Optional[base.String]` + :param foursquare_type: Foursquare type of the venue, if known - :type foursquare_type: :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 - :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 foursquare_type: :obj:`typing.Optional[base.String]` + + :param google_place_id: Google Places identifier of the venue + :type google_place_id: :obj:`typing.Optional[base.String]` + + :param google_place_type: Google Places type of the venue. See supported + types: https://developers.google.com/places/web-service/supported_types + :type google_place_type: :obj:`typing.Optional[base.String]` + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param reply_to_message_id: If the message is a reply, ID of the original + message + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, + None]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -824,10 +1255,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_contact(self, chat_id: typing.Union[base.Integer, base.String], phone_number: base.String, first_name: base.String, - last_name: typing.Union[base.String, None] = None, - vcard: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_to_message_id: typing.Union[base.Integer, None] = None, + last_name: typing.Optional[base.String] = None, + vcard: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, @@ -839,22 +1271,34 @@ 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 phone_number: Contact's phone number :type phone_number: :obj:`base.String` + :param first_name: Contact's first name :type first_name: :obj:`base.String` + :param last_name: Contact's last name - :type last_name: :obj:`typing.Union[base.String, None]` + :type last_name: :obj:`typing.Optional[base.String]` + :param vcard: vcard - :type vcard: :obj:`typing.Union[base.String, None]` + :type vcard: :obj:`typing.Optional[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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -864,7 +1308,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_CONTACT, payload) return types.Message(**result) - async def send_poll(self, chat_id: typing.Union[base.Integer, base.String], + 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, @@ -873,56 +1318,97 @@ 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, None] = None, + explanation_entities: typing.Optional[typing.List[types.MessageEntity]] = None, + open_period: typing.Optional[base.Integer] = None, close_date: typing.Union[ - base.Integer, datetime.datetime, datetime.timedelta, None] = None, + 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, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + ) -> types.Message: """ - Use this method to send a native poll. A native poll can't be sent to a private chat. - On success, the sent Message is returned. + Use this method to send a native poll. On success, the sent Message is + returned. Source: https://core.telegram.org/bots/api#sendpoll - :param chat_id: Unique identifier for the target chat - or username of the target channel (in the format @channelusername). - A native poll can't be sent to a private chat. + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` - :param question: Poll question, 1-255 characters + + :param question: Poll question, 1-300 characters :type question: :obj:`base.String` - :param options: List of answer options, 2-10 strings 1-100 characters each + + :param options: A list of answer options, 2-10 strings 1-100 characters each :type options: :obj:`typing.List[base.String]` + :param is_anonymous: True, if the poll needs to be anonymous, defaults to True :type is_anonymous: :obj:`typing.Optional[base.Boolean]` + :param type: Poll type, โ€œquizโ€ or โ€œregularโ€, defaults to โ€œregularโ€ :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: True, if the poll allows multiple answers, + ignored for polls in quiz mode, defaults to False :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: 0-based identifier of the correct answer option, + required for polls in quiz mode :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 + + :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. + + :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, 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 explanation_entities: List of special entities that appear in message + text, which can be specified instead of parse_mode + :type explanation_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + + :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.Optional[base.Integer]` + + :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. + + :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 + + :param reply_to_message_id: If the message is a reply, ID of the original + message :type reply_to_message_id: :obj:`typing.Optional[Integer]` - :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 + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, + None]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -936,34 +1422,50 @@ 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, - emoji: typing.Union[base.String, None] = None, - reply_to_message_id: typing.Union[base.Integer, None] = None, + async def send_dice(self, + chat_id: typing.Union[base.Integer, base.String], + disable_notification: typing.Optional[base.Boolean] = None, + emoji: typing.Optional[base.String] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, - types.ForceReply, None] = None) -> types.Message: + types.ForceReply, None] = None, + ) -> types.Message: """ - Use this method to send a dice, which will have a random value from 1 to 6. + Use this method to send an animated emoji that will display a random value. 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 + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) :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 emoji: Emoji on which the dice throw animation is based. Currently, + must be one of โ€œ๐ŸŽฒโ€, โ€œ๐ŸŽฏโ€, โ€œ๐Ÿ€โ€, โ€œโšฝโ€, or โ€œ๐ŸŽฐโ€. Dice can have values 1-6 + for โ€œ๐ŸŽฒโ€ and โ€œ๐ŸŽฏโ€, values 1-5 for โ€œ๐Ÿ€โ€ and โ€œโšฝโ€, and values 1-64 for โ€œ๐ŸŽฐโ€. + Defaults to โ€œ๐ŸŽฒโ€ + :type emoji: :obj:`typing.Optional[base.String]` + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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` """ @@ -998,8 +1500,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_CHAT_ACTION, payload) return result - async def get_user_profile_photos(self, user_id: base.Integer, offset: typing.Union[base.Integer, None] = None, - limit: typing.Union[base.Integer, None] = None) -> types.UserProfilePhotos: + async def get_user_profile_photos(self, user_id: base.Integer, offset: typing.Optional[base.Integer] = None, + limit: typing.Optional[base.Integer] = None) -> types.UserProfilePhotos: """ Use this method to get a list of profile pictures for a user. Returns a UserProfilePhotos object. @@ -1008,9 +1510,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` :param offset: Sequential number of the first photo to be returned. By default, all photos are returned - :type offset: :obj:`typing.Union[base.Integer, None]` + :type offset: :obj:`typing.Optional[base.Integer]` :param limit: Limits the number of photos to be retrieved. Values between 1โ€”100 are accepted. Defaults to 100 - :type limit: :obj:`typing.Union[base.Integer, None]` + :type limit: :obj:`typing.Optional[base.Integer]` :return: Returns a UserProfilePhotos object :rtype: :obj:`types.UserProfilePhotos` """ @@ -1060,7 +1562,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` :param until_date: Date when the user will be unbanned, unix time - :type until_date: :obj:`typing.Union[base.Integer, None]` + :type until_date: :obj:`typing.Optional[base.Integer]` :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1070,20 +1572,32 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.KICK_CHAT_MEMBER, payload) return result - async def unban_chat_member(self, chat_id: typing.Union[base.Integer, base.String], - user_id: base.Integer) -> base.Boolean: + async def unban_chat_member(self, + chat_id: typing.Union[base.Integer, base.String], + user_id: base.Integer, + only_if_banned: typing.Optional[base.Boolean] = None, + ) -> 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. - - The bot must be an administrator for this to work. + 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. The bot must be an administrator for this to + work. By default, this method guarantees that after the call the user is not + a member of the chat, but will be able to join it. So if the user is a member + of the chat they will also be removed from the chat. If you don't want this, + use the parameter only_if_banned. Returns True on success. Source: https://core.telegram.org/bots/api#unbanchatmember - :param chat_id: Unique identifier for the target group or username of the target supergroup or channel + :param chat_id: Unique identifier for the target group or username of the + target supergroup or channel (in the format @username) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` + + :param only_if_banned: Do nothing if the user is not banned + :type only_if_banned: :obj:`typing.Optional[base.Boolean]` + :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1098,10 +1612,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): # permissions argument need to be required after removing other `can_*` arguments 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, - can_add_web_page_previews: typing.Union[base.Boolean, None] = None) -> base.Boolean: + can_send_messages: typing.Optional[base.Boolean] = None, + can_send_media_messages: typing.Optional[base.Boolean] = None, + can_send_other_messages: typing.Optional[base.Boolean] = None, + can_add_web_page_previews: typing.Optional[base.Boolean] = None) -> base.Boolean: """ Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights. @@ -1116,18 +1630,18 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param permissions: New user permissions :type permissions: :obj:`ChatPermissions` :param until_date: Date when restrictions will be lifted for the user, unix time - :type until_date: :obj:`typing.Union[base.Integer, None]` + :type until_date: :obj:`typing.Optional[base.Integer]` :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues - :type can_send_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_send_messages: :obj:`typing.Optional[base.Boolean]` :param can_send_media_messages: Pass True, if the user can send audios, documents, photos, videos, video notes and voice notes, implies can_send_messages - :type can_send_media_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_send_media_messages: :obj:`typing.Optional[base.Boolean]` :param can_send_other_messages: Pass True, if the user can send animations, games, stickers and use inline bots, implies can_send_media_messages - :type can_send_other_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_send_other_messages: :obj:`typing.Optional[base.Boolean]` :param can_add_web_page_previews: Pass True, if the user may add web page previews to their messages, implies can_send_media_messages - :type can_add_web_page_previews: :obj:`typing.Union[base.Boolean, None]` + :type can_add_web_page_previews: :obj:`typing.Optional[base.Boolean]` :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1148,16 +1662,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload) return result - async def promote_chat_member(self, chat_id: typing.Union[base.Integer, base.String], + async def promote_chat_member(self, + chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer, - can_change_info: typing.Union[base.Boolean, None] = None, - can_post_messages: typing.Union[base.Boolean, None] = None, - can_edit_messages: typing.Union[base.Boolean, None] = None, - can_delete_messages: typing.Union[base.Boolean, None] = None, - can_invite_users: typing.Union[base.Boolean, None] = None, - can_restrict_members: typing.Union[base.Boolean, None] = None, - can_pin_messages: typing.Union[base.Boolean, None] = None, - can_promote_members: typing.Union[base.Boolean, None] = None) -> base.Boolean: + is_anonymous: typing.Optional[base.Boolean] = None, + can_change_info: typing.Optional[base.Boolean] = None, + can_post_messages: typing.Optional[base.Boolean] = None, + can_edit_messages: typing.Optional[base.Boolean] = None, + can_delete_messages: typing.Optional[base.Boolean] = None, + can_invite_users: typing.Optional[base.Boolean] = None, + can_restrict_members: typing.Optional[base.Boolean] = None, + can_pin_messages: typing.Optional[base.Boolean] = None, + can_promote_members: typing.Optional[base.Boolean] = None, + ) -> base.Boolean: """ Use this method to promote or demote a user in 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. @@ -1167,26 +1684,39 @@ 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 user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` + + :param is_anonymous: Pass True, if the administrator's presence in the chat is hidden + :type is_anonymous: :obj:`typing.Optional[base.Boolean]` + :param can_change_info: Pass True, if the administrator can change chat title, photo and other settings - :type can_change_info: :obj:`typing.Union[base.Boolean, None]` + :type can_change_info: :obj:`typing.Optional[base.Boolean]` + :param can_post_messages: Pass True, if the administrator can create channel posts, channels only - :type can_post_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_post_messages: :obj:`typing.Optional[base.Boolean]` + :param can_edit_messages: Pass True, if the administrator can edit messages of other users, channels only - :type can_edit_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_edit_messages: :obj:`typing.Optional[base.Boolean]` + :param can_delete_messages: Pass True, if the administrator can delete messages of other users - :type can_delete_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_delete_messages: :obj:`typing.Optional[base.Boolean]` + :param can_invite_users: Pass True, if the administrator can invite new users to the chat - :type can_invite_users: :obj:`typing.Union[base.Boolean, None]` + :type can_invite_users: :obj:`typing.Optional[base.Boolean]` + :param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members - :type can_restrict_members: :obj:`typing.Union[base.Boolean, None]` + :type can_restrict_members: :obj:`typing.Optional[base.Boolean]` + :param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only - :type can_pin_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_pin_messages: :obj:`typing.Optional[base.Boolean]` + :param can_promote_members: Pass True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) - :type can_promote_members: :obj:`typing.Union[base.Boolean, None]` + :type can_promote_members: :obj:`typing.Optional[base.Boolean]` + :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1320,7 +1850,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return result async def set_chat_description(self, chat_id: typing.Union[base.Integer, base.String], - description: typing.Union[base.String, None] = None) -> base.Boolean: + description: typing.Optional[base.String] = None) -> 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. @@ -1330,7 +1860,7 @@ 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 description: New chat description, 0-255 characters - :type description: :obj:`typing.Union[base.String, None]` + :type description: :obj:`typing.Optional[base.String]` :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1339,21 +1869,31 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SET_CHAT_DESCRIPTION, payload) return result - async def pin_chat_message(self, chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer, - disable_notification: typing.Union[base.Boolean, None] = None) -> base.Boolean: + async def pin_chat_message(self, + chat_id: typing.Union[base.Integer, base.String], + message_id: base.Integer, + disable_notification: typing.Optional[base.Boolean] = 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. + Use this method to add a message to the list of pinned messages in a chat. + If the chat is not a private chat, the bot must be an administrator in the + chat for this to work and must have the 'can_pin_messages' admin right in a + supergroup or 'can_edit_messages' admin right in a channel. Returns True on + success. Source: https://core.telegram.org/bots/api#pinchatmessage - :param chat_id: Unique identifier for the target chat or username of the target supergroup + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param message_id: Identifier of a message to pin :type message_id: :obj:`base.Integer` - :param disable_notification: Pass True, if it is not necessary to send a notification to - all group members about the new pinned message - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + + :param disable_notification: Pass True, if it is not necessary to send a + notification to all group members about the new pinned message + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1362,15 +1902,27 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.PIN_CHAT_MESSAGE, payload) return result - async def unpin_chat_message(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: + async def unpin_chat_message(self, + chat_id: typing.Union[base.Integer, base.String], + message_id: typing.Optional[base.Integer] = None, + ) -> 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. + Use this method to remove a message from the list of pinned messages in a + chat. If the chat is not a private chat, the bot must be an administrator in + the chat for this to work and must have the 'can_pin_messages' admin right in + a supergroup or 'can_edit_messages' admin right in a channel. Returns True on + success. Source: https://core.telegram.org/bots/api#unpinchatmessage - :param chat_id: Unique identifier for the target chat or username of the target supergroup + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + + :param message_id: Identifier of a message to unpin. If not specified, the + most recent pinned message (by sending date) will be unpinned. + :type message_id: :obj:`typing.Optional[base.Integer]` + :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1379,6 +1931,29 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.UNPIN_CHAT_MESSAGE, payload) return result + async def unpin_all_chat_messages(self, + chat_id: typing.Union[base.Integer, base.String], + ) -> base.Boolean: + """ + Use this method to clear the list of pinned messages in a chat. If the chat + is not a private chat, the bot must be an administrator in the chat for this + to work and must have the 'can_pin_messages' admin right in a supergroup or + 'can_edit_messages' admin right in a channel. Returns True on success. + + Source: https://core.telegram.org/bots/api#unpinallchatmessages + + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.UNPIN_ALL_CHAT_MESSAGES, payload) + return result + async def leave_chat(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: """ Use this method for your bot to leave a group, supergroup or channel. @@ -1511,10 +2086,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return result async def answer_callback_query(self, callback_query_id: base.String, - text: typing.Union[base.String, None] = None, - show_alert: typing.Union[base.Boolean, None] = None, - url: typing.Union[base.String, None] = None, - cache_time: typing.Union[base.Integer, None] = None) -> base.Boolean: + text: typing.Optional[base.String] = None, + show_alert: typing.Optional[base.Boolean] = None, + url: typing.Optional[base.String] = None, + cache_time: typing.Optional[base.Integer] = None) -> base.Boolean: """ Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. @@ -1528,15 +2103,15 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param callback_query_id: Unique identifier for the query to be answered :type callback_query_id: :obj:`base.String` :param text: Text of the notification. If not specified, nothing will be shown to the user, 0-1024 characters - :type text: :obj:`typing.Union[base.String, None]` + :type text: :obj:`typing.Optional[base.String]` :param show_alert: If true, an alert will be shown by the client instead of a notification at the top of the chat screen. Defaults to false. - :type show_alert: :obj:`typing.Union[base.Boolean, None]` + :type show_alert: :obj:`typing.Optional[base.Boolean]` :param url: URL that will be opened by the user's client - :type url: :obj:`typing.Union[base.String, None]` + :type url: :obj:`typing.Optional[base.String]` :param cache_time: The maximum amount of time in seconds that the result of the callback query may be cached client-side. - :type cache_time: :obj:`typing.Union[base.Integer, None]` + :type cache_time: :obj:`typing.Optional[base.Integer]` :return: On success, True is returned :rtype: :obj:`base.Boolean` """ @@ -1576,14 +2151,17 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_MY_COMMANDS, payload) return [types.BotCommand(**bot_command_data) for bot_command_data in result] - async def edit_message_text(self, text: base.String, + 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, - inline_message_id: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_web_page_preview: typing.Union[base.Boolean, None] = None, + message_id: typing.Optional[base.Integer] = None, + inline_message_id: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + entities: typing.Optional[typing.List[types.MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, - None] = None) -> types.Message or base.Boolean: + None] = None, + ) -> types.Message or base.Boolean: """ Use this method to edit text and game messages sent by the bot or via the bot (for inline bots). @@ -1592,19 +2170,30 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Required if inline_message_id is not specified Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String, None]` + :param message_id: Required if inline_message_id is not specified. Identifier of the sent message - :type message_id: :obj:`typing.Union[base.Integer, None]` + :type message_id: :obj:`typing.Optional[base.Integer]` + :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :type inline_message_id: :obj:`typing.Union[base.String, None]` + :type inline_message_id: :obj:`typing.Optional[base.String]` + :param text: New text of the message :type text: :obj:`base.String` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + :param disable_web_page_preview: Disables link previews for links in this message - :type disable_web_page_preview: :obj:`typing.Union[base.Boolean, None]` + :type disable_web_page_preview: :obj:`typing.Optional[base.Boolean]` + :param reply_markup: A JSON-serialized object for an inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + :return: On success, if edited message is sent by the bot, the edited Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -1620,10 +2209,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return types.Message(**result) async def edit_message_caption(self, chat_id: typing.Union[base.Integer, base.String, None] = None, - message_id: typing.Union[base.Integer, None] = None, - inline_message_id: typing.Union[base.String, None] = None, - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, + message_id: typing.Optional[base.Integer] = None, + inline_message_id: typing.Optional[base.String] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[types.MessageEntity]] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, None] = None) -> types.Message or base.Boolean: """ @@ -1634,17 +2224,27 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Required if inline_message_id is not specified Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String, None]` + :param message_id: Required if inline_message_id is not specified. Identifier of the sent message - :type message_id: :obj:`typing.Union[base.Integer, None]` + :type message_id: :obj:`typing.Optional[base.Integer]` + :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :type inline_message_id: :obj:`typing.Union[base.String, None]` + :type inline_message_id: :obj:`typing.Optional[base.String]` + :param caption: New caption of the message - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[types.MessageEntity]]` + :param reply_markup: A JSON-serialized object for an inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + :return: On success, if edited message is sent by the bot, the edited Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -1662,9 +2262,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def edit_message_media(self, media: types.InputMedia, chat_id: typing.Union[typing.Union[base.Integer, base.String], None] = None, - message_id: typing.Union[base.Integer, None] = None, - inline_message_id: typing.Union[base.String, None] = None, - reply_markup: typing.Union[types.InlineKeyboardMarkup, None] = None, + message_id: typing.Optional[base.Integer] = None, + inline_message_id: typing.Optional[base.String] = None, + reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None, ) -> typing.Union[types.Message, base.Boolean]: """ Use this method to edit audio, document, photo, or video messages. @@ -1681,13 +2281,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Required if inline_message_id is not specified :type chat_id: :obj:`typing.Union[typing.Union[base.Integer, base.String], None]` :param message_id: Required if inline_message_id is not specified. Identifier of the sent message - :type message_id: :obj:`typing.Union[base.Integer, None]` + :type message_id: :obj:`typing.Optional[base.Integer]` :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :type inline_message_id: :obj:`typing.Union[base.String, None]` + :type inline_message_id: :obj:`typing.Optional[base.String]` :param media: A JSON-serialized object for a new media content of the message :type media: :obj:`types.InputMedia` :param reply_markup: A JSON-serialized object for a new inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` :return: On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -1707,8 +2307,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def edit_message_reply_markup(self, chat_id: typing.Union[base.Integer, base.String, None] = None, - message_id: typing.Union[base.Integer, None] = None, - inline_message_id: typing.Union[base.String, None] = None, + message_id: typing.Optional[base.Integer] = None, + inline_message_id: typing.Optional[base.String] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, None] = None) -> types.Message or base.Boolean: """ @@ -1720,11 +2320,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): Unique identifier for the target chat or username of the target channel :type chat_id: :obj:`typing.Union[base.Integer, base.String, None]` :param message_id: Required if inline_message_id is not specified. Identifier of the sent message - :type message_id: :obj:`typing.Union[base.Integer, None]` + :type message_id: :obj:`typing.Optional[base.Integer]` :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :type inline_message_id: :obj:`typing.Union[base.String, None]` + :type inline_message_id: :obj:`typing.Optional[base.String]` :param reply_markup: A JSON-serialized object for an inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` :return: On success, if edited message is sent by the bot, the edited Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -1739,7 +2339,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def stop_poll(self, chat_id: typing.Union[base.String, base.Integer], message_id: base.Integer, - reply_markup: typing.Union[types.InlineKeyboardMarkup, None] = None) -> types.Poll: + reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None) -> types.Poll: """ Use this method to stop a poll which was sent by the bot. On success, the stopped Poll with the final results is returned. @@ -1749,7 +2349,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param message_id: Identifier of the original message with the poll :type message_id: :obj:`base.Integer` :param reply_markup: A JSON-serialized object for a new message inline keyboard. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` :return: On success, the stopped Poll with the final results is returned. :rtype: :obj:`types.Poll` """ @@ -1788,8 +2388,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_sticker(self, chat_id: typing.Union[base.Integer, base.String], sticker: typing.Union[base.InputFile, base.String], - disable_notification: typing.Union[base.Boolean, None] = None, - reply_to_message_id: typing.Union[base.Integer, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, @@ -1801,16 +2402,25 @@ 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 sticker: Sticker to send :type sticker: :obj:`typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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` """ @@ -1869,8 +2479,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): 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: + contains_masks: typing.Optional[base.Boolean] = None, + mask_position: typing.Optional[types.MaskPosition] = None) -> base.Boolean: """ 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. @@ -1899,9 +2509,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 - :type contains_masks: :obj:`typing.Union[base.Boolean, None]` + :type contains_masks: :obj:`typing.Optional[base.Boolean]` :param mask_position: A JSON-serialized object for position where the mask should be placed on faces - :type mask_position: :obj:`typing.Union[types.MaskPosition, None]` + :type mask_position: :obj:`typing.Optional[types.MaskPosition]` :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1921,7 +2531,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): 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: + mask_position: typing.Optional[types.MaskPosition] = 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. @@ -1947,7 +2557,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 - :type mask_position: :obj:`typing.Union[types.MaskPosition, None]` + :type mask_position: :obj:`typing.Optional[types.MaskPosition]` :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -2030,11 +2640,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def answer_inline_query(self, inline_query_id: base.String, results: typing.List[types.InlineQueryResult], - cache_time: typing.Union[base.Integer, None] = None, - is_personal: typing.Union[base.Boolean, None] = None, - next_offset: typing.Union[base.String, None] = None, - switch_pm_text: typing.Union[base.String, None] = None, - switch_pm_parameter: typing.Union[base.String, None] = None) -> base.Boolean: + cache_time: typing.Optional[base.Integer] = None, + is_personal: typing.Optional[base.Boolean] = None, + next_offset: typing.Optional[base.String] = None, + switch_pm_text: typing.Optional[base.String] = None, + switch_pm_parameter: typing.Optional[base.String] = None) -> base.Boolean: """ Use this method to send answers to an inline query. No more than 50 results per query are allowed. @@ -2047,22 +2657,22 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type results: :obj:`typing.List[types.InlineQueryResult]` :param cache_time: The maximum amount of time in seconds that the result of the inline query may be cached on the server. Defaults to 300. - :type cache_time: :obj:`typing.Union[base.Integer, None]` + :type cache_time: :obj:`typing.Optional[base.Integer]` :param is_personal: Pass True, if results may be cached on the server side only for the user that sent the query. By default, results may be returned to any user who sends the same query - :type is_personal: :obj:`typing.Union[base.Boolean, None]` + :type is_personal: :obj:`typing.Optional[base.Boolean]` :param next_offset: Pass the offset that a client should send in the next query with the same text to receive more results. Pass an empty string if there are no more results or if you donโ€˜t support pagination. Offset length canโ€™t exceed 64 bytes. - :type next_offset: :obj:`typing.Union[base.String, None]` + :type next_offset: :obj:`typing.Optional[base.String]` :param switch_pm_text: If passed, clients will display a button with specified text that switches the user to a private chat with the bot and sends the bot a start message with the parameter switch_pm_parameter - :type switch_pm_text: :obj:`typing.Union[base.String, None]` + :type switch_pm_text: :obj:`typing.Optional[base.String]` :param switch_pm_parameter: Deep-linking parameter for the /start message sent to the bot when user presses the switch button. 1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed. - :type switch_pm_parameter: :obj:`typing.Union[base.String, None]` + :type switch_pm_parameter: :obj:`typing.Optional[base.String]` :return: On success, True is returned :rtype: :obj:`base.Boolean` """ @@ -2079,21 +2689,22 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): description: base.String, payload: base.String, provider_token: base.String, start_parameter: base.String, currency: base.String, prices: typing.List[types.LabeledPrice], - provider_data: typing.Union[typing.Dict, None] = None, - photo_url: typing.Union[base.String, None] = None, - photo_size: typing.Union[base.Integer, None] = None, - photo_width: typing.Union[base.Integer, None] = None, - photo_height: typing.Union[base.Integer, None] = None, - need_name: typing.Union[base.Boolean, None] = None, - 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, - reply_markup: typing.Union[types.InlineKeyboardMarkup, None] = None) -> types.Message: + provider_data: typing.Optional[typing.Dict] = None, + photo_url: typing.Optional[base.String] = None, + photo_size: typing.Optional[base.Integer] = None, + photo_width: typing.Optional[base.Integer] = None, + photo_height: typing.Optional[base.Integer] = None, + need_name: typing.Optional[base.Boolean] = None, + need_phone_number: typing.Optional[base.Boolean] = None, + need_email: typing.Optional[base.Boolean] = None, + need_shipping_address: typing.Optional[base.Boolean] = None, + send_phone_number_to_provider: typing.Optional[base.Boolean] = None, + send_email_to_provider: typing.Optional[base.Boolean] = None, + is_flexible: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None) -> types.Message: """ Use this method to send invoices. @@ -2101,54 +2712,81 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Unique identifier for the target private chat :type chat_id: :obj:`base.Integer` + :param title: Product name, 1-32 characters :type title: :obj:`base.String` + :param description: Product description, 1-255 characters :type description: :obj:`base.String` + :param payload: Bot-defined invoice payload, 1-128 bytes This will not be displayed to the user, use for your internal processes. :type payload: :obj:`base.String` + :param provider_token: Payments provider token, obtained via Botfather :type provider_token: :obj:`base.String` + :param start_parameter: Unique deep-linking parameter that can be used to generate this invoice when used as a start parameter :type start_parameter: :obj:`base.String` + :param currency: Three-letter ISO 4217 currency code, see more on currencies :type currency: :obj:`base.String` + :param prices: Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) :type prices: :obj:`typing.List[types.LabeledPrice]` + :param provider_data: JSON-encoded data about the invoice, which will be shared with the payment provider - :type provider_data: :obj:`typing.Union[typing.Dict, None]` + :type provider_data: :obj:`typing.Optional[typing.Dict]` + :param photo_url: URL of the product photo for the invoice - :type photo_url: :obj:`typing.Union[base.String, None]` + :type photo_url: :obj:`typing.Optional[base.String]` + :param photo_size: Photo size - :type photo_size: :obj:`typing.Union[base.Integer, None]` + :type photo_size: :obj:`typing.Optional[base.Integer]` + :param photo_width: Photo width - :type photo_width: :obj:`typing.Union[base.Integer, None]` + :type photo_width: :obj:`typing.Optional[base.Integer]` + :param photo_height: Photo height - :type photo_height: :obj:`typing.Union[base.Integer, None]` + :type photo_height: :obj:`typing.Optional[base.Integer]` + :param need_name: Pass True, if you require the user's full name to complete the order - :type need_name: :obj:`typing.Union[base.Boolean, None]` + :type need_name: :obj:`typing.Optional[base.Boolean]` + :param need_phone_number: Pass True, if you require the user's phone number to complete the order - :type need_phone_number: :obj:`typing.Union[base.Boolean, None]` + :type need_phone_number: :obj:`typing.Optional[base.Boolean]` + :param need_email: Pass True, if you require the user's email to complete the order - :type need_email: :obj:`typing.Union[base.Boolean, None]` + :type need_email: :obj:`typing.Optional[base.Boolean]` + :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]` + :type need_shipping_address: :obj:`typing.Optional[base.Boolean]` + :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]` + :type send_phone_number_to_provider: :obj:`typing.Optional[base.Boolean]` + :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]` + :type send_email_to_provider: :obj:`typing.Optional[base.Boolean]` + :param is_flexible: Pass True, if the final price depends on the shipping method - :type is_flexible: :obj:`typing.Union[base.Boolean, None]` + :type is_flexible: :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.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :param reply_markup: A JSON-serialized object for an inline keyboard If empty, one 'Pay total price' button will be shown. If not empty, the first button must be a Pay button. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -2161,7 +2799,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def answer_shipping_query(self, shipping_query_id: base.String, ok: base.Boolean, shipping_options: typing.Union[typing.List[types.ShippingOption], None] = None, - error_message: typing.Union[base.String, None] = None) -> base.Boolean: + error_message: typing.Optional[base.String] = None) -> base.Boolean: """ If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the Bot API will send an Update with a shipping_query field to the bot. @@ -2179,7 +2817,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): Error message in human readable form that explains why it is impossible to complete the order (e.g. "Sorry, delivery to your desired address is unavailable'). Telegram will display this message to the user. - :type error_message: :obj:`typing.Union[base.String, None]` + :type error_message: :obj:`typing.Optional[base.String]` :return: On success, True is returned :rtype: :obj:`base.Boolean` """ @@ -2194,7 +2832,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return result async def answer_pre_checkout_query(self, pre_checkout_query_id: base.String, ok: base.Boolean, - error_message: typing.Union[base.String, None] = None) -> base.Boolean: + error_message: typing.Optional[base.String] = None) -> base.Boolean: """ Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query. @@ -2212,7 +2850,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): (e.g. "Sorry, somebody just bought the last of our amazing black T-shirts while you were busy filling out your payment details. Please choose a different color or garment!"). Telegram will display this message to the user. - :type error_message: :obj:`typing.Union[base.String, None]` + :type error_message: :obj:`typing.Optional[base.String]` :return: On success, True is returned :rtype: :obj:`base.Boolean` """ @@ -2256,10 +2894,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): # === Games === # https://core.telegram.org/bots/api#games - async def send_game(self, chat_id: base.Integer, game_short_name: 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, None] = None) -> types.Message: + async def send_game(self, + chat_id: base.Integer, + game_short_name: base.String, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None, + ) -> types.Message: """ Use this method to send a game. @@ -2267,16 +2909,25 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param chat_id: Unique identifier for the target chat :type chat_id: :obj:`base.Integer` - :param game_short_name: Short name of the game, serves as the unique identifier for the game. \ + + :param game_short_name: Short name of the game, serves as the unique identifier for the game. Set up your games via Botfather. :type game_short_name: :obj:`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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :param reply_markup: A JSON-serialized object for an inline keyboard If empty, one โ€˜Play game_titleโ€™ button will be shown. If not empty, the first button must launch the game. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -2287,10 +2938,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return types.Message(**result) async def set_game_score(self, user_id: base.Integer, score: base.Integer, - force: typing.Union[base.Boolean, None] = None, - disable_edit_message: typing.Union[base.Boolean, None] = None, - chat_id: typing.Union[base.Integer, None] = None, - message_id: typing.Union[base.Integer, None] = None, + force: typing.Optional[base.Boolean] = None, + disable_edit_message: typing.Optional[base.Boolean] = None, + chat_id: typing.Optional[base.Integer] = None, + message_id: typing.Optional[base.Integer] = None, inline_message_id: typing.Union[base.String, None] = None) -> types.Message or base.Boolean: """ @@ -2304,16 +2955,16 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type score: :obj:`base.Integer` :param force: Pass True, if the high score is allowed to decrease This can be useful when fixing mistakes or banning cheaters - :type force: :obj:`typing.Union[base.Boolean, None]` + :type force: :obj:`typing.Optional[base.Boolean]` :param disable_edit_message: Pass True, if the game message should not be automatically edited to include the current scoreboard - :type disable_edit_message: :obj:`typing.Union[base.Boolean, None]` + :type disable_edit_message: :obj:`typing.Optional[base.Boolean]` :param chat_id: Required if inline_message_id is not specified. Unique identifier for the target chat - :type chat_id: :obj:`typing.Union[base.Integer, None]` + :type chat_id: :obj:`typing.Optional[base.Integer]` :param message_id: Required if inline_message_id is not specified. Identifier of the sent message - :type message_id: :obj:`typing.Union[base.Integer, None]` + :type message_id: :obj:`typing.Optional[base.Integer]` :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :type inline_message_id: :obj:`typing.Union[base.String, None]` + :type inline_message_id: :obj:`typing.Optional[base.String]` :return: On success, if the message was sent by the bot, returns the edited Message, otherwise returns True Returns an error, if the new score is not greater than the user's current score in the chat and force is False. @@ -2327,8 +2978,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return types.Message(**result) async def get_game_high_scores(self, user_id: base.Integer, - chat_id: typing.Union[base.Integer, None] = None, - message_id: typing.Union[base.Integer, None] = None, + chat_id: typing.Optional[base.Integer] = None, + message_id: typing.Optional[base.Integer] = None, inline_message_id: typing.Union[base.String, None] = None) -> typing.List[types.GameHighScore]: """ @@ -2343,11 +2994,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param user_id: Target user id :type user_id: :obj:`base.Integer` :param chat_id: Required if inline_message_id is not specified. Unique identifier for the target chat - :type chat_id: :obj:`typing.Union[base.Integer, None]` + :type chat_id: :obj:`typing.Optional[base.Integer]` :param message_id: Required if inline_message_id is not specified. Identifier of the sent message - :type message_id: :obj:`typing.Union[base.Integer, None]` + :type message_id: :obj:`typing.Optional[base.Integer]` :param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message - :type inline_message_id: :obj:`typing.Union[base.String, None]` + :type inline_message_id: :obj:`typing.Optional[base.String]` :return: Will return the score of the specified user and several of his neighbors in a game On success, returns an Array of GameHighScore objects. This method will currently return scores for the target user, diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index b19327ca..5bb9062a 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -55,7 +55,7 @@ class RethinkDBStorage(BaseStorage): self._ssl = ssl or {} self._loop = loop - self._conn: typing.Union[Connection, None] = None + self._conn: typing.Optional[Connection] = None async def connect(self) -> Connection: """ diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index a2992322..9a4eac76 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -300,7 +300,7 @@ class FSMContext: async def update_data(self, data: typing.Dict = None, **kwargs): await self.storage.update_data(chat=self.chat, user=self.user, data=data, **kwargs) - async def set_state(self, state: typing.Union[typing.AnyStr, None] = None): + async def set_state(self, state: typing.Optional[typing.AnyStr] = None): await self.storage.set_state(chat=self.chat, user=self.user, state=self._resolve_state(state)) async def set_data(self, data: typing.Dict = None): diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 5199d591..b6280ed1 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -939,8 +939,8 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): def __init__(self, chat_id: Union[Integer, String], media: Union[types.MediaGroup, List] = None, - disable_notification: typing.Union[Boolean, None] = None, - reply_to_message_id: typing.Union[Integer, None] = None): + disable_notification: typing.Optional[Boolean] = None, + reply_to_message_id: typing.Optional[Integer] = None): """ Use this method to send a group of photos or videos as an album. @@ -951,9 +951,9 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param media: A JSON-serialized array describing photos and videos to be sent :type media: :obj:`typing.Union[types.MediaGroup, typing.List]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` :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]` + :type reply_to_message_id: :obj:`typing.Optional[base.Integer]` :return: On success, an array of the sent Messages is returned. :rtype: typing.List[types.Message] """ diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index d46f24da..26201130 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -7,6 +7,7 @@ from .bot_command import BotCommand from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType +from .chat_location import ChatLocation from .chat_member import ChatMember, ChatMemberStatus from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto @@ -40,6 +41,7 @@ from .login_url import LoginUrl from .mask_position import MaskPosition from .message import ContentType, ContentTypes, Message, ParseMode from .message_entity import MessageEntity, MessageEntityType +from .message_id import MessageId from .order_info import OrderInfo from .passport_data import PassportData from .passport_element_error import PassportElementError, PassportElementErrorDataField, PassportElementErrorFile, \ @@ -49,6 +51,7 @@ from .passport_file import PassportFile from .photo_size import PhotoSize from .poll import PollOption, Poll, PollAnswer, PollType from .pre_checkout_query import PreCheckoutQuery +from .proximity_alert_triggered import ProximityAlertTriggered from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, KeyboardButtonPollType from .response_parameters import ResponseParameters from .shipping_address import ShippingAddress @@ -76,6 +79,7 @@ __all__ = ( 'CallbackQuery', 'Chat', 'ChatActions', + 'ChatLocation', 'ChatMember', 'ChatMemberStatus', 'ChatPermissions', @@ -141,6 +145,7 @@ __all__ = ( 'Message', 'MessageEntity', 'MessageEntityType', + 'MessageId', 'OrderInfo', 'ParseMode', 'PassportData', @@ -158,6 +163,7 @@ __all__ = ( 'PollOption', 'PollType', 'PreCheckoutQuery', + 'ProximityAlertTriggered', 'ReplyKeyboardMarkup', 'ReplyKeyboardRemove', 'ResponseParameters', diff --git a/aiogram/types/audio.py b/aiogram/types/audio.py index 6859668f..1657c9cc 100644 --- a/aiogram/types/audio.py +++ b/aiogram/types/audio.py @@ -15,6 +15,7 @@ class Audio(base.TelegramObject, mixins.Downloadable): duration: base.Integer = fields.Field() performer: base.String = fields.Field() title: base.String = fields.Field() + file_name: base.String = fields.Field() mime_type: base.String = fields.Field() file_size: base.Integer = fields.Field() thumb: PhotoSize = fields.Field(base=PhotoSize) diff --git a/aiogram/types/callback_query.py b/aiogram/types/callback_query.py index e847bff8..5eeb2f0c 100644 --- a/aiogram/types/callback_query.py +++ b/aiogram/types/callback_query.py @@ -28,10 +28,10 @@ class CallbackQuery(base.TelegramObject): data: base.String = fields.Field() game_short_name: base.String = fields.Field() - async def answer(self, text: typing.Union[base.String, None] = None, - show_alert: typing.Union[base.Boolean, None] = None, - url: typing.Union[base.String, None] = None, - cache_time: typing.Union[base.Integer, None] = None): + async def answer(self, text: typing.Optional[base.String] = None, + show_alert: typing.Optional[base.Boolean] = None, + url: typing.Optional[base.String] = None, + cache_time: typing.Optional[base.Integer] = None): """ Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. @@ -43,15 +43,15 @@ class CallbackQuery(base.TelegramObject): Source: https://core.telegram.org/bots/api#answercallbackquery :param text: Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters - :type text: :obj:`typing.Union[base.String, None]` + :type text: :obj:`typing.Optional[base.String]` :param show_alert: If true, an alert will be shown by the client instead of a notification at the top of the chat screen. Defaults to false. - :type show_alert: :obj:`typing.Union[base.Boolean, None]` + :type show_alert: :obj:`typing.Optional[base.Boolean]` :param url: URL that will be opened by the user's client. - :type url: :obj:`typing.Union[base.String, None]` + :type url: :obj:`typing.Optional[base.String]` :param cache_time: The maximum amount of time in seconds that the result of the callback query may be cached client-side. - :type cache_time: :obj:`typing.Union[base.Integer, None]` + :type cache_time: :obj:`typing.Optional[base.Integer]` :return: On success, True is returned. :rtype: :obj:`base.Boolean`""" return await self.bot.answer_callback_query(callback_query_id=self.id, diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 7fcea5ea..0021eb23 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -4,12 +4,13 @@ import asyncio import datetime import typing -from ..utils import helper, markdown from . import base, fields +from .chat_location import ChatLocation from .chat_member import ChatMember from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .input_file import InputFile +from ..utils import helper, markdown from ..utils.deprecated import deprecated, DeprecatedReadOnlyClassVar @@ -27,6 +28,7 @@ class Chat(base.TelegramObject): last_name: base.String = fields.Field() all_members_are_administrators: base.Boolean = fields.Field() photo: ChatPhoto = fields.Field(base=ChatPhoto) + bio: base.String = fields.Field() description: base.String = fields.Field() invite_link: base.String = fields.Field() pinned_message: 'Message' = fields.Field(base='Message') @@ -34,6 +36,8 @@ class Chat(base.TelegramObject): slow_mode_delay: base.Integer = fields.Field() sticker_set_name: base.String = fields.Field() can_set_sticker_set: base.Boolean = fields.Field() + linked_chat_id: base.Integer = fields.Field() + location: ChatLocation = fields.Field() def __hash__(self): return self.id @@ -48,7 +52,7 @@ class Chat(base.TelegramObject): return self.title @property - def mention(self) -> typing.Union[base.String, None]: + def mention(self) -> typing.Optional[base.String]: """ Get mention if a Chat has a username, or get full name if this is a Private Chat, otherwise None is returned """ @@ -175,14 +179,15 @@ class Chat(base.TelegramObject): Source: https://core.telegram.org/bots/api#setchatdescription :param description: New chat description, 0-255 characters - :type description: :obj:`typing.Union[base.String, None]` + :type description: :obj:`typing.Optional[base.String]` :return: Returns True on success. :rtype: :obj:`base.Boolean` """ 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: + 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 @@ -199,35 +204,49 @@ class Chat(base.TelegramObject): :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` :param until_date: Date when the user will be unbanned, unix time. - :type until_date: :obj:`typing.Union[base.Integer, None]` + :type until_date: :obj:`typing.Optional[base.Integer]` :return: Returns True on success. :rtype: :obj:`base.Boolean` """ 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) -> base.Boolean: + async def unban(self, + user_id: base.Integer, + only_if_banned: typing.Optional[base.Boolean] = None, + ) -> 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. - - The bot must be an administrator for this to work. + 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. The bot must be an administrator for this to + work. By default, this method guarantees that after the call the user is not + a member of the chat, but will be able to join it. So if the user is a member + of the chat they will also be removed from the chat. If you don't want this, + use the parameter only_if_banned. Returns True on success. Source: https://core.telegram.org/bots/api#unbanchatmember :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` + + :param only_if_banned: Do nothing if the user is not banned + :type only_if_banned: :obj:`typing.Optional[base.Boolean]` + :return: Returns True on success. :rtype: :obj:`base.Boolean` """ - return await self.bot.unban_chat_member(self.id, user_id=user_id) + return await self.bot.unban_chat_member( + chat_id=self.id, + user_id=user_id, + only_if_banned=only_if_banned, + ) async def restrict(self, user_id: base.Integer, permissions: typing.Optional[ChatPermissions] = 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, - can_add_web_page_previews: typing.Union[base.Boolean, None] = None) -> base.Boolean: + can_send_messages: typing.Optional[base.Boolean] = None, + can_send_media_messages: typing.Optional[base.Boolean] = None, + can_send_other_messages: typing.Optional[base.Boolean] = None, + can_add_web_page_previews: typing.Optional[base.Boolean] = None) -> base.Boolean: """ Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights. @@ -240,18 +259,18 @@ class Chat(base.TelegramObject): :param permissions: New user permissions :type permissions: :obj:`ChatPermissions` :param until_date: Date when restrictions will be lifted for the user, unix time. - :type until_date: :obj:`typing.Union[base.Integer, None]` + :type until_date: :obj:`typing.Optional[base.Integer]` :param can_send_messages: Pass True, if the user can send text messages, contacts, locations and venues - :type can_send_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_send_messages: :obj:`typing.Optional[base.Boolean]` :param can_send_media_messages: Pass True, if the user can send audios, documents, photos, videos, video notes and voice notes, implies can_send_messages - :type can_send_media_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_send_media_messages: :obj:`typing.Optional[base.Boolean]` :param can_send_other_messages: Pass True, if the user can send animations, games, stickers and use inline bots, implies can_send_media_messages - :type can_send_other_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_send_other_messages: :obj:`typing.Optional[base.Boolean]` :param can_add_web_page_previews: Pass True, if the user may add web page previews to their messages, implies can_send_media_messages - :type can_add_web_page_previews: :obj:`typing.Union[base.Boolean, None]` + :type can_add_web_page_previews: :obj:`typing.Optional[base.Boolean]` :return: Returns True on success. :rtype: :obj:`base.Boolean` """ @@ -264,14 +283,14 @@ class Chat(base.TelegramObject): can_add_web_page_previews=can_add_web_page_previews) async def promote(self, user_id: base.Integer, - can_change_info: typing.Union[base.Boolean, None] = None, - can_post_messages: typing.Union[base.Boolean, None] = None, - can_edit_messages: typing.Union[base.Boolean, None] = None, - can_delete_messages: typing.Union[base.Boolean, None] = None, - can_invite_users: typing.Union[base.Boolean, None] = None, - can_restrict_members: typing.Union[base.Boolean, None] = None, - can_pin_messages: typing.Union[base.Boolean, None] = None, - can_promote_members: typing.Union[base.Boolean, None] = None) -> base.Boolean: + can_change_info: typing.Optional[base.Boolean] = None, + can_post_messages: typing.Optional[base.Boolean] = None, + can_edit_messages: typing.Optional[base.Boolean] = None, + can_delete_messages: typing.Optional[base.Boolean] = None, + can_invite_users: typing.Optional[base.Boolean] = None, + can_restrict_members: typing.Optional[base.Boolean] = None, + can_pin_messages: typing.Optional[base.Boolean] = None, + can_promote_members: typing.Optional[base.Boolean] = None) -> base.Boolean: """ Use this method to promote or demote a user in 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. @@ -282,23 +301,23 @@ class Chat(base.TelegramObject): :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` :param can_change_info: Pass True, if the administrator can change chat title, photo and other settings - :type can_change_info: :obj:`typing.Union[base.Boolean, None]` + :type can_change_info: :obj:`typing.Optional[base.Boolean]` :param can_post_messages: Pass True, if the administrator can create channel posts, channels only - :type can_post_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_post_messages: :obj:`typing.Optional[base.Boolean]` :param can_edit_messages: Pass True, if the administrator can edit messages of other users, channels only - :type can_edit_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_edit_messages: :obj:`typing.Optional[base.Boolean]` :param can_delete_messages: Pass True, if the administrator can delete messages of other users - :type can_delete_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_delete_messages: :obj:`typing.Optional[base.Boolean]` :param can_invite_users: Pass True, if the administrator can invite new users to the chat - :type can_invite_users: :obj:`typing.Union[base.Boolean, None]` + :type can_invite_users: :obj:`typing.Optional[base.Boolean]` :param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members - :type can_restrict_members: :obj:`typing.Union[base.Boolean, None]` + :type can_restrict_members: :obj:`typing.Optional[base.Boolean]` :param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only - :type can_pin_messages: :obj:`typing.Union[base.Boolean, None]` + :type can_pin_messages: :obj:`typing.Optional[base.Boolean]` :param can_promote_members: Pass True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) - :type can_promote_members: :obj:`typing.Union[base.Boolean, None]` + :type can_promote_members: :obj:`typing.Optional[base.Boolean]` :return: Returns True on success. :rtype: :obj:`base.Boolean` """ @@ -338,36 +357,73 @@ class Chat(base.TelegramObject): :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) + 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: + async def pin_message(self, + message_id: base.Integer, + disable_notification: typing.Optional[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. + Use this method to add a message to the list of pinned messages in a chat. + If the chat is not a private chat, the bot must be an administrator in the + chat for this to work and must have the 'can_pin_messages' admin right in a + supergroup or 'can_edit_messages' admin right in a channel. Returns True on + success. Source: https://core.telegram.org/bots/api#pinchatmessage :param message_id: Identifier of a message to pin :type message_id: :obj:`base.Integer` - :param disable_notification: Pass True, if it is not necessary to send a notification to - all group members about the new pinned message - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :return: Returns True on success. + + :param disable_notification: Pass True, if it is not necessary to send a + notification to all group members about the new pinned message + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :return: Returns True on success :rtype: :obj:`base.Boolean` """ return await self.bot.pin_chat_message(self.id, message_id, disable_notification) - async def unpin_message(self) -> base.Boolean: + async def unpin_message(self, + message_id: typing.Optional[base.Integer] = None, + ) -> 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. + Use this method to remove a message from the list of pinned messages in a + chat. If the chat is not a private chat, the bot must be an administrator in + the chat for this to work and must have the 'can_pin_messages' admin right in + a supergroup or 'can_edit_messages' admin right in a channel. Returns True on + success. Source: https://core.telegram.org/bots/api#unpinchatmessage - :return: Returns True on success. + :param message_id: Identifier of a message to unpin. If not specified, the + most recent pinned message (by sending date) will be unpinned. + :type message_id: :obj:`typing.Optional[base.Integer]` + + :return: Returns True on success :rtype: :obj:`base.Boolean` """ - return await self.bot.unpin_chat_message(self.id) + return await self.bot.unpin_chat_message( + chat_id=self.id, + message_id=message_id, + ) + + async def unpin_all_messages(self): + """ + Use this method to clear the list of pinned messages in a chat. If the chat + is not a private chat, the bot must be an administrator in the chat for this + to work and must have the 'can_pin_messages' admin right in a supergroup or + 'can_edit_messages' admin right in a channel. Returns True on success. + + Source: https://core.telegram.org/bots/api#unpinallchatmessages + + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + return await self.bot.unpin_all_chat_messages( + chat_id=self.id, + ) async def leave(self) -> base.Boolean: """ diff --git a/aiogram/types/chat_location.py b/aiogram/types/chat_location.py new file mode 100644 index 00000000..0438c544 --- /dev/null +++ b/aiogram/types/chat_location.py @@ -0,0 +1,16 @@ +from . import base +from . import fields +from .location import Location + + +class ChatLocation(base.TelegramObject): + """ + Represents a location to which a chat is connected. + + https://core.telegram.org/bots/api#chatlocation + """ + location: Location = fields.Field() + address: base.String = fields.Field() + + def __init__(self, location: Location, address: base.String): + super().__init__(location=location, address=address) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 274c8a26..4aa52b80 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,6 +1,4 @@ import datetime -import warnings -from typing import Optional from . import base from . import fields @@ -17,6 +15,7 @@ class ChatMember(base.TelegramObject): user: User = fields.Field(base=User) status: base.String = fields.Field() custom_title: base.String = fields.Field() + is_anonymous: base.Boolean = 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/dice.py b/aiogram/types/dice.py index 7b3f1727..70c50e09 100644 --- a/aiogram/types/dice.py +++ b/aiogram/types/dice.py @@ -17,3 +17,5 @@ class DiceEmoji: DICE = '๐ŸŽฒ' DART = '๐ŸŽฏ' BASKETBALL = '๐Ÿ€' + FOOTBALL = 'โšฝ' + SLOT_MACHINE = '๐ŸŽฐ' diff --git a/aiogram/types/inline_query.py b/aiogram/types/inline_query.py index 379394a0..436c11b0 100644 --- a/aiogram/types/inline_query.py +++ b/aiogram/types/inline_query.py @@ -23,11 +23,11 @@ class InlineQuery(base.TelegramObject): async def answer(self, results: typing.List[InlineQueryResult], - cache_time: typing.Union[base.Integer, None] = None, - is_personal: typing.Union[base.Boolean, None] = None, - next_offset: typing.Union[base.String, None] = None, - switch_pm_text: typing.Union[base.String, None] = None, - switch_pm_parameter: typing.Union[base.String, None] = None): + cache_time: typing.Optional[base.Integer] = None, + is_personal: typing.Optional[base.Boolean] = None, + next_offset: typing.Optional[base.String] = None, + switch_pm_text: typing.Optional[base.String] = None, + switch_pm_parameter: typing.Optional[base.String] = None): """ Use this method to send answers to an inline query. No more than 50 results per query are allowed. @@ -38,22 +38,22 @@ class InlineQuery(base.TelegramObject): :type results: :obj:`typing.List[types.InlineQueryResult]` :param cache_time: The maximum amount of time in seconds that the result of the inline query may be cached on the server. Defaults to 300. - :type cache_time: :obj:`typing.Union[base.Integer, None]` + :type cache_time: :obj:`typing.Optional[base.Integer]` :param is_personal: Pass True, if results may be cached on the server side only for the user that sent the query. By default, results may be returned to any user who sends the same query - :type is_personal: :obj:`typing.Union[base.Boolean, None]` + :type is_personal: :obj:`typing.Optional[base.Boolean]` :param next_offset: Pass the offset that a client should send in the next query with the same text to receive more results. Pass an empty string if there are no more results or if you donโ€˜t support pagination. Offset length canโ€™t exceed 64 bytes. - :type next_offset: :obj:`typing.Union[base.String, None]` + :type next_offset: :obj:`typing.Optional[base.String]` :param switch_pm_text: If passed, clients will display a button with specified text that switches the user to a private chat with the bot and sends the bot a start message with the parameter switch_pm_parameter - :type switch_pm_text: :obj:`typing.Union[base.String, None]` + :type switch_pm_text: :obj:`typing.Optional[base.String]` :param switch_pm_parameter: Deep-linking parameter for the /start message sent to the bot when user presses the switch button. 1-64 characters, only A-Z, a-z, 0-9, _ and - are allowed. - :type switch_pm_parameter: :obj:`typing.Union[base.String, None]` + :type switch_pm_parameter: :obj:`typing.Optional[base.String]` :return: On success, True is returned :rtype: :obj:`base.Boolean` """ diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py index fccaa2a1..da09db9d 100644 --- a/aiogram/types/inline_query_result.py +++ b/aiogram/types/inline_query_result.py @@ -4,6 +4,7 @@ from . import base from . import fields from .inline_keyboard import InlineKeyboardMarkup from .input_message_content import InputMessageContent +from .message_entity import MessageEntity class InlineQueryResult(base.TelegramObject): @@ -83,23 +84,29 @@ class InlineQueryResultPhoto(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - photo_url: base.String, - thumb_url: base.String, - photo_width: typing.Optional[base.Integer] = None, - photo_height: typing.Optional[base.Integer] = None, - 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, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + photo_url: base.String, + thumb_url: base.String, + photo_width: typing.Optional[base.Integer] = None, + photo_height: typing.Optional[base.Integer] = None, + 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, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__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, + parse_mode=parse_mode, caption_entities=caption_entities, + reply_markup=reply_markup, input_message_content=input_message_content, + ) class InlineQueryResultGif(InlineQueryResult): @@ -123,23 +130,29 @@ class InlineQueryResultGif(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - gif_url: base.String, - gif_width: typing.Optional[base.Integer] = None, - gif_height: typing.Optional[base.Integer] = None, - gif_duration: typing.Optional[base.Integer] = None, - thumb_url: typing.Optional[base.String] = None, - title: 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(InlineQueryResultGif, self).__init__(id=id, gif_url=gif_url, gif_width=gif_width, - gif_height=gif_height, gif_duration=gif_duration, - thumb_url=thumb_url, title=title, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + gif_url: base.String, + gif_width: typing.Optional[base.Integer] = None, + gif_height: typing.Optional[base.Integer] = None, + gif_duration: typing.Optional[base.Integer] = None, + thumb_url: typing.Optional[base.String] = None, + title: typing.Optional[base.String] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, gif_url=gif_url, gif_width=gif_width, gif_height=gif_height, + gif_duration=gif_duration, thumb_url=thumb_url, title=title, + caption=caption, parse_mode=parse_mode, reply_markup=reply_markup, + caption_entities=caption_entities, + input_message_content=input_message_content, + ) class InlineQueryResultMpeg4Gif(InlineQueryResult): @@ -163,23 +176,30 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - mpeg4_url: base.String, - thumb_url: base.String, - mpeg4_width: typing.Optional[base.Integer] = None, - mpeg4_height: typing.Optional[base.Integer] = None, - mpeg4_duration: typing.Optional[base.Integer] = None, - title: 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(InlineQueryResultMpeg4Gif, self).__init__(id=id, mpeg4_url=mpeg4_url, mpeg4_width=mpeg4_width, - mpeg4_height=mpeg4_height, mpeg4_duration=mpeg4_duration, - thumb_url=thumb_url, title=title, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + mpeg4_url: base.String, + thumb_url: base.String, + mpeg4_width: typing.Optional[base.Integer] = None, + mpeg4_height: typing.Optional[base.Integer] = None, + mpeg4_duration: typing.Optional[base.Integer] = None, + title: typing.Optional[base.String] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, mpeg4_url=mpeg4_url, mpeg4_width=mpeg4_width, + mpeg4_height=mpeg4_height, mpeg4_duration=mpeg4_duration, + thumb_url=thumb_url, title=title, caption=caption, + parse_mode=parse_mode, reply_markup=reply_markup, + caption_entities=caption_entities, + input_message_content=input_message_content, + ) class InlineQueryResultVideo(InlineQueryResult): @@ -207,26 +227,32 @@ class InlineQueryResultVideo(InlineQueryResult): description: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - video_url: base.String, - mime_type: base.String, - thumb_url: base.String, - title: base.String, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - video_width: typing.Optional[base.Integer] = None, - video_height: typing.Optional[base.Integer] = None, - video_duration: typing.Optional[base.Integer] = None, - description: typing.Optional[base.String] = None, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, - input_message_content: typing.Optional[InputMessageContent] = None): - super(InlineQueryResultVideo, self).__init__(id=id, video_url=video_url, mime_type=mime_type, - thumb_url=thumb_url, title=title, caption=caption, - video_width=video_width, video_height=video_height, - video_duration=video_duration, description=description, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + video_url: base.String, + mime_type: base.String, + thumb_url: base.String, + title: base.String, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + video_width: typing.Optional[base.Integer] = None, + video_height: typing.Optional[base.Integer] = None, + video_duration: typing.Optional[base.Integer] = None, + description: typing.Optional[base.String] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, video_url=video_url, mime_type=mime_type, thumb_url=thumb_url, + title=title, caption=caption, video_width=video_width, + video_height=video_height, video_duration=video_duration, + description=description, parse_mode=parse_mode, + reply_markup=reply_markup, caption_entities=caption_entities, + input_message_content=input_message_content, + ) class InlineQueryResultAudio(InlineQueryResult): @@ -248,21 +274,27 @@ class InlineQueryResultAudio(InlineQueryResult): audio_duration: base.Integer = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - audio_url: base.String, - title: base.String, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - performer: typing.Optional[base.String] = None, - audio_duration: typing.Optional[base.Integer] = None, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, - input_message_content: typing.Optional[InputMessageContent] = None): - super(InlineQueryResultAudio, self).__init__(id=id, audio_url=audio_url, title=title, - caption=caption, parse_mode=parse_mode, - performer=performer, audio_duration=audio_duration, - reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + audio_url: base.String, + title: base.String, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + performer: typing.Optional[base.String] = None, + audio_duration: typing.Optional[base.Integer] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, audio_url=audio_url, title=title, + caption=caption, parse_mode=parse_mode, + performer=performer, audio_duration=audio_duration, + reply_markup=reply_markup, caption_entities=caption_entities, + input_message_content=input_message_content, + ) class InlineQueryResultVoice(InlineQueryResult): @@ -285,19 +317,25 @@ class InlineQueryResultVoice(InlineQueryResult): voice_duration: base.Integer = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - voice_url: base.String, - title: base.String, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - voice_duration: typing.Optional[base.Integer] = None, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, - input_message_content: typing.Optional[InputMessageContent] = None): - super(InlineQueryResultVoice, self).__init__(id=id, voice_url=voice_url, title=title, - caption=caption, voice_duration=voice_duration, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + voice_url: base.String, + title: base.String, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + voice_duration: typing.Optional[base.Integer] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, voice_url=voice_url, title=title, caption=caption, + voice_duration=voice_duration, parse_mode=parse_mode, + reply_markup=reply_markup, caption_entities=caption_entities, + input_message_content=input_message_content, + ) class InlineQueryResultDocument(InlineQueryResult): @@ -323,25 +361,31 @@ class InlineQueryResultDocument(InlineQueryResult): thumb_width: base.Integer = fields.Field() thumb_height: base.Integer = fields.Field() - def __init__(self, *, - id: base.String, - title: base.String, - caption: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - document_url: typing.Optional[base.String] = None, - mime_type: typing.Optional[base.String] = None, - description: typing.Optional[base.String] = None, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, - input_message_content: typing.Optional[InputMessageContent] = None, - thumb_url: typing.Optional[base.String] = None, - thumb_width: typing.Optional[base.Integer] = None, - thumb_height: typing.Optional[base.Integer] = None): - super(InlineQueryResultDocument, self).__init__(id=id, title=title, caption=caption, - document_url=document_url, mime_type=mime_type, - description=description, reply_markup=reply_markup, - input_message_content=input_message_content, - thumb_url=thumb_url, thumb_width=thumb_width, - thumb_height=thumb_height, parse_mode=parse_mode) + def __init__( + self, + *, + id: base.String, + title: base.String, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + document_url: typing.Optional[base.String] = None, + mime_type: typing.Optional[base.String] = None, + description: typing.Optional[base.String] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + thumb_url: typing.Optional[base.String] = None, + thumb_width: typing.Optional[base.Integer] = None, + thumb_height: typing.Optional[base.Integer] = None, + ): + super().__init__( + id=id, title=title, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, document_url=document_url, + mime_type=mime_type, description=description, reply_markup=reply_markup, + input_message_content=input_message_content, + thumb_url=thumb_url, thumb_width=thumb_width, + thumb_height=thumb_height, + ) class InlineQueryResultLocation(InlineQueryResult): @@ -352,16 +396,16 @@ class InlineQueryResultLocation(InlineQueryResult): Alternatively, you can use input_message_content to send a message with the specified content instead of the location. - Note: This will only work in Telegram versions released after 9 April, 2016. - Older clients will ignore them. - https://core.telegram.org/bots/api#inlinequeryresultlocation """ type: base.String = fields.Field(alias='type', default='location') latitude: base.Float = fields.Field() longitude: base.Float = fields.Field() title: base.String = fields.Field() + horizontal_accuracy: typing.Optional[base.Float] = fields.Field() live_period: base.Integer = fields.Field() + heading: typing.Optional[base.Integer] = fields.Field() + proximity_alert_radius: typing.Optional[base.Integer] = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) thumb_url: base.String = fields.Field() thumb_width: base.Integer = fields.Field() @@ -372,18 +416,31 @@ class InlineQueryResultLocation(InlineQueryResult): latitude: base.Float, longitude: base.Float, title: base.String, + horizontal_accuracy: typing.Optional[base.Float] = None, live_period: typing.Optional[base.Integer] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, reply_markup: typing.Optional[InlineKeyboardMarkup] = None, input_message_content: typing.Optional[InputMessageContent] = None, thumb_url: typing.Optional[base.String] = None, thumb_width: typing.Optional[base.Integer] = None, - thumb_height: typing.Optional[base.Integer] = None): - super(InlineQueryResultLocation, self).__init__(id=id, latitude=latitude, longitude=longitude, - title=title, live_period=live_period, - reply_markup=reply_markup, - input_message_content=input_message_content, - thumb_url=thumb_url, thumb_width=thumb_width, - thumb_height=thumb_height) + thumb_height: typing.Optional[base.Integer] = None, + ): + super().__init__( + id=id, + latitude=latitude, + longitude=longitude, + title=title, + horizontal_accuracy=horizontal_accuracy, + live_period=live_period, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + reply_markup=reply_markup, + input_message_content=input_message_content, + thumb_url=thumb_url, + thumb_width=thumb_width, + thumb_height=thumb_height + ) class InlineQueryResultVenue(InlineQueryResult): @@ -404,31 +461,40 @@ class InlineQueryResultVenue(InlineQueryResult): title: base.String = fields.Field() address: base.String = fields.Field() foursquare_id: base.String = fields.Field() + foursquare_type: base.String = fields.Field() + google_place_id: base.String = fields.Field() + google_place_type: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) thumb_url: base.String = fields.Field() thumb_width: base.Integer = fields.Field() thumb_height: base.Integer = fields.Field() - foursquare_type: base.String = fields.Field() - def __init__(self, *, - id: base.String, - latitude: base.Float, - longitude: base.Float, - title: base.String, - address: base.String, - foursquare_id: typing.Optional[base.String] = None, - reply_markup: typing.Optional[InlineKeyboardMarkup] = None, - input_message_content: typing.Optional[InputMessageContent] = None, - thumb_url: typing.Optional[base.String] = None, - thumb_width: typing.Optional[base.Integer] = None, - thumb_height: typing.Optional[base.Integer] = None, - foursquare_type: typing.Optional[base.String] = None): - super(InlineQueryResultVenue, self).__init__(id=id, latitude=latitude, longitude=longitude, - title=title, address=address, foursquare_id=foursquare_id, - reply_markup=reply_markup, - input_message_content=input_message_content, thumb_url=thumb_url, - thumb_width=thumb_width, thumb_height=thumb_height, - foursquare_type=foursquare_type) + def __init__( + self, + *, + id: base.String, + latitude: base.Float, + longitude: base.Float, + title: base.String, + address: base.String, + foursquare_id: typing.Optional[base.String] = None, + foursquare_type: typing.Optional[base.String] = None, + google_place_id: typing.Optional[base.String] = None, + google_place_type: typing.Optional[base.String] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + thumb_url: typing.Optional[base.String] = None, + thumb_width: typing.Optional[base.Integer] = None, + thumb_height: typing.Optional[base.Integer] = None, + ): + super().__init__( + id=id, latitude=latitude, longitude=longitude, title=title, + address=address, foursquare_id=foursquare_id, + foursquare_type=foursquare_type, google_place_id=google_place_id, + google_place_type=google_place_type, reply_markup=reply_markup, + input_message_content=input_message_content, thumb_url=thumb_url, + thumb_width=thumb_width, thumb_height=thumb_height, + ) class InlineQueryResultContact(InlineQueryResult): @@ -510,19 +576,24 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - photo_file_id: base.String, - 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(InlineQueryResultCachedPhoto, self).__init__(id=id, photo_file_id=photo_file_id, title=title, - description=description, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + photo_file_id: base.String, + 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, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, photo_file_id=photo_file_id, title=title, description=description, + caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + reply_markup=reply_markup, input_message_content=input_message_content, + ) class InlineQueryResultCachedGif(InlineQueryResult): @@ -541,18 +612,23 @@ class InlineQueryResultCachedGif(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - gif_file_id: base.String, - title: 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(InlineQueryResultCachedGif, self).__init__(id=id, gif_file_id=gif_file_id, - title=title, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + gif_file_id: base.String, + title: typing.Optional[base.String] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, gif_file_id=gif_file_id, title=title, caption=caption, + parse_mode=parse_mode, caption_entities=caption_entities, + reply_markup=reply_markup, input_message_content=input_message_content, + ) class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): @@ -571,18 +647,23 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - mpeg4_file_id: base.String, - title: 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(InlineQueryResultCachedMpeg4Gif, self).__init__(id=id, mpeg4_file_id=mpeg4_file_id, - title=title, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + mpeg4_file_id: base.String, + title: typing.Optional[base.String] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, mpeg4_file_id=mpeg4_file_id, title=title, caption=caption, + parse_mode=parse_mode, caption_entities=caption_entities, + reply_markup=reply_markup, input_message_content=input_message_content, + ) class InlineQueryResultCachedSticker(InlineQueryResult): @@ -631,20 +712,25 @@ class InlineQueryResultCachedDocument(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - title: base.String, - document_file_id: base.String, - 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(InlineQueryResultCachedDocument, self).__init__(id=id, title=title, - document_file_id=document_file_id, - description=description, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + title: base.String, + document_file_id: base.String, + description: typing.Optional[base.String] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, title=title, document_file_id=document_file_id, + description=description, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, reply_markup=reply_markup, + input_message_content=input_message_content, + ) class InlineQueryResultCachedVideo(InlineQueryResult): @@ -664,19 +750,24 @@ class InlineQueryResultCachedVideo(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - video_file_id: base.String, - title: base.String, - 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(InlineQueryResultCachedVideo, self).__init__(id=id, video_file_id=video_file_id, title=title, - description=description, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + video_file_id: base.String, + title: base.String, + description: typing.Optional[base.String] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, video_file_id=video_file_id, title=title, description=description, + caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + reply_markup=reply_markup, input_message_content=input_message_content, + ) class InlineQueryResultCachedVoice(InlineQueryResult): @@ -697,18 +788,23 @@ class InlineQueryResultCachedVoice(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - voice_file_id: base.String, - title: base.String, - 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(InlineQueryResultCachedVoice, self).__init__(id=id, voice_file_id=voice_file_id, - title=title, caption=caption, - parse_mode=parse_mode, reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + voice_file_id: base.String, + title: base.String, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, voice_file_id=voice_file_id, title=title, caption=caption, + parse_mode=parse_mode, caption_entities=caption_entities, + reply_markup=reply_markup, input_message_content=input_message_content, + ) class InlineQueryResultCachedAudio(InlineQueryResult): @@ -729,14 +825,19 @@ class InlineQueryResultCachedAudio(InlineQueryResult): caption: base.String = fields.Field() input_message_content: InputMessageContent = fields.Field(base=InputMessageContent) - def __init__(self, *, - id: base.String, - audio_file_id: base.String, - 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(InlineQueryResultCachedAudio, self).__init__(id=id, audio_file_id=audio_file_id, - caption=caption, parse_mode=parse_mode, - reply_markup=reply_markup, - input_message_content=input_message_content) + def __init__( + self, + *, + id: base.String, + audio_file_id: base.String, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, + input_message_content: typing.Optional[InputMessageContent] = None, + ): + super().__init__( + id=id, audio_file_id=audio_file_id, caption=caption, + parse_mode=parse_mode, caption_entities=caption_entities, + reply_markup=reply_markup, input_message_content=input_message_content, + ) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 25422df1..943a534c 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -5,6 +5,7 @@ import typing from . import base from . import fields from .input_file import InputFile +from .message_entity import MessageEntity ATTACHMENT_PREFIX = 'attach://' @@ -106,28 +107,48 @@ class InputMediaAnimation(InputMedia): height: base.Integer = fields.Field() duration: base.Integer = fields.Field() - 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, - parse_mode: base.String = None, **kwargs): - super(InputMediaAnimation, self).__init__(type='animation', media=media, thumb=thumb, caption=caption, - width=width, height=height, duration=duration, - parse_mode=parse_mode, conf=kwargs) + 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, + parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + **kwargs, + ): + super().__init__( + type='animation', media=media, thumb=thumb, caption=caption, width=width, + height=height, duration=duration, parse_mode=parse_mode, + caption_entities=caption_entities, conf=kwargs, + ) class InputMediaDocument(InputMedia): """ - Represents a photo to be sent. + Represents a general file to be sent. https://core.telegram.org/bots/api#inputmediadocument """ - def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, - caption: base.String = None, parse_mode: base.String = None, **kwargs): - super(InputMediaDocument, self).__init__(type='document', media=media, thumb=thumb, - caption=caption, parse_mode=parse_mode, - conf=kwargs) + def __init__( + self, + media: base.InputFile, + thumb: typing.Union[base.InputFile, base.String, None] = None, + caption: base.String = None, + parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_content_type_detection: typing.Optional[base.Boolean] = None, + **kwargs, + ): + super().__init__( + type='document', media=media, thumb=thumb, caption=caption, + parse_mode=parse_mode, caption_entities=caption_entities, + disable_content_type_detection=disable_content_type_detection, + conf=kwargs, + ) class InputMediaAudio(InputMedia): @@ -141,17 +162,23 @@ class InputMediaAudio(InputMedia): performer: base.String = fields.Field() title: base.String = fields.Field() - def __init__(self, media: base.InputFile, - thumb: typing.Union[base.InputFile, base.String] = None, - caption: base.String = 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, duration=duration, - performer=performer, title=title, - parse_mode=parse_mode, conf=kwargs) + def __init__( + self, + media: base.InputFile, + thumb: typing.Union[base.InputFile, base.String] = None, + caption: base.String = None, + duration: base.Integer = None, + performer: base.String = None, + title: base.String = None, + parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + **kwargs, + ): + super().__init__( + type='audio', media=media, thumb=thumb, caption=caption, + duration=duration, performer=performer, title=title, + parse_mode=parse_mode, caption_entities=caption_entities, conf=kwargs, + ) class InputMediaPhoto(InputMedia): @@ -161,11 +188,18 @@ class InputMediaPhoto(InputMedia): https://core.telegram.org/bots/api#inputmediaphoto """ - def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, - caption: base.String = None, parse_mode: base.String = None, **kwargs): - super(InputMediaPhoto, self).__init__(type='photo', media=media, thumb=thumb, - caption=caption, parse_mode=parse_mode, - conf=kwargs) + def __init__( + self, + media: base.InputFile, + caption: base.String = None, + parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + **kwargs, + ): + super().__init__( + type='photo', media=media, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, conf=kwargs, + ) class InputMediaVideo(InputMedia): @@ -179,16 +213,25 @@ class InputMediaVideo(InputMedia): duration: base.Integer = fields.Field() supports_streaming: base.Boolean = fields.Field() - 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, - parse_mode: base.String = None, - supports_streaming: base.Boolean = None, **kwargs): - super(InputMediaVideo, self).__init__(type='video', media=media, thumb=thumb, caption=caption, - width=width, height=height, duration=duration, - parse_mode=parse_mode, - supports_streaming=supports_streaming, conf=kwargs) + 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, + parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + supports_streaming: base.Boolean = None, + **kwargs, + ): + super().__init__( + type='video', media=media, thumb=thumb, caption=caption, + width=width, height=height, duration=duration, + parse_mode=parse_mode, caption_entities=caption_entities, + supports_streaming=supports_streaming, conf=kwargs + ) class MediaGroup(base.TelegramObject): diff --git a/aiogram/types/input_message_content.py b/aiogram/types/input_message_content.py index 736a4454..522d44c5 100644 --- a/aiogram/types/input_message_content.py +++ b/aiogram/types/input_message_content.py @@ -2,6 +2,7 @@ import typing from . import base from . import fields +from .message_entity import MessageEntity class InputMessageContent(base.TelegramObject): @@ -40,17 +41,31 @@ class InputLocationMessageContent(InputMessageContent): """ Represents the content of a location message to be sent as the result of an inline query. - Note: This will only work in Telegram versions released after 9 April, 2016. - Older clients will ignore them. - https://core.telegram.org/bots/api#inputlocationmessagecontent """ latitude: base.Float = fields.Field() longitude: base.Float = fields.Field() + horizontal_accuracy: typing.Optional[base.Float] = fields.Field() + live_period: typing.Optional[base.Integer] = fields.Field() + heading: typing.Optional[base.Integer] = fields.Field() + proximity_alert_radius: typing.Optional[base.Integer] = fields.Field() - def __init__(self, latitude: base.Float, - longitude: base.Float): - super(InputLocationMessageContent, self).__init__(latitude=latitude, longitude=longitude) + def __init__(self, + latitude: base.Float, + longitude: base.Float, + horizontal_accuracy: typing.Optional[base.Float] = None, + live_period: typing.Optional[base.Integer] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, + ): + super().__init__( + latitude=latitude, + longitude=longitude, + horizontal_accuracy=horizontal_accuracy, + live_period=live_period, + heading=heading, + proximity_alert_radius=proximity_alert_radius, + ) class InputTextMessageContent(InputMessageContent): @@ -69,14 +84,21 @@ class InputTextMessageContent(InputMessageContent): except RuntimeError: pass - def __init__(self, message_text: typing.Optional[base.String] = None, - parse_mode: typing.Optional[base.String] = None, - disable_web_page_preview: typing.Optional[base.Boolean] = None): + def __init__( + self, + message_text: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, + ): if parse_mode is None: parse_mode = self.safe_get_parse_mode() - super(InputTextMessageContent, self).__init__(message_text=message_text, parse_mode=parse_mode, - disable_web_page_preview=disable_web_page_preview) + super().__init__( + message_text=message_text, parse_mode=parse_mode, + caption_entities=caption_entities, + disable_web_page_preview=disable_web_page_preview, + ) class InputVenueMessageContent(InputMessageContent): @@ -93,11 +115,24 @@ class InputVenueMessageContent(InputMessageContent): title: base.String = fields.Field() address: base.String = fields.Field() foursquare_id: base.String = fields.Field() + foursquare_type: base.String = fields.Field() + google_place_id: base.String = fields.Field() + google_place_type: base.String = fields.Field() - def __init__(self, latitude: typing.Optional[base.Float] = None, - longitude: typing.Optional[base.Float] = None, - title: typing.Optional[base.String] = None, - address: typing.Optional[base.String] = None, - foursquare_id: typing.Optional[base.String] = None): - super(InputVenueMessageContent, self).__init__(latitude=latitude, longitude=longitude, title=title, - address=address, foursquare_id=foursquare_id) + def __init__( + self, + latitude: typing.Optional[base.Float] = None, + longitude: typing.Optional[base.Float] = None, + title: typing.Optional[base.String] = None, + address: typing.Optional[base.String] = None, + foursquare_id: typing.Optional[base.String] = None, + foursquare_type: typing.Optional[base.String] = None, + google_place_id: typing.Optional[base.String] = None, + google_place_type: typing.Optional[base.String] = None, + ): + super().__init__( + latitude=latitude, longitude=longitude, title=title, + address=address, foursquare_id=foursquare_id, + foursquare_type=foursquare_type, google_place_id=google_place_id, + google_place_type=google_place_type, + ) diff --git a/aiogram/types/location.py b/aiogram/types/location.py index ea2f81c4..5f159e33 100644 --- a/aiogram/types/location.py +++ b/aiogram/types/location.py @@ -1,3 +1,5 @@ +import typing + from . import base from . import fields @@ -10,3 +12,7 @@ class Location(base.TelegramObject): """ longitude: base.Float = fields.Field() latitude: base.Float = fields.Field() + horizontal_accuracy: typing.Optional[base.Float] = fields.Field() + live_period: typing.Optional[base.Integer] = fields.Field() + heading: typing.Optional[base.Integer] = fields.Field() + proximity_alert_radius: typing.Optional[base.Integer] = fields.Field() diff --git a/aiogram/types/message.py b/aiogram/types/message.py index fc6bc77b..900b731c 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -6,6 +6,7 @@ import typing from ..utils import helper from ..utils import markdown as md +from ..utils.deprecated import deprecated from ..utils.text_decorations import html_decoration, markdown_decoration from . import base, fields from .animation import Animation @@ -21,9 +22,11 @@ from .input_media import InputMedia, MediaGroup from .invoice import Invoice from .location import Location from .message_entity import MessageEntity +from .message_id import MessageId from .passport_data import PassportData from .photo_size import PhotoSize from .poll import Poll +from .proximity_alert_triggered import ProximityAlertTriggered from .reply_keyboard import ReplyKeyboardMarkup, ReplyKeyboardRemove from .sticker import Sticker from .successful_payment import SuccessfulPayment @@ -43,6 +46,7 @@ class Message(base.TelegramObject): message_id: base.Integer = fields.Field() from_user: User = fields.Field(alias="from", base=User) + sender_chat: Chat = fields.Field(base=Chat) date: datetime.datetime = fields.DateTimeField() chat: Chat = fields.Field(base=Chat) forward_from: User = fields.Field(base=User) @@ -89,6 +93,7 @@ class Message(base.TelegramObject): successful_payment: SuccessfulPayment = fields.Field(base=SuccessfulPayment) connected_website: base.String = fields.Field() passport_data: PassportData = fields.Field(base=PassportData) + proximity_alert_triggered: ProximityAlertTriggered = fields.Field(base=ProximityAlertTriggered) reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup) @property @@ -150,6 +155,8 @@ class Message(base.TelegramObject): return ContentType.GROUP_CHAT_CREATED if self.passport_data: return ContentType.PASSPORT_DATA + if self.proximity_alert_triggered: + return ContentType.PROXIMITY_ALERT_TRIGGERED return ContentType.UNKNOWN @@ -280,9 +287,11 @@ class Message(base.TelegramObject): 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, + parse_mode: typing.Optional[base.String] = None, + entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -297,19 +306,33 @@ class Message(base.TelegramObject): :param text: Text of the message to be sent :type text: :obj:`base.String` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_web_page_preview: Disables link previews for links in this message - :type disable_web_page_preview: :obj:`typing.Union[base.Boolean, None]` + :type disable_web_page_preview: :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.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -317,18 +340,22 @@ class Message(base.TelegramObject): chat_id=self.chat.id, text=text, parse_mode=parse_mode, + entities=entities, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -345,19 +372,33 @@ class Message(base.TelegramObject): :param photo: Photo to send :type photo: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Photo caption (may also be used when resending photos by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -366,21 +407,25 @@ class Message(base.TelegramObject): photo=photo, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + performer: typing.Optional[base.String] = None, + title: typing.Optional[base.String] = None, thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -400,27 +445,46 @@ class Message(base.TelegramObject): :param audio: Audio file to send. :type audio: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Audio caption, 0-200 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param duration: Duration of the audio in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param performer: Performer - :type performer: :obj:`typing.Union[base.String, None]` + :type performer: :obj:`typing.Optional[base.String]` + :param title: Track name - :type title: :obj:`typing.Union[base.String, None]` + :type title: :obj:`typing.Optional[base.String]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -429,25 +493,29 @@ class Message(base.TelegramObject): audio=audio, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, duration=duration, performer=performer, title=title, thumb=thumb, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -469,27 +537,46 @@ class Message(base.TelegramObject): on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data :type animation: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent animation in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param width: Animation width - :type width: :obj:`typing.Union[base.Integer, None]` + :type width: :obj:`typing.Optional[base.Integer]` + :param height: Animation height - :type height: :obj:`typing.Union[base.Integer, None]` + :type height: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` + :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -502,8 +589,10 @@ class Message(base.TelegramObject): thumb=thumb, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) @@ -511,9 +600,12 @@ class Message(base.TelegramObject): self, document: typing.Union[base.InputFile, base.String], 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_content_type_detection: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -524,30 +616,53 @@ class Message(base.TelegramObject): reply: base.Boolean = False, ) -> Message: """ - Use this method to send general files. - - Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. + Use this method to send general files. On success, the sent Message is + returned. Bots can currently send files of any type of up to 50 MB in size, + this limit may be changed in the future. Source: https://core.telegram.org/bots/api#senddocument - :param document: File to send. + :param document: File to send :type document: :obj:`typing.Union[base.InputFile, base.String]` - :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnailโ€˜s width and height should not exceed 320. - :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` - :param caption: Document caption (may also be used when resending documents by file_id), 0-200 characters - :type caption: :obj:`typing.Union[base.String, None]` - :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, - fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + + :param thumb: Thumbnail of the file sent + :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` + + :param caption: Document caption (may also be used when resending documents + by file_id), 0-1024 characters + :type caption: :obj:`typing.Optional[base.String]` + + :param disable_content_type_detection: Disables automatic server-side content + type detection for files uploaded using multipart/form-data + :type disable_content_type_detection: :obj:`typing.Optional[base.Boolean]` + + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in your bot's message. + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + + :param disable_notification: Sends the message silently. Users will receive a + notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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. + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], + None]` + + :param reply: True if the message is a reply + :type reply: :obj:`typing.Optional[base.Boolean]` + + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ return await self.bot.send_document( @@ -556,21 +671,26 @@ class Message(base.TelegramObject): document=document, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, + disable_content_type_detection=disable_content_type_detection, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, - 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, + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = None, + thumb: typing.Union[base.InputFile, base.String, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -588,27 +708,46 @@ class Message(base.TelegramObject): :param video: Video to send. :type video: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent video in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param width: Video width - :type width: :obj:`typing.Union[base.Integer, None]` + :type width: :obj:`typing.Optional[base.Integer]` + :param height: Video height - :type height: :obj:`typing.Union[base.Integer, None]` + :type height: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. - :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` + :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` + :param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -621,18 +760,22 @@ class Message(base.TelegramObject): thumb=thumb, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -653,20 +796,36 @@ class Message(base.TelegramObject): :param voice: Audio file to send. :type voice: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Voice message caption, 0-200 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param duration: Duration of the voice message in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -675,19 +834,22 @@ class Message(base.TelegramObject): voice=voice, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, duration=duration, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + duration: typing.Optional[base.Integer] = None, + length: typing.Optional[base.Integer] = None, thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -705,20 +867,32 @@ class Message(base.TelegramObject): :param video_note: Video note to send. :type video_note: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent video in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param length: Video width and height - :type length: :obj:`typing.Union[base.Integer, None]` + :type length: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -730,25 +904,39 @@ class Message(base.TelegramObject): thumb=thumb, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) async def answer_media_group( self, media: typing.Union[MediaGroup, typing.List], - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply: base.Boolean = False, ) -> typing.List[Message]: """ - Use this method to send a group of photos or videos as an album. + Use this method to send a group of photos, videos, documents or audios as + an album. Documents and audio files can be only group in an album with + messages of the same type. On success, an array of Messages that were sent + is returned. Source: https://core.telegram.org/bots/api#sendmediagroup :param media: A JSON-serialized array describing photos and videos to be sent :type media: :obj:`typing.Union[types.MediaGroup, typing.List]` - :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 disable_notification: Sends the message silently. Users will receive + a notification with no sound. + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, an array of the sent Messages is returned. :rtype: typing.List[types.Message] """ @@ -757,14 +945,16 @@ class Message(base.TelegramObject): media=media, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, ) 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, + live_period: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -781,17 +971,28 @@ class Message(base.TelegramObject): :param latitude: Latitude of the location :type latitude: :obj:`base.Float` + :param longitude: Longitude of the location :type longitude: :obj:`base.Float` + :param live_period: Period in seconds for which the location will be updated - :type live_period: :obj:`typing.Union[base.Integer, None]` + :type live_period: :obj:`typing.Optional[base.Integer]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -802,6 +1003,7 @@ class Message(base.TelegramObject): live_period=live_period, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) @@ -811,8 +1013,12 @@ class Message(base.TelegramObject): 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, + foursquare_id: typing.Optional[base.String] = None, + foursquare_type: typing.Optional[base.String] = None, + google_place_id: typing.Optional[base.String] = None, + google_place_type: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -827,23 +1033,53 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#sendvenue + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param latitude: Latitude of the venue :type latitude: :obj:`base.Float` + :param longitude: Longitude of the venue :type longitude: :obj:`base.Float` + :param title: Name of the venue :type title: :obj:`base.String` + :param address: Address of the venue :type address: :obj:`base.String` + :param foursquare_id: Foursquare identifier of the venue - :type foursquare_id: :obj:`typing.Union[base.String, None]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type foursquare_id: :obj:`typing.Optional[base.String]` + + :param foursquare_type: Foursquare type of the venue, if known + :type foursquare_type: :obj:`typing.Optional[base.String]` + + :param google_place_id: Google Places identifier of the venue + :type google_place_id: :obj:`typing.Optional[base.String]` + + :param google_place_type: Google Places type of the venue. See supported + types: https://developers.google.com/places/web-service/supported_types + :type google_place_type: :obj:`typing.Optional[base.String]` + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, + None]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -854,8 +1090,12 @@ class Message(base.TelegramObject): title=title, address=address, foursquare_id=foursquare_id, + foursquare_type=foursquare_type, + google_place_id=google_place_id, + google_place_type=google_place_type, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) @@ -863,8 +1103,9 @@ class Message(base.TelegramObject): 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, + last_name: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -881,17 +1122,28 @@ class Message(base.TelegramObject): :param phone_number: Contact's phone number :type phone_number: :obj:`base.String` + :param first_name: Contact's first name :type first_name: :obj:`base.String` + :param last_name: Contact's last name - :type last_name: :obj:`typing.Union[base.String, None]` + :type last_name: :obj:`typing.Optional[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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -902,13 +1154,15 @@ class Message(base.TelegramObject): last_name=last_name, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) async def answer_sticker( self, sticker: typing.Union[base.InputFile, base.String], - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -925,13 +1179,22 @@ class Message(base.TelegramObject): :param sticker: Sticker to send. :type sticker: :obj:`typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -940,6 +1203,7 @@ class Message(base.TelegramObject): sticker=sticker, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) @@ -953,10 +1217,12 @@ class Message(base.TelegramObject): 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, None] = None, + explanation_entities: typing.Optional[typing.List[MessageEntity]] = None, + open_period: typing.Optional[base.Integer] = 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, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -967,40 +1233,75 @@ class Message(base.TelegramObject): reply: base.Boolean = False, ) -> Message: """ - Use this method to send a native poll. A native poll can't be sent to a private chat. - On success, the sent Message is returned. + Use this method to send a native poll. On success, the sent Message is + returned. Source: https://core.telegram.org/bots/api#sendpoll :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 :type options: :obj:`typing.List[base.String]` + :param is_anonymous: True, if the poll needs to be anonymous, defaults to True :type is_anonymous: :obj:`typing.Optional[base.Boolean]` + :param type: Poll type, โ€œquizโ€ or โ€œregularโ€, defaults to โ€œregularโ€ :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: True, if the poll allows multiple answers, + ignored for polls in quiz mode, defaults to False :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: 0-based identifier of the correct answer option, + required for polls in quiz mode :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 + + :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. + + :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, 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 explanation_entities: List of special entities that appear in message + text, which can be specified instead of parse_mode + :type explanation_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + + :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.Optional[base.Integer]` + + :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. + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound. :type disable_notification: :obj:`typing.Optional[Boolean]` - :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 + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, + None]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1014,18 +1315,21 @@ class Message(base.TelegramObject): correct_option_id=correct_option_id, explanation=explanation, explanation_parse_mode=explanation_parse_mode, + explanation_entities=explanation_entities, open_period=open_period, close_date=close_date, is_closed=is_closed, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) async def answer_dice( self, - emoji: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, + emoji: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1036,22 +1340,39 @@ class Message(base.TelegramObject): reply: base.Boolean = False, ) -> Message: """ - Use this method to send a dice, which will have a random value from 1 to 6. + Use this method to send an animated emoji that will display a random value. 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 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, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :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 โ€œ๐ŸŽฐโ€. Dice can have values 1-6 + for โ€œ๐ŸŽฒโ€ and โ€œ๐ŸŽฏโ€, values 1-5 for โ€œ๐Ÿ€โ€ and โ€œโšฝโ€, and values 1-64 for โ€œ๐ŸŽฐโ€. + Defaults to โ€œ๐ŸŽฒโ€ + :type emoji: :obj:`typing.Optional[base.String]` + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, + None]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1060,15 +1381,18 @@ class Message(base.TelegramObject): emoji=emoji, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + parse_mode: typing.Optional[base.String] = None, + entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1083,19 +1407,33 @@ class Message(base.TelegramObject): :param text: Text of the message to be sent :type text: :obj:`base.String` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_web_page_preview: Disables link previews for links in this message - :type disable_web_page_preview: :obj:`typing.Union[base.Boolean, None]` + :type disable_web_page_preview: :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.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1103,18 +1441,22 @@ class Message(base.TelegramObject): chat_id=self.chat.id, text=text, parse_mode=parse_mode, + entities=entities, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1131,19 +1473,33 @@ class Message(base.TelegramObject): :param photo: Photo to send :type photo: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Photo caption (may also be used when resending photos by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1152,21 +1508,25 @@ class Message(base.TelegramObject): photo=photo, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + performer: typing.Optional[base.String] = None, + title: typing.Optional[base.String] = None, thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1186,27 +1546,46 @@ class Message(base.TelegramObject): :param audio: Audio file to send. :type audio: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Audio caption, 0-200 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param duration: Duration of the audio in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param performer: Performer - :type performer: :obj:`typing.Union[base.String, None]` + :type performer: :obj:`typing.Optional[base.String]` + :param title: Track name - :type title: :obj:`typing.Union[base.String, None]` + :type title: :obj:`typing.Optional[base.String]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1215,25 +1594,29 @@ class Message(base.TelegramObject): audio=audio, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, duration=duration, performer=performer, title=title, thumb=thumb, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1255,27 +1638,46 @@ class Message(base.TelegramObject): on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data :type animation: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent animation in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param width: Animation width - :type width: :obj:`typing.Union[base.Integer, None]` + :type width: :obj:`typing.Optional[base.Integer]` + :param height: Animation height - :type height: :obj:`typing.Union[base.Integer, None]` + :type height: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` + :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1288,8 +1690,10 @@ class Message(base.TelegramObject): thumb=thumb, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) @@ -1297,9 +1701,12 @@ class Message(base.TelegramObject): self, document: typing.Union[base.InputFile, base.String], 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_content_type_detection: typing.Optional[base.Boolean] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1310,30 +1717,53 @@ class Message(base.TelegramObject): reply: base.Boolean = True, ) -> Message: """ - Use this method to send general files. - - Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. + Use this method to send general files. On success, the sent Message is + returned. Bots can currently send files of any type of up to 50 MB in size, + this limit may be changed in the future. Source: https://core.telegram.org/bots/api#senddocument - :param document: File to send. + :param document: File to send :type document: :obj:`typing.Union[base.InputFile, base.String]` - :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. - A thumbnailโ€˜s width and height should not exceed 320. - :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` - :param caption: Document caption (may also be used when resending documents by file_id), 0-200 characters - :type caption: :obj:`typing.Union[base.String, None]` - :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, - fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + + :param thumb: Thumbnail of the file sent + :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` + + :param caption: Document caption (may also be used when resending documents + by file_id), 0-1024 characters + :type caption: :obj:`typing.Optional[base.String]` + + :param disable_content_type_detection: Disables automatic server-side content + type detection for files uploaded using multipart/form-data + :type disable_content_type_detection: :obj:`typing.Optional[base.Boolean]` + + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in your bot's message. + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + + :param disable_notification: Sends the message silently. Users will receive a + notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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. + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], + None]` + + :param reply: True if the message is a reply + :type reply: :obj:`typing.Optional[base.Boolean]` + + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ return await self.bot.send_document( @@ -1342,21 +1772,26 @@ class Message(base.TelegramObject): thumb=thumb, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, + disable_content_type_detection=disable_content_type_detection, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, - 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, + duration: typing.Optional[base.Integer] = None, + width: typing.Optional[base.Integer] = None, + height: typing.Optional[base.Integer] = None, + thumb: typing.Union[base.InputFile, base.String, None] = None, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1374,27 +1809,46 @@ class Message(base.TelegramObject): :param video: Video to send. :type video: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent video in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param width: Video width - :type width: :obj:`typing.Union[base.Integer, None]` + :type width: :obj:`typing.Optional[base.Integer]` + :param height: Video height - :type height: :obj:`typing.Union[base.Integer, None]` + :type height: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. - :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` + :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` + :param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1407,18 +1861,22 @@ class Message(base.TelegramObject): thumb=thumb, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + duration: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1439,20 +1897,36 @@ class Message(base.TelegramObject): :param voice: Audio file to send. :type voice: :obj:`typing.Union[base.InputFile, base.String]` + :param caption: Voice message caption, 0-200 characters - :type caption: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in the media caption - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param duration: Duration of the voice message in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1461,19 +1935,22 @@ class Message(base.TelegramObject): voice=voice, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, duration=duration, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, 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, + duration: typing.Optional[base.Integer] = None, + length: typing.Optional[base.Integer] = None, thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1491,20 +1968,32 @@ class Message(base.TelegramObject): :param video_note: Video note to send. :type video_note: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent video in seconds - :type duration: :obj:`typing.Union[base.Integer, None]` + :type duration: :obj:`typing.Optional[base.Integer]` + :param length: Video width and height - :type length: :obj:`typing.Union[base.Integer, None]` + :type length: :obj:`typing.Optional[base.Integer]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None] + ` :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1516,25 +2005,39 @@ class Message(base.TelegramObject): thumb=thumb, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) async def reply_media_group( self, media: typing.Union[MediaGroup, typing.List], - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply: base.Boolean = True, ) -> typing.List[Message]: """ - Use this method to send a group of photos or videos as an album. + Use this method to send a group of photos, videos, documents or audios as + an album. Documents and audio files can be only group in an album with + messages of the same type. On success, an array of Messages that were sent + is returned. Source: https://core.telegram.org/bots/api#sendmediagroup :param media: A JSON-serialized array describing photos and videos to be sent :type media: :obj:`typing.Union[types.MediaGroup, typing.List]` - :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 disable_notification: Sends the message silently. Users will receive + a notification with no sound. + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, an array of the sent Messages is returned. :rtype: typing.List[types.Message] """ @@ -1543,14 +2046,15 @@ class Message(base.TelegramObject): media=media, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, ) 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, + live_period: typing.Optional[base.Integer] = None, + disable_notification: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1570,14 +2074,15 @@ class Message(base.TelegramObject): :param longitude: Longitude of the location :type longitude: :obj:`base.Float` :param live_period: Period in seconds for which the location will be updated - :type live_period: :obj:`typing.Union[base.Integer, None]` + :type live_period: :obj:`typing.Optional[base.Integer]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` :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' + :type reply: :obj:`base.Boolean` :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1597,8 +2102,12 @@ class Message(base.TelegramObject): 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, + foursquare_id: typing.Optional[base.String] = None, + foursquare_type: typing.Optional[base.String] = None, + google_place_id: typing.Optional[base.String] = None, + google_place_type: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1613,23 +2122,53 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#sendvenue + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param latitude: Latitude of the venue :type latitude: :obj:`base.Float` + :param longitude: Longitude of the venue :type longitude: :obj:`base.Float` + :param title: Name of the venue :type title: :obj:`base.String` + :param address: Address of the venue :type address: :obj:`base.String` + :param foursquare_id: Foursquare identifier of the venue - :type foursquare_id: :obj:`typing.Union[base.String, None]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound. - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type foursquare_id: :obj:`typing.Optional[base.String]` + + :param foursquare_type: Foursquare type of the venue, if known + :type foursquare_type: :obj:`typing.Optional[base.String]` + + :param google_place_id: Google Places identifier of the venue + :type google_place_id: :obj:`typing.Optional[base.String]` + + :param google_place_type: Google Places type of the venue. See supported + types: https://developers.google.com/places/web-service/supported_types + :type google_place_type: :obj:`typing.Optional[base.String]` + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, + None]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1640,8 +2179,12 @@ class Message(base.TelegramObject): title=title, address=address, foursquare_id=foursquare_id, + foursquare_type=foursquare_type, + google_place_id=google_place_id, + google_place_type=google_place_type, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) @@ -1649,8 +2192,9 @@ class Message(base.TelegramObject): 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, + last_name: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1667,17 +2211,28 @@ class Message(base.TelegramObject): :param phone_number: Contact's phone number :type phone_number: :obj:`base.String` + :param first_name: Contact's first name :type first_name: :obj:`base.String` + :param last_name: Contact's last name - :type last_name: :obj:`typing.Union[base.String, None]` + :type last_name: :obj:`typing.Optional[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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1688,6 +2243,7 @@ class Message(base.TelegramObject): last_name=last_name, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) @@ -1701,10 +2257,12 @@ class Message(base.TelegramObject): 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, None] = None, + explanation_entities: typing.Optional[typing.List[MessageEntity]] = None, + open_period: typing.Optional[base.Integer] = 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, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1715,40 +2273,75 @@ class Message(base.TelegramObject): reply: base.Boolean = True, ) -> Message: """ - Use this method to send a native poll. A native poll can't be sent to a private chat. - On success, the sent Message is returned. + Use this method to send a native poll. On success, the sent Message is + returned. Source: https://core.telegram.org/bots/api#sendpoll :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 :type options: :obj:`typing.List[base.String]` + :param is_anonymous: True, if the poll needs to be anonymous, defaults to True :type is_anonymous: :obj:`typing.Optional[base.Boolean]` + :param type: Poll type, โ€œquizโ€ or โ€œregularโ€, defaults to โ€œregularโ€ :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: True, if the poll allows multiple answers, + ignored for polls in quiz mode, defaults to False :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: 0-based identifier of the correct answer option, + required for polls in quiz mode :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 + + :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. + + :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, 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 explanation_entities: List of special entities that appear in message + text, which can be specified instead of parse_mode + :type explanation_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + + :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.Optional[base.Integer]` + + :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. + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound. :type disable_notification: :obj:`typing.Optional[Boolean]` - :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 + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, + None]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1762,18 +2355,21 @@ class Message(base.TelegramObject): correct_option_id=correct_option_id, explanation=explanation, explanation_parse_mode=explanation_parse_mode, + explanation_entities=explanation_entities, open_period=open_period, close_date=close_date, is_closed=is_closed, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) async def reply_sticker( self, sticker: typing.Union[base.InputFile, base.String], - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1790,13 +2386,22 @@ class Message(base.TelegramObject): :param sticker: Sticker to send. :type sticker: :obj:`typing.Union[base.InputFile, 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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + :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' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1805,13 +2410,15 @@ class Message(base.TelegramObject): sticker=sticker, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) async def reply_dice( self, - emoji: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, + emoji: typing.Optional[base.String] = None, + disable_notification: typing.Optional[base.Boolean] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -1822,22 +2429,39 @@ class Message(base.TelegramObject): reply: base.Boolean = True, ) -> Message: """ - Use this method to send a dice, which will have a random value from 1 to 6. + Use this method to send an animated emoji that will display a random value. 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 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, - custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :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 โ€œ๐ŸŽฐโ€. Dice can have values 1-6 + for โ€œ๐ŸŽฒโ€ and โ€œ๐ŸŽฏโ€, values 1-5 for โ€œ๐Ÿ€โ€ and โ€œโšฝโ€, and values 1-64 for โ€œ๐ŸŽฐโ€. + Defaults to โ€œ๐ŸŽฒโ€ + :type emoji: :obj:`typing.Optional[base.String]` + + :param disable_notification: Sends the message silently. Users will receive + a notification with no sound + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + + :param allow_sending_without_reply: Pass True, if the message should be sent + even if the specified replied-to message is not found + :type allow_sending_without_reply: :obj:`typing.Optional[base.Boolean]` + + :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]` + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, + None]` + :param reply: fill 'reply_to_message_id' + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -1846,13 +2470,14 @@ class Message(base.TelegramObject): emoji=emoji, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, + allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) async def forward( self, chat_id: typing.Union[base.Integer, base.String], - disable_notification: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Optional[base.Boolean] = None, ) -> Message: """ Forward this message @@ -1862,7 +2487,7 @@ class Message(base.TelegramObject): :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]` + :type disable_notification: :obj:`typing.Optional[base.Boolean]` :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ @@ -1873,9 +2498,10 @@ class Message(base.TelegramObject): 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, + parse_mode: typing.Optional[base.String] = None, + entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_web_page_preview: typing.Optional[base.Boolean] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = 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). @@ -1884,13 +2510,21 @@ class Message(base.TelegramObject): :param text: New text of the message :type text: :obj:`base.String` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param disable_web_page_preview: Disables link previews for links in this message - :type disable_web_page_preview: :obj:`typing.Union[base.Boolean, None]` + :type disable_web_page_preview: :obj:`typing.Optional[base.Boolean]` + :param reply_markup: A JSON-serialized object for an inline keyboard. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + :return: On success, if edited message is sent by the bot, the edited Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -1900,6 +2534,7 @@ class Message(base.TelegramObject): chat_id=self.chat.id, message_id=self.message_id, parse_mode=parse_mode, + entities=entities, disable_web_page_preview=disable_web_page_preview, reply_markup=reply_markup, ) @@ -1907,23 +2542,32 @@ class Message(base.TelegramObject): async def edit_caption( self, caption: base.String, - parse_mode: typing.Union[base.String, None] = None, - reply_markup: typing.Union[InlineKeyboardMarkup, None] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = 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). + Use this method to edit captions of messages sent by the bot or via the bot + (for inline bots). Source: https://core.telegram.org/bots/api#editmessagecaption :param caption: New caption of the message - :type caption: :obj:`typing.Union[base.String, None]` - :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, - fixed-width text or inline URLs in your bot's message. - :type parse_mode: :obj:`typing.Union[base.String, None]` + :type caption: :obj:`typing.Optional[base.String]` + + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show + bold, italic, fixed-width text or inline URLs in your bot's message. + :type parse_mode: :obj:`typing.Optional[base.String]` + + :param caption_entities: List of special entities that appear in message text, + which can be specified instead of parse_mode + :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param reply_markup: A JSON-serialized object for an inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` - :return: On success, if edited message is sent by the bot, the edited Message is returned, - otherwise True is returned. + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` + + :return: On success, if edited message is sent by the bot, the edited Message + is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ return await self.bot.edit_message_caption( @@ -1931,13 +2575,14 @@ class Message(base.TelegramObject): message_id=self.message_id, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, reply_markup=reply_markup, ) async def edit_media( self, media: InputMedia, - reply_markup: typing.Union[InlineKeyboardMarkup, None] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = None, ) -> typing.Union[Message, base.Boolean]: """ Use this method to edit audio, document, photo, or video messages. @@ -1954,7 +2599,7 @@ class Message(base.TelegramObject): :param media: A JSON-serialized object for a new media content of the message :type media: :obj:`types.InputMedia` :param reply_markup: A JSON-serialized object for a new inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` :return: On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -1967,7 +2612,7 @@ class Message(base.TelegramObject): ) async def edit_reply_markup( - self, reply_markup: typing.Union[InlineKeyboardMarkup, None] = None + self, reply_markup: typing.Optional[InlineKeyboardMarkup] = 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). @@ -1975,7 +2620,7 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#editmessagereplymarkup :param reply_markup: A JSON-serialized object for an inline keyboard - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` :return: On success, if edited message is sent by the bot, the edited Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -2000,7 +2645,7 @@ class Message(base.TelegramObject): self, latitude: base.Float, longitude: base.Float, - reply_markup: typing.Union[InlineKeyboardMarkup, None] = None, + reply_markup: typing.Optional[InlineKeyboardMarkup] = 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). @@ -2014,7 +2659,7 @@ class Message(base.TelegramObject): :param longitude: Longitude of new location :type longitude: :obj:`base.Float` :param reply_markup: A JSON-serialized object for a new inline keyboard. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` :return: On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -2028,7 +2673,7 @@ class Message(base.TelegramObject): ) async def stop_live_location( - self, reply_markup: typing.Union[InlineKeyboardMarkup, None] = None + self, reply_markup: typing.Optional[InlineKeyboardMarkup] = None ) -> typing.Union[Message, base.Boolean]: """ Use this method to stop updating a live location message sent by the bot or via the bot @@ -2037,7 +2682,7 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#stopmessagelivelocation :param reply_markup: A JSON-serialized object for a new inline keyboard. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` + :type reply_markup: :obj:`typing.Optional[types.InlineKeyboardMarkup]` :return: On success, if the message was sent by the bot, the sent Message is returned, otherwise True is returned. :rtype: :obj:`typing.Union[types.Message, base.Boolean]` @@ -2064,28 +2709,55 @@ 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 + self, disable_notification: typing.Optional[base.Boolean] = 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. + Use this method to add a message to the list of pinned messages in a chat. + If the chat is not a private chat, the bot must be an administrator in the + chat for this to work and must have the 'can_pin_messages' admin right in a + supergroup or 'can_edit_messages' admin right in a channel. Returns True on + success. Source: https://core.telegram.org/bots/api#pinchatmessage - :param disable_notification: Pass True, if it is not necessary to send a notification to - all group members about the new pinned message - :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param disable_notification: Pass True, if it is not necessary to send a + notification to all group members about the new pinned message + :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :return: Returns True on success :rtype: :obj:`base.Boolean` """ return await self.chat.pin_message(self.message_id, disable_notification) + async def unpin(self) -> base.Boolean: + """ + Use this method to remove a message from the list of pinned messages in a + chat. If the chat is not a private chat, the bot must be an administrator in + the chat for this to work and must have the 'can_pin_messages' admin right in + a supergroup or 'can_edit_messages' admin right in a channel. Returns True on + success. + + Source: https://core.telegram.org/bots/api#unpinchatmessage + + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + return await self.chat.unpin_message( + message_id=self.message_id, + ) + + @deprecated( + "This method deprecated since Bot API 4.5. Use method `copy_to` instead. \n" + "Read more: https://core.telegram.org/bots/api#copymessage", + stacklevel=3 + ) async def send_copy( self: Message, chat_id: typing.Union[str, int], disable_notification: typing.Optional[bool] = None, disable_web_page_preview: typing.Optional[bool] = None, reply_to_message_id: typing.Optional[int] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, None ] = None, @@ -2097,11 +2769,13 @@ class Message(base.TelegramObject): :param disable_notification: :param disable_web_page_preview: for text messages only :param reply_to_message_id: + :param allow_sending_without_reply: :param reply_markup: :return: """ kwargs = { "chat_id": chat_id, + "allow_sending_without_reply": allow_sending_without_reply, "reply_markup": reply_markup or self.reply_markup, "parse_mode": ParseMode.HTML, "disable_notification": disable_notification, @@ -2184,6 +2858,33 @@ class Message(base.TelegramObject): else: raise TypeError("This type of message can't be copied.") + async def copy_to( + self, + chat_id: typing.Union[base.Integer, base.String], + caption: typing.Optional[base.String] = None, + parse_mode: typing.Optional[base.String] = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_notification: typing.Optional[base.Boolean] = None, + reply_to_message_id: typing.Optional[base.Integer] = None, + allow_sending_without_reply: typing.Optional[base.Boolean] = None, + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + ) -> MessageId: + return await self.bot.copy_message( + chat_id=chat_id, + from_chat_id=self.chat.id, + message_id=self.message_id, + caption=caption, + parse_mode=parse_mode, + caption_entities=caption_entities, + disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, + allow_sending_without_reply=allow_sending_without_reply, + reply_markup=reply_markup + ) + def __int__(self): return self.message_id @@ -2247,6 +2948,7 @@ 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 + PROXIMITY_ALERT_TRIGGERED = helper.Item() # proximity_alert_triggered UNKNOWN = helper.Item() # unknown ANY = helper.Item() # any diff --git a/aiogram/types/message_id.py b/aiogram/types/message_id.py new file mode 100644 index 00000000..0157f5e7 --- /dev/null +++ b/aiogram/types/message_id.py @@ -0,0 +1,10 @@ +from . import base, fields + + +class MessageId(base.TelegramObject): + """ + This object represents a unique message identifier. + + https://core.telegram.org/bots/api#messageid + """ + message_id: base.String = fields.Field() diff --git a/aiogram/types/proximity_alert_triggered.py b/aiogram/types/proximity_alert_triggered.py new file mode 100644 index 00000000..240854d8 --- /dev/null +++ b/aiogram/types/proximity_alert_triggered.py @@ -0,0 +1,15 @@ +from . import base +from . import fields +from .user import User + + +class ProximityAlertTriggered(base.TelegramObject): + """ + This object represents the content of a service message, sent whenever a user in + the chat triggers a proximity alert set by another user. + + https://core.telegram.org/bots/api#proximityalerttriggered + """ + traveler: User = fields.Field() + watcher: User = fields.Field() + distance: base.Integer = fields.Field() diff --git a/aiogram/types/venue.py b/aiogram/types/venue.py index f7b2a277..b851650b 100644 --- a/aiogram/types/venue.py +++ b/aiogram/types/venue.py @@ -14,3 +14,5 @@ class Venue(base.TelegramObject): address: base.String = fields.Field() foursquare_id: base.String = fields.Field() foursquare_type: base.String = fields.Field() + google_place_id: base.String = fields.Field() + google_place_type: base.String = fields.Field() diff --git a/aiogram/types/video.py b/aiogram/types/video.py index 97dbb90f..d4958761 100644 --- a/aiogram/types/video.py +++ b/aiogram/types/video.py @@ -16,5 +16,6 @@ class Video(base.TelegramObject, mixins.Downloadable): height: base.Integer = fields.Field() duration: base.Integer = fields.Field() thumb: PhotoSize = fields.Field(base=PhotoSize) + file_name: base.String = fields.Field() mime_type: base.String = fields.Field() file_size: base.Integer = fields.Field() diff --git a/aiogram/types/webhook_info.py b/aiogram/types/webhook_info.py index 995d3aaa..dc1a7cd9 100644 --- a/aiogram/types/webhook_info.py +++ b/aiogram/types/webhook_info.py @@ -13,6 +13,7 @@ class WebhookInfo(base.TelegramObject): url: base.String = fields.Field() has_custom_certificate: base.Boolean = fields.Field() pending_update_count: base.Integer = fields.Field() + ip_address: base.String = fields.Field() last_error_date: base.Integer = fields.Field() last_error_message: base.String = fields.Field() max_connections: base.Integer = fields.Field() diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index fe4837e3..1b5b0b0b 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -354,7 +354,7 @@ class Executor: self.dispatcher.stop_polling() await self.dispatcher.storage.close() await self.dispatcher.storage.wait_closed() - await self.dispatcher.bot.close() + await self.dispatcher.bot.session.close() async def _startup_polling(self): await self._welcome() diff --git a/docs/source/examples/advanced_executor_example.rst b/docs/source/examples/advanced_executor_example.rst index 9eb5d950..a824ebaf 100644 --- a/docs/source/examples/advanced_executor_example.rst +++ b/docs/source/examples/advanced_executor_example.rst @@ -1,28 +1,8 @@ -.. Autogenerated file at 2018-10-28 19:31:48.335963 - ========================= Advanced executor example ========================= -!/usr/bin/env python3 -**This example is outdated** -In this example used ArgumentParser for configuring Your bot. -Provided to start bot with webhook: -python advanced_executor_example.py \ ---token TOKEN_HERE \ ---host 0.0.0.0 \ ---port 8084 \ ---host-name example.com \ ---webhook-port 443 -Or long polling: -python advanced_executor_example.py --token TOKEN_HERE -So... In this example found small trouble: -can't get bot instance in handlers. -If you want to automatic change getting updates method use executor utils (from aiogram.utils.executor) - TODO: Move token to environment variables. - .. literalinclude:: ../../../examples/advanced_executor_example.py :caption: advanced_executor_example.py :language: python :linenos: - :lines: 25- diff --git a/docs/source/examples/broadcast_example.rst b/docs/source/examples/broadcast_example.rst index 97556e73..837ad02f 100644 --- a/docs/source/examples/broadcast_example.rst +++ b/docs/source/examples/broadcast_example.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.593501 - ================= Broadcast example ================= diff --git a/docs/source/examples/check_user_language.rst b/docs/source/examples/check_user_language.rst index 0fc2d90e..5d859d43 100644 --- a/docs/source/examples/check_user_language.rst +++ b/docs/source/examples/check_user_language.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.558059 - =================== Check user language =================== @@ -10,4 +8,3 @@ Babel is required. :caption: check_user_language.py :language: python :linenos: - :lines: 5- diff --git a/docs/source/examples/echo_bot.rst b/docs/source/examples/echo_bot.rst index 18c45569..dfbecce1 100644 --- a/docs/source/examples/echo_bot.rst +++ b/docs/source/examples/echo_bot.rst @@ -1,8 +1,7 @@ +======== Echo bot ======== -Very simple example of the bot which will sent text of the received messages to the sender - .. literalinclude:: ../../../examples/echo_bot.py :caption: echo_bot.py :language: python diff --git a/docs/source/examples/finite_state_machine_example.rst b/docs/source/examples/finite_state_machine_example.rst index 3eea4a22..29e251b8 100644 --- a/docs/source/examples/finite_state_machine_example.rst +++ b/docs/source/examples/finite_state_machine_example.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.595032 - ============================ Finite state machine example ============================ diff --git a/docs/source/examples/i18n_example.rst b/docs/source/examples/i18n_example.rst index 875006be..dc794619 100644 --- a/docs/source/examples/i18n_example.rst +++ b/docs/source/examples/i18n_example.rst @@ -1,28 +1,8 @@ -.. Autogenerated file at 2018-09-08 02:07:37.591007 - ============ I18n example ============ -Internalize your bot -Step 1: extract texts -# pybabel extract i18n_example.py -o locales/mybot.pot -Step 2: create *.po files. For e.g. create en, ru, uk locales. -# echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l -Step 3: translate texts -Step 4: compile translations -# pybabel compile -d locales -D mybot -Step 5: When you change the code of your bot you need to update po & mo files. -Step 5.1: regenerate pot file: -command from step 1 -Step 5.2: update po files -# pybabel update -d locales -D mybot -i locales/mybot.pot -Step 5.3: update your translations -Step 5.4: compile mo files -command from step 4 - .. literalinclude:: ../../../examples/i18n_example.py :caption: i18n_example.py :language: python :linenos: - :lines: 22- diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index 2bedaa52..c5be4bfc 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -19,3 +19,4 @@ Examples payments broadcast_example media_group + local_server diff --git a/docs/source/examples/inline_bot.rst b/docs/source/examples/inline_bot.rst index b6059974..ca3d227c 100644 --- a/docs/source/examples/inline_bot.rst +++ b/docs/source/examples/inline_bot.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.561907 - ========== Inline bot ========== diff --git a/docs/source/examples/local_server.rst b/docs/source/examples/local_server.rst new file mode 100644 index 00000000..6e056a6f --- /dev/null +++ b/docs/source/examples/local_server.rst @@ -0,0 +1,8 @@ +============ +Local server +============ + +.. literalinclude:: ../../../examples/local_server.py + :caption: local_server.py + :language: python + :linenos: diff --git a/docs/source/examples/media_group.rst b/docs/source/examples/media_group.rst index ebf2d3f2..1eb12e4f 100644 --- a/docs/source/examples/media_group.rst +++ b/docs/source/examples/media_group.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.566615 - =========== Media group =========== diff --git a/docs/source/examples/middleware_and_antiflood.rst b/docs/source/examples/middleware_and_antiflood.rst index 4f634c93..ce9e99db 100644 --- a/docs/source/examples/middleware_and_antiflood.rst +++ b/docs/source/examples/middleware_and_antiflood.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.560132 - ======================== Middleware and antiflood ======================== diff --git a/docs/source/examples/payments.rst b/docs/source/examples/payments.rst index 7b634ce7..f663c8ee 100644 --- a/docs/source/examples/payments.rst +++ b/docs/source/examples/payments.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.579017 - ======== Payments ======== diff --git a/docs/source/examples/proxy_and_emojize.rst b/docs/source/examples/proxy_and_emojize.rst index 3dbaa268..96a4f9a7 100644 --- a/docs/source/examples/proxy_and_emojize.rst +++ b/docs/source/examples/proxy_and_emojize.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.555359 - ================= Proxy and emojize ================= diff --git a/docs/source/examples/regexp_commands_filter_example.rst b/docs/source/examples/regexp_commands_filter_example.rst index 36452350..e524640a 100644 --- a/docs/source/examples/regexp_commands_filter_example.rst +++ b/docs/source/examples/regexp_commands_filter_example.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.568530 - ============================== Regexp commands filter example ============================== diff --git a/docs/source/examples/throtling_example.rst b/docs/source/examples/throtling_example.rst index f64eaccc..258d3d22 100644 --- a/docs/source/examples/throtling_example.rst +++ b/docs/source/examples/throtling_example.rst @@ -1,14 +1,8 @@ -.. Autogenerated file at 2018-09-08 02:07:37.563878 - -================= -Throtling example -================= - -Example for throttling manager. -You can use that for flood controlling. +================== +Throttling example +================== .. literalinclude:: ../../../examples/throttling_example.py :caption: throttling_example.py :language: python :linenos: - :lines: 7- diff --git a/docs/source/examples/webhook_example.rst b/docs/source/examples/webhook_example.rst index 48de8622..ce71339a 100644 --- a/docs/source/examples/webhook_example.rst +++ b/docs/source/examples/webhook_example.rst @@ -1,13 +1,8 @@ -.. Autogenerated file at 2018-10-28 19:31:48.341172 - =============== Webhook example =============== -Example outdated - .. literalinclude:: ../../../examples/webhook_example.py :caption: webhook_example.py :language: python :linenos: - :lines: 5- diff --git a/docs/source/examples/webhook_example_2.rst b/docs/source/examples/webhook_example_2.rst index 025264e4..8c104f12 100644 --- a/docs/source/examples/webhook_example_2.rst +++ b/docs/source/examples/webhook_example_2.rst @@ -1,5 +1,3 @@ -.. Autogenerated file at 2018-09-08 02:07:37.576034 - =================== Webhook example old =================== diff --git a/docs/source/index.rst b/docs/source/index.rst index 0ac6eccd..e08e8830 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.9-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.0-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/examples/local_server.py b/examples/local_server.py new file mode 100644 index 00000000..7dbc4aca --- /dev/null +++ b/examples/local_server.py @@ -0,0 +1,27 @@ +import logging + +from aiogram import Bot, Dispatcher, executor, types +from aiogram.bot.api import TelegramAPIServer +from aiogram.types import ContentType + +API_TOKEN = 'BOT TOKEN HERE' + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Create private Bot API server endpoints wrapper +local_server = TelegramAPIServer.from_base('http://localhost') + +# Initialize bot with using local server +bot = Bot(token=API_TOKEN, server=local_server) +# ... and dispatcher +dp = Dispatcher(bot) + + +@dp.message_handler(content_types=ContentType.ANY) +async def echo(message: types.Message): + await message.copy_to(message.chat.id) + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) diff --git a/requirements.txt b/requirements.txt index 7f7dc1ac..6f393257 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -aiohttp>=3.5.4,<4.0.0 -Babel>=2.6.0 -certifi>=2019.3.9 +aiohttp>=3.7.2,<4.0.0 +Babel>=2.8.0 +certifi>=2020.6.20 diff --git a/setup.py b/setup.py index b21b4e57..de9e8b4e 100755 --- a/setup.py +++ b/setup.py @@ -57,12 +57,13 @@ setup( 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Software Development :: Libraries :: Application Frameworks', ], install_requires=[ - 'aiohttp>=3.5.4,<4.0.0', - 'Babel>=2.6.0', - 'certifi>=2019.3.9', + 'aiohttp>=3.7.2,<4.0.0', + 'Babel>=2.8.0', + 'certifi>=2020.6.20', ], extras_require={ 'proxy': [ diff --git a/tests/test_bot.py b/tests/test_bot.py index cf1c3c3b..45d3a3fa 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -24,6 +24,22 @@ async def test_get_me(bot: Bot, event_loop): assert result == user +async def test_log_out(bot: Bot, event_loop): + """ logOut method test """ + + async with FakeTelegram(message_data=True, loop=event_loop): + result = await bot.log_out() + assert result is True + + +async def test_close_bot(bot: Bot, event_loop): + """ close method test """ + + async with FakeTelegram(message_data=True, loop=event_loop): + result = await bot.close_bot() + assert result is True + + async def test_send_message(bot: Bot, event_loop): """ sendMessage method test """ from .types.dataset import MESSAGE From 9a27b79d4067340dff68d755eb1613ca5c6ada2b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Nov 2020 17:54:22 +0200 Subject: [PATCH 035/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 1c66de7e..03d29bf7 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.10' +__version__ = '2.11' __api_version__ = '5.0' From b170bbc5f623f45e3a1c6283f4b517cd4893259d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Nov 2020 17:57:02 +0200 Subject: [PATCH 036/118] Fix tests --- tests/test_utils/test_text_decorations.py | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_utils/test_text_decorations.py b/tests/test_utils/test_text_decorations.py index dd0e595d..cc724dd7 100644 --- a/tests/test_utils/test_text_decorations.py +++ b/tests/test_utils/test_text_decorations.py @@ -10,7 +10,7 @@ class TestTextDecorations: MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), ] - ) == "hi *i'm bold _and italic_\r and still bold*" + ) == "hi *i'm bold _\rand italic_\r and still bold*" def test_unparse_entities_emoji_text(self): """ @@ -22,4 +22,4 @@ class TestTextDecorations: MessageEntity(offset=3, length=34, type=MessageEntityType.BOLD), MessageEntity(offset=12, length=10, type=MessageEntityType.ITALIC), ] - ) == "๐Ÿš€ *i'm bold _and italic_\r and still bold*" + ) == "๐Ÿš€ *i'm bold _\rand italic_\r and still bold*" diff --git a/tox.ini b/tox.ini index aff44213..063a9e9c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37 +envlist = py38 [testenv] deps = -rdev_requirements.txt From d0bfa8a13bb5dc006908be4df447b656d175503b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Nov 2020 18:58:06 +0200 Subject: [PATCH 037/118] Fix MessageEntity serialization for API calls #457 --- aiogram/bot/bot.py | 11 +++++++++ aiogram/types/message_entity.py | 41 +++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 0ac13702..8e7c1137 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -310,6 +310,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ reply_markup = prepare_arg(reply_markup) + entities = prepare_arg(entities) payload = generate_payload(**locals()) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) @@ -410,6 +411,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) + caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals()) if self.parse_mode: @@ -473,6 +475,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) + caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['photo']) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) @@ -557,6 +560,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) + caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['audio', 'thumb']) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) @@ -641,6 +645,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) + caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['document']) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) @@ -726,6 +731,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) + caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['video', 'thumb']) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) @@ -815,6 +821,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) + caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=["animation", "thumb"]) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) @@ -889,6 +896,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) + caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['voice']) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) @@ -1413,6 +1421,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`types.Message` """ options = prepare_arg(options) + explanation_entities = prepare_arg(explanation_entities) open_period = prepare_arg(open_period) close_date = prepare_arg(close_date) payload = generate_payload(**locals()) @@ -2199,6 +2208,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ reply_markup = prepare_arg(reply_markup) + entities = prepare_arg(entities) payload = generate_payload(**locals()) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) @@ -2250,6 +2260,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ reply_markup = prepare_arg(reply_markup) + caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals()) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 77b23c5c..58705265 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -1,10 +1,9 @@ import sys -from . import base -from . import fields -from .user import User from ..utils import helper, markdown from ..utils.deprecated import deprecated +from . import base, fields +from .user import User class MessageEntity(base.TelegramObject): @@ -13,6 +12,7 @@ class MessageEntity(base.TelegramObject): https://core.telegram.org/bots/api#messageentity """ + type: base.String = fields.Field() offset: base.Integer = fields.Field() length: base.Integer = fields.Field() @@ -20,6 +20,26 @@ class MessageEntity(base.TelegramObject): user: User = fields.Field(base=User) language: base.String = fields.Field() + def __init__( + self, + type: base.String, + offset: base.Integer, + length: base.Integer, + url: base.String = None, + user: User = None, + language: base.String = None, + **kwargs + ): + super().__init__( + type=type, + offset=offset, + length=length, + url=url, + user=user, + language=language, + **kwargs + ) + def get_text(self, text): """ Get value of entity @@ -27,18 +47,20 @@ class MessageEntity(base.TelegramObject): :param text: full text :return: part of text """ - if sys.maxunicode == 0xffff: - return text[self.offset:self.offset + self.length] + if sys.maxunicode == 0xFFFF: + return text[self.offset : self.offset + self.length] if not isinstance(text, bytes): - entity_text = text.encode('utf-16-le') + entity_text = text.encode("utf-16-le") else: entity_text = text - entity_text = entity_text[self.offset * 2:(self.offset + self.length) * 2] - return entity_text.decode('utf-16-le') + 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") + @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 @@ -95,6 +117,7 @@ class MessageEntityType(helper.Helper): :key: TEXT_LINK :key: TEXT_MENTION """ + mode = helper.HelperMode.snake_case MENTION = helper.Item() # mention - @username From c6a43c89c48dd6f2cc1860f926cb02eb8086f94a Mon Sep 17 00:00:00 2001 From: Daniil Kovalenko <40635760+WhiteMemory99@users.noreply.github.com> Date: Mon, 9 Nov 2020 01:35:09 +0700 Subject: [PATCH 038/118] Add missing parameter to promoteChatMember alias (#458) is_anonymous exists in the full method but is missing in its shortcut. --- aiogram/types/chat.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 0021eb23..1ff5dc75 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -282,7 +282,9 @@ class Chat(base.TelegramObject): can_send_other_messages=can_send_other_messages, can_add_web_page_previews=can_add_web_page_previews) - async def promote(self, user_id: base.Integer, + async def promote(self, + user_id: base.Integer, + is_anonymous: typing.Optional[base.Boolean] = None, can_change_info: typing.Optional[base.Boolean] = None, can_post_messages: typing.Optional[base.Boolean] = None, can_edit_messages: typing.Optional[base.Boolean] = None, @@ -300,29 +302,42 @@ class Chat(base.TelegramObject): :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` + + :param is_anonymous: Pass True, if the administrator's presence in the chat is hidden + :type is_anonymous: :obj:`typing.Optional[base.Boolean]` + :param can_change_info: Pass True, if the administrator can change chat title, photo and other settings :type can_change_info: :obj:`typing.Optional[base.Boolean]` + :param can_post_messages: Pass True, if the administrator can create channel posts, channels only :type can_post_messages: :obj:`typing.Optional[base.Boolean]` + :param can_edit_messages: Pass True, if the administrator can edit messages of other users, channels only :type can_edit_messages: :obj:`typing.Optional[base.Boolean]` + :param can_delete_messages: Pass True, if the administrator can delete messages of other users :type can_delete_messages: :obj:`typing.Optional[base.Boolean]` + :param can_invite_users: Pass True, if the administrator can invite new users to the chat :type can_invite_users: :obj:`typing.Optional[base.Boolean]` + :param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members :type can_restrict_members: :obj:`typing.Optional[base.Boolean]` + :param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only :type can_pin_messages: :obj:`typing.Optional[base.Boolean]` + :param can_promote_members: Pass True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) :type can_promote_members: :obj:`typing.Optional[base.Boolean]` + :return: Returns True on success. :rtype: :obj:`base.Boolean` """ return await self.bot.promote_chat_member(self.id, user_id=user_id, + is_anonymous=is_anonymous, can_change_info=can_change_info, can_post_messages=can_post_messages, can_edit_messages=can_edit_messages, From 44c1cc1f61d1badaf5d589788ef995457095652f Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 9 Nov 2020 00:59:16 +0300 Subject: [PATCH 039/118] Add parameter supports_streaming to reply_video, remove redundant docstrings (#459) --- aiogram/types/message.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 900b731c..ab56f363 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1033,10 +1033,6 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#sendvenue - :param chat_id: Unique identifier for the target chat or username of the - target channel (in the format @channelusername) - :type chat_id: :obj:`typing.Union[base.Integer, base.String]` - :param latitude: Latitude of the venue :type latitude: :obj:`base.Float` @@ -1345,10 +1341,6 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#senddice - :param chat_id: Unique identifier for the target chat or username of the - target channel (in the format @channelusername) - :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 โ€œ๐ŸŽฐโ€. Dice can have values 1-6 for โ€œ๐ŸŽฒโ€ and โ€œ๐ŸŽฏโ€, values 1-5 for โ€œ๐Ÿ€โ€ and โ€œโšฝโ€, and values 1-64 for โ€œ๐ŸŽฐโ€. @@ -1790,6 +1782,7 @@ class Message(base.TelegramObject): caption: typing.Optional[base.String] = None, parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + supports_streaming: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ @@ -1834,6 +1827,9 @@ class Message(base.TelegramObject): which can be specified instead of parse_mode :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param supports_streaming: Pass True, if the uploaded video is suitable for streaming + :type supports_streaming: :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[base.Boolean]` @@ -1862,6 +1858,7 @@ class Message(base.TelegramObject): caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + supports_streaming=supports_streaming, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, @@ -2122,10 +2119,6 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#sendvenue - :param chat_id: Unique identifier for the target chat or username of the - target channel (in the format @channelusername) - :type chat_id: :obj:`typing.Union[base.Integer, base.String]` - :param latitude: Latitude of the venue :type latitude: :obj:`base.Float` @@ -2434,10 +2427,6 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#senddice - :param chat_id: Unique identifier for the target chat or username of the - target channel (in the format @channelusername) - :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 โ€œ๐ŸŽฐโ€. Dice can have values 1-6 for โ€œ๐ŸŽฒโ€ and โ€œ๐ŸŽฏโ€, values 1-5 for โ€œ๐Ÿ€โ€ and โ€œโšฝโ€, and values 1-64 for โ€œ๐ŸŽฐโ€. From 7f448b4d8e90f1da7f5f3a85ae40244b6ed7f180 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 9 Nov 2020 11:39:19 +0200 Subject: [PATCH 040/118] Fix files URL template --- 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 03c7e209..9b86e7ca 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -47,7 +47,7 @@ class TelegramAPIServer: base = base.rstrip("/") return cls( base=f"{base}/bot{{token}}/{{method}}", - file=f"{base}/file/bot{{token}}/{{method}}", + file=f"{base}/file/bot{{token}}/{{path}}", ) From 89e52ae18b36d182b15d4b7a864fa2d8de093460 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Mon, 9 Nov 2020 16:54:23 -0500 Subject: [PATCH 041/118] when entities are set, default parse_mode become disabled (#461) * when entities are set, default parse_mode become disabled * check entities is not None --- aiogram/bot/bot.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 8e7c1137..390548b5 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -312,7 +312,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) entities = prepare_arg(entities) payload = generate_payload(**locals()) - if self.parse_mode: + if self.parse_mode and entities is not None: payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.SEND_MESSAGE, payload) @@ -414,7 +414,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals()) - if self.parse_mode: + if self.parse_mode and caption_entities is not None: payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.COPY_MESSAGE, payload) @@ -477,7 +477,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['photo']) - if self.parse_mode: + if self.parse_mode and caption_entities is not None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -562,7 +562,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['audio', 'thumb']) - if self.parse_mode: + if self.parse_mode and caption_entities is not None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -647,7 +647,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['document']) - if self.parse_mode: + if self.parse_mode and caption_entities is not None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -733,7 +733,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['video', 'thumb']) - if self.parse_mode: + if self.parse_mode and caption_entities is not None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -823,7 +823,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=["animation", "thumb"]) - if self.parse_mode: + if self.parse_mode and caption_entities is not None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -898,7 +898,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['voice']) - if self.parse_mode: + if self.parse_mode and caption_entities is not None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -1425,7 +1425,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): open_period = prepare_arg(open_period) close_date = prepare_arg(close_date) payload = generate_payload(**locals()) - if self.parse_mode: + if self.parse_mode and explanation_entities is not None: payload.setdefault('explanation_parse_mode', self.parse_mode) result = await self.request(api.Methods.SEND_POLL, payload) @@ -2210,7 +2210,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) entities = prepare_arg(entities) payload = generate_payload(**locals()) - if self.parse_mode: + if self.parse_mode and entities is not None: payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.EDIT_MESSAGE_TEXT, payload) @@ -2262,7 +2262,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals()) - if self.parse_mode: + if self.parse_mode and caption_entities is not None: payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.EDIT_MESSAGE_CAPTION, payload) From 0158f99deffcff4cd499b1d845218f8edb544a2b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 10 Nov 2020 00:40:05 +0200 Subject: [PATCH 042/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 03d29bf7..12aaed58 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.11' +__version__ = '2.11.1' __api_version__ = '5.0' From 9c2c652df5a695c9113c15c630764b6c13e82539 Mon Sep 17 00:00:00 2001 From: Groosha Date: Tue, 10 Nov 2020 02:03:06 +0300 Subject: [PATCH 043/118] Added missing "supports_streaming" argument to answer_video method (#462) --- aiogram/types/message.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index ab56f363..a260e62d 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -689,6 +689,7 @@ class Message(base.TelegramObject): caption: typing.Optional[base.String] = None, parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + supports_streaming: typing.Optional[base.Boolean] = None, disable_notification: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ @@ -733,6 +734,9 @@ class Message(base.TelegramObject): which can be specified instead of parse_mode :type caption_entities: :obj:`typing.Optional[typing.List[MessageEntity]]` + :param supports_streaming: Pass True, if the uploaded video is suitable for streaming + :type supports_streaming: :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[base.Boolean]` @@ -761,6 +765,7 @@ class Message(base.TelegramObject): caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + supports_streaming=supports_streaming, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, From f18f538b6f1aeace849b6dd48d9cb2c8283bdcc7 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 10 Nov 2020 09:57:41 +0200 Subject: [PATCH 044/118] Hotfix: Default parse mode --- aiogram/__init__.py | 2 +- aiogram/bot/bot.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 12aaed58..a266ce1b 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.11.1' +__version__ = '2.11.2' __api_version__ = '5.0' diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 390548b5..ab244252 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -312,7 +312,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) entities = prepare_arg(entities) payload = generate_payload(**locals()) - if self.parse_mode and entities is not None: + if self.parse_mode and entities is None: payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.SEND_MESSAGE, payload) @@ -414,7 +414,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals()) - if self.parse_mode and caption_entities is not None: + if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.COPY_MESSAGE, payload) @@ -477,7 +477,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['photo']) - if self.parse_mode and caption_entities is not None: + if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -562,7 +562,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['audio', 'thumb']) - if self.parse_mode and caption_entities is not None: + if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -647,7 +647,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['document']) - if self.parse_mode and caption_entities is not None: + if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -733,7 +733,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['video', 'thumb']) - if self.parse_mode and caption_entities is not None: + if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -823,7 +823,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=["animation", "thumb"]) - if self.parse_mode and caption_entities is not None: + if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -898,7 +898,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals(), exclude=['voice']) - if self.parse_mode and caption_entities is not None: + if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) files = {} @@ -1425,7 +1425,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): open_period = prepare_arg(open_period) close_date = prepare_arg(close_date) payload = generate_payload(**locals()) - if self.parse_mode and explanation_entities is not None: + if self.parse_mode and explanation_entities is None: payload.setdefault('explanation_parse_mode', self.parse_mode) result = await self.request(api.Methods.SEND_POLL, payload) @@ -2210,7 +2210,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) entities = prepare_arg(entities) payload = generate_payload(**locals()) - if self.parse_mode and entities is not None: + if self.parse_mode and entities is None: payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.EDIT_MESSAGE_TEXT, payload) @@ -2262,7 +2262,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): reply_markup = prepare_arg(reply_markup) caption_entities = prepare_arg(caption_entities) payload = generate_payload(**locals()) - if self.parse_mode and caption_entities is not None: + if self.parse_mode and caption_entities is None: payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.EDIT_MESSAGE_CAPTION, payload) From ee3b0954a31125ddc43de2eee2c41bed184e0827 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Thu, 12 Nov 2020 15:02:52 -0500 Subject: [PATCH 045/118] Telegram API readme updated (#463) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cc22e92..3c43b881 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.9-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.0-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 ae7b250e3349bb7cc9aec7729aed6ec2186346e6 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Fri, 13 Nov 2020 14:58:00 -0500 Subject: [PATCH 046/118] fixed: Cannot send playlist of audio files (#465) * fixed: Cannot send playlist of audio files #464 * #463 removed document and audio from check * #463 style fix --- aiogram/types/input_media.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 943a534c..e60913ff 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -270,10 +270,10 @@ class MediaGroup(base.TelegramObject): media = InputMediaPhoto(**media) elif media_type == 'video': media = InputMediaVideo(**media) - # elif media_type == 'document': - # media = InputMediaDocument(**media) - # elif media_type == 'audio': - # media = InputMediaAudio(**media) + elif media_type == 'document': + media = InputMediaDocument(**media) + elif media_type == 'audio': + media = InputMediaAudio(**media) # elif media_type == 'animation': # media = InputMediaAnimation(**media) else: @@ -282,8 +282,8 @@ class MediaGroup(base.TelegramObject): elif not isinstance(media, InputMedia): raise TypeError(f"Media must be an instance of InputMedia or dict, not {type(media).__name__}") - elif media.type in ('document', 'audio', 'animation'): - raise ValueError(f"This type of media is not supported by media groups!") + elif media.type == 'animation': + raise ValueError("This type of media is not supported by media groups!") self.media.append(media) From 2cf6b22a693d0bc11303c3fc28e5be43deb92b3c Mon Sep 17 00:00:00 2001 From: Forden Date: Sun, 15 Nov 2020 21:19:08 +0300 Subject: [PATCH 047/118] Update message.py (#466) Added DICE to ContentTypes --- aiogram/types/message.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index a260e62d..4b5a47bc 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -2901,6 +2901,8 @@ class ContentType(helper.Helper): :key: CONTACT :key: LOCATION :key: VENUE + :key: POLL + :key: DICE :key: NEW_CHAT_MEMBERS :key: LEFT_CHAT_MEMBER :key: INVOICE @@ -2966,6 +2968,8 @@ class ContentTypes(helper.Helper): :key: CONTACT :key: LOCATION :key: VENUE + :key: POLL + :key: DICE :key: NEW_CHAT_MEMBERS :key: LEFT_CHAT_MEMBER :key: INVOICE @@ -2992,6 +2996,8 @@ class ContentTypes(helper.Helper): CONTACT = helper.ListItem() # contact LOCATION = helper.ListItem() # location VENUE = helper.ListItem() # venue + POLL = helper.ListItem() # poll + DICE = helper.ListItem() # dice NEW_CHAT_MEMBERS = helper.ListItem() # new_chat_member LEFT_CHAT_MEMBER = helper.ListItem() # left_chat_member INVOICE = helper.ListItem() # invoice @@ -3005,7 +3011,6 @@ class ContentTypes(helper.Helper): DELETE_CHAT_PHOTO = helper.ListItem() # delete_chat_photo GROUP_CHAT_CREATED = helper.ListItem() # group_chat_created PASSPORT_DATA = helper.ListItem() # passport_data - POLL = helper.ListItem() UNKNOWN = helper.ListItem() # unknown ANY = helper.ListItem() # any From b3103183d735576f5fd684c1bf6d16eec179f14c Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 17 Nov 2020 19:06:59 -0500 Subject: [PATCH 048/118] #467 Uncommented `attach_document` and `attach_audio` MediaGroup methods (#468) --- aiogram/types/input_media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index e60913ff..2d325d89 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -309,6 +309,7 @@ class MediaGroup(base.TelegramObject): width=width, height=height, duration=duration, parse_mode=parse_mode) self.attach(animation) + ''' def attach_audio(self, audio: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, @@ -351,7 +352,6 @@ class MediaGroup(base.TelegramObject): if not isinstance(document, InputMedia): document = InputMediaDocument(media=document, thumb=thumb, caption=caption, parse_mode=parse_mode) self.attach(document) - ''' def attach_photo(self, photo: typing.Union[InputMediaPhoto, base.InputFile], caption: base.String = None): From ca45b248a895c526927201ff812684cec3b4b429 Mon Sep 17 00:00:00 2001 From: SvineruS <37367870+SvineruS@users.noreply.github.com> Date: Sat, 5 Dec 2020 15:28:34 +0200 Subject: [PATCH 049/118] Fixed optionality of some properties, add vcard argument to InputContactMessageContent constructor (#473) --- aiogram/types/input_message_content.py | 55 ++++++++++++++++---------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/aiogram/types/input_message_content.py b/aiogram/types/input_message_content.py index 522d44c5..0008a2ee 100644 --- a/aiogram/types/input_message_content.py +++ b/aiogram/types/input_message_content.py @@ -27,14 +27,21 @@ class InputContactMessageContent(InputMessageContent): """ phone_number: base.String = fields.Field() first_name: base.String = fields.Field() - last_name: base.String = fields.Field() - vcard: base.String = fields.Field() + last_name: typing.Optional[base.String] = fields.Field() + vcard: typing.Optional[base.String] = fields.Field() - def __init__(self, phone_number: base.String, - first_name: typing.Optional[base.String] = None, - last_name: typing.Optional[base.String] = None): - super(InputContactMessageContent, self).__init__(phone_number=phone_number, first_name=first_name, - last_name=last_name) + def __init__(self, + phone_number: base.String, + first_name: base.String = None, + last_name: typing.Optional[base.String] = None, + vcard: typing.Optional[base.String] = None, + ): + super().__init__( + phone_number=phone_number, + first_name=first_name, + last_name=last_name, + vcard=vcard + ) class InputLocationMessageContent(InputMessageContent): @@ -75,7 +82,8 @@ class InputTextMessageContent(InputMessageContent): https://core.telegram.org/bots/api#inputtextmessagecontent """ message_text: base.String = fields.Field() - parse_mode: base.String = fields.Field() + parse_mode: typing.Optional[base.String] = fields.Field() + caption_entities: typing.Optional[typing.List[MessageEntity]] = fields.Field() disable_web_page_preview: base.Boolean = fields.Field() def safe_get_parse_mode(self): @@ -86,7 +94,7 @@ class InputTextMessageContent(InputMessageContent): def __init__( self, - message_text: typing.Optional[base.String] = None, + message_text: base.String, parse_mode: typing.Optional[base.String] = None, caption_entities: typing.Optional[typing.List[MessageEntity]] = None, disable_web_page_preview: typing.Optional[base.Boolean] = None, @@ -95,7 +103,8 @@ class InputTextMessageContent(InputMessageContent): parse_mode = self.safe_get_parse_mode() super().__init__( - message_text=message_text, parse_mode=parse_mode, + message_text=message_text, + parse_mode=parse_mode, caption_entities=caption_entities, disable_web_page_preview=disable_web_page_preview, ) @@ -114,25 +123,29 @@ class InputVenueMessageContent(InputMessageContent): longitude: base.Float = fields.Field() title: base.String = fields.Field() address: base.String = fields.Field() - foursquare_id: base.String = fields.Field() - foursquare_type: base.String = fields.Field() - google_place_id: base.String = fields.Field() - google_place_type: base.String = fields.Field() + foursquare_id: typing.Optional[base.String] = fields.Field() + foursquare_type: typing.Optional[base.String] = fields.Field() + google_place_id: typing.Optional[base.String] = fields.Field() + google_place_type: typing.Optional[base.String] = fields.Field() def __init__( self, - latitude: typing.Optional[base.Float] = None, - longitude: typing.Optional[base.Float] = None, - title: typing.Optional[base.String] = None, - address: typing.Optional[base.String] = None, + latitude: base.Float, + longitude: base.Float, + title: base.String, + address: base.String, foursquare_id: typing.Optional[base.String] = None, foursquare_type: typing.Optional[base.String] = None, google_place_id: typing.Optional[base.String] = None, google_place_type: typing.Optional[base.String] = None, ): super().__init__( - latitude=latitude, longitude=longitude, title=title, - address=address, foursquare_id=foursquare_id, - foursquare_type=foursquare_type, google_place_id=google_place_id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + foursquare_type=foursquare_type, + google_place_id=google_place_id, google_place_type=google_place_type, ) From 3440ab3c96e31384cbcf515e75904dcade6f5fd0 Mon Sep 17 00:00:00 2001 From: OGURCHINSKIY <41481736+OGURCHINSKIY@users.noreply.github.com> Date: Sat, 5 Dec 2020 16:28:56 +0300 Subject: [PATCH 050/118] Add exception MessageIdInvalid (#474) --- aiogram/utils/exceptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index cc1885dd..d38ff009 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -6,6 +6,7 @@ - MessageError - MessageNotModified - MessageToForwardNotFound + - MessageIdInvalid - MessageToDeleteNotFound - MessageToPinNotFound - MessageIdentifierNotSpecified @@ -178,6 +179,11 @@ class MessageToForwardNotFound(MessageError): match = 'message to forward not found' +class MessageIdInvalid(MessageError): + text = 'Invalid message id' + match = 'message_id_invalid' + + class MessageToDeleteNotFound(MessageError): """ Will be raised when you try to delete very old or deleted or unknown message. From d7b1bd2daecd6ec82438ac2ef4b001e642c91b56 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Sun, 10 Jan 2021 12:49:48 -0600 Subject: [PATCH 051/118] Correct a typo in executor.py (#489) --- aiogram/utils/executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 1b5b0b0b..35107975 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -155,7 +155,7 @@ class Executor: def set_web_app(self, application: web.Application): """ - Change instance of aiohttp.web.Applicaton + Change instance of aiohttp.web.Application :param application: """ From 4fb4d6cac1253b677709d4b1f35312f4df43b825 Mon Sep 17 00:00:00 2001 From: Ibraheem Asaad Date: Sun, 10 Jan 2021 20:52:50 +0200 Subject: [PATCH 052/118] fix return value of FSMContextProxy's setdefault (#405) (#491) --- aiogram/dispatcher/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index 9a4eac76..74492361 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -397,7 +397,7 @@ class FSMContextProxy: def setdefault(self, key, default): self._check_closed() - self._data.setdefault(key, default) + return self._data.setdefault(key, default) def update(self, data=None, **kwargs): self._check_closed() From ee12911f240175d216ce33c78012994a34fe2e25 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 10 Jan 2021 21:53:59 +0300 Subject: [PATCH 053/118] pytest update; yield_fixture deprecation fix; event_loop removed (#479) --- dev_requirements.txt | 3 +- tests/test_bot.py | 190 +++++++++++++++++++-------------------- tests/test_dispatcher.py | 7 +- tests/test_message.py | 12 +-- 4 files changed, 105 insertions(+), 107 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 0252e7e1..ef5272af 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -3,11 +3,10 @@ ujson>=1.35 python-rapidjson>=0.7.0 emoji>=0.5.2 -pytest>=5.4 +pytest>=6.2.1 pytest-asyncio>=0.10.0 tox>=3.9.0 aresponses>=1.1.1 -uvloop>=0.12.2 aioredis>=1.2.0 wheel>=0.31.1 sphinx>=2.0.1 diff --git a/tests/test_bot.py b/tests/test_bot.py index 45d3a3fa..224666ec 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -6,103 +6,103 @@ from . import FakeTelegram, TOKEN, BOT_ID pytestmark = pytest.mark.asyncio -@pytest.yield_fixture(name='bot') -async def bot_fixture(event_loop): +@pytest.fixture(name='bot') +async def bot_fixture(): """ Bot fixture """ - _bot = Bot(TOKEN, loop=event_loop, parse_mode=types.ParseMode.MARKDOWN) + _bot = Bot(TOKEN, parse_mode=types.ParseMode.MARKDOWN_V2) yield _bot await _bot.close() -async def test_get_me(bot: Bot, event_loop): +async def test_get_me(bot: Bot): """ getMe method test """ from .types.dataset import USER user = types.User(**USER) - async with FakeTelegram(message_data=USER, loop=event_loop): + async with FakeTelegram(message_data=USER): result = await bot.me assert result == user -async def test_log_out(bot: Bot, event_loop): +async def test_log_out(bot: Bot): """ logOut method test """ - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.log_out() assert result is True -async def test_close_bot(bot: Bot, event_loop): +async def test_close_bot(bot: Bot): """ close method test """ - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.close_bot() assert result is True -async def test_send_message(bot: Bot, event_loop): +async def test_send_message(bot: Bot): """ sendMessage method test """ from .types.dataset import MESSAGE msg = types.Message(**MESSAGE) - async with FakeTelegram(message_data=MESSAGE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE): result = await bot.send_message(chat_id=msg.chat.id, text=msg.text) assert result == msg -async def test_forward_message(bot: Bot, event_loop): +async def test_forward_message(bot: Bot): """ forwardMessage method test """ from .types.dataset import FORWARDED_MESSAGE msg = types.Message(**FORWARDED_MESSAGE) - async with FakeTelegram(message_data=FORWARDED_MESSAGE, loop=event_loop): + async with FakeTelegram(message_data=FORWARDED_MESSAGE): 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 -async def test_send_photo(bot: Bot, event_loop): +async def test_send_photo(bot: Bot): """ sendPhoto method test with file_id """ from .types.dataset import MESSAGE_WITH_PHOTO, PHOTO msg = types.Message(**MESSAGE_WITH_PHOTO) photo = types.PhotoSize(**PHOTO) - async with FakeTelegram(message_data=MESSAGE_WITH_PHOTO, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_PHOTO): 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 -async def test_send_audio(bot: Bot, event_loop): +async def test_send_audio(bot: Bot): """ sendAudio method test with file_id """ from .types.dataset import MESSAGE_WITH_AUDIO msg = types.Message(**MESSAGE_WITH_AUDIO) - async with FakeTelegram(message_data=MESSAGE_WITH_AUDIO, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_AUDIO): 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) assert result == msg -async def test_send_document(bot: Bot, event_loop): +async def test_send_document(bot: Bot): """ sendDocument method test with file_id """ from .types.dataset import MESSAGE_WITH_DOCUMENT msg = types.Message(**MESSAGE_WITH_DOCUMENT) - async with FakeTelegram(message_data=MESSAGE_WITH_DOCUMENT, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_DOCUMENT): 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 -async def test_send_video(bot: Bot, event_loop): +async def test_send_video(bot: Bot): """ sendVideo method test with file_id """ from .types.dataset import MESSAGE_WITH_VIDEO, VIDEO msg = types.Message(**MESSAGE_WITH_VIDEO) video = types.Video(**VIDEO) - async with FakeTelegram(message_data=MESSAGE_WITH_VIDEO, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_VIDEO): 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, @@ -110,204 +110,204 @@ async def test_send_video(bot: Bot, event_loop): assert result == msg -async def test_send_voice(bot: Bot, event_loop): +async def test_send_voice(bot: Bot): """ sendVoice method test with file_id """ from .types.dataset import MESSAGE_WITH_VOICE, VOICE msg = types.Message(**MESSAGE_WITH_VOICE) voice = types.Voice(**VOICE) - async with FakeTelegram(message_data=MESSAGE_WITH_VOICE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_VOICE): 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) assert result == msg -async def test_send_video_note(bot: Bot, event_loop): +async def test_send_video_note(bot: Bot): """ sendVideoNote method test with file_id """ from .types.dataset import MESSAGE_WITH_VIDEO_NOTE, VIDEO_NOTE msg = types.Message(**MESSAGE_WITH_VIDEO_NOTE) video_note = types.VideoNote(**VIDEO_NOTE) - async with FakeTelegram(message_data=MESSAGE_WITH_VIDEO_NOTE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_VIDEO_NOTE): 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) assert result == msg -async def test_send_media_group(bot: Bot, event_loop): +async def test_send_media_group(bot: Bot): """ sendMediaGroup method test with file_id """ from .types.dataset import MESSAGE_WITH_MEDIA_GROUP, PHOTO msg = types.Message(**MESSAGE_WITH_MEDIA_GROUP) photo = types.PhotoSize(**PHOTO) media = [types.InputMediaPhoto(media=photo.file_id), types.InputMediaPhoto(media=photo.file_id)] - async with FakeTelegram(message_data=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP], loop=event_loop): + async with FakeTelegram(message_data=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP]): 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 -async def test_send_location(bot: Bot, event_loop): +async def test_send_location(bot: Bot): """ sendLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) location = types.Location(**LOCATION) - async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION): result = await bot.send_location(msg.chat.id, latitude=location.latitude, longitude=location.longitude, live_period=10, disable_notification=False) assert result == msg -async def test_edit_message_live_location_by_bot(bot: Bot, event_loop): +async def test_edit_message_live_location_by_bot(bot: Bot): """ editMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) location = types.Location(**LOCATION) # editing bot message - async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION): 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 -async def test_edit_message_live_location_by_user(bot: Bot, event_loop): +async def test_edit_message_live_location_by_user(bot: Bot): """ editMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) location = types.Location(**LOCATION) # editing user's message - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): 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 -async def test_stop_message_live_location_by_bot(bot: Bot, event_loop): +async def test_stop_message_live_location_by_bot(bot: Bot): """ stopMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) # stopping bot message - async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_LOCATION): result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id) assert result == msg -async def test_stop_message_live_location_by_user(bot: Bot, event_loop): +async def test_stop_message_live_location_by_user(bot: Bot): """ stopMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) # stopping user's message - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): 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 -async def test_send_venue(bot: Bot, event_loop): +async def test_send_venue(bot: Bot): """ sendVenue method test """ from .types.dataset import MESSAGE_WITH_VENUE, VENUE, LOCATION msg = types.Message(**MESSAGE_WITH_VENUE) location = types.Location(**LOCATION) venue = types.Venue(**VENUE) - async with FakeTelegram(message_data=MESSAGE_WITH_VENUE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_VENUE): 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) assert result == msg -async def test_send_contact(bot: Bot, event_loop): +async def test_send_contact(bot: Bot): """ sendContact method test """ from .types.dataset import MESSAGE_WITH_CONTACT, CONTACT msg = types.Message(**MESSAGE_WITH_CONTACT) contact = types.Contact(**CONTACT) - async with FakeTelegram(message_data=MESSAGE_WITH_CONTACT, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_CONTACT): 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 -async def test_send_dice(bot: Bot, event_loop): +async def test_send_dice(bot: Bot): """ sendDice method test """ from .types.dataset import MESSAGE_WITH_DICE msg = types.Message(**MESSAGE_WITH_DICE) - async with FakeTelegram(message_data=MESSAGE_WITH_DICE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE_WITH_DICE): result = await bot.send_dice(msg.chat.id, disable_notification=False) assert result == msg -async def test_send_chat_action(bot: Bot, event_loop): +async def test_send_chat_action(bot: Bot): """ sendChatAction method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.send_chat_action(chat_id=chat.id, action=types.ChatActions.TYPING) assert isinstance(result, bool) assert result is True -async def test_get_user_profile_photo(bot: Bot, event_loop): +async def test_get_user_profile_photo(bot: Bot): """ getUserProfilePhotos method test """ from .types.dataset import USER_PROFILE_PHOTOS, USER user = types.User(**USER) - async with FakeTelegram(message_data=USER_PROFILE_PHOTOS, loop=event_loop): + async with FakeTelegram(message_data=USER_PROFILE_PHOTOS): result = await bot.get_user_profile_photos(user_id=user.id, offset=1, limit=1) assert isinstance(result, types.UserProfilePhotos) -async def test_get_file(bot: Bot, event_loop): +async def test_get_file(bot: Bot): """ getFile method test """ from .types.dataset import FILE file = types.File(**FILE) - async with FakeTelegram(message_data=FILE, loop=event_loop): + async with FakeTelegram(message_data=FILE): result = await bot.get_file(file_id=file.file_id) assert isinstance(result, types.File) -async def test_kick_chat_member(bot: Bot, event_loop): +async def test_kick_chat_member(bot: Bot): """ kickChatMember method test """ from .types.dataset import USER, CHAT user = types.User(**USER) chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): 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 -async def test_unban_chat_member(bot: Bot, event_loop): +async def test_unban_chat_member(bot: Bot): """ unbanChatMember method test """ from .types.dataset import USER, CHAT user = types.User(**USER) chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.unban_chat_member(chat_id=chat.id, user_id=user.id) assert isinstance(result, bool) assert result is True -async def test_restrict_chat_member(bot: Bot, event_loop): +async def test_restrict_chat_member(bot: Bot): """ restrictChatMember method test """ from .types.dataset import USER, CHAT user = types.User(**USER) chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.restrict_chat_member( chat_id=chat.id, user_id=user.id, @@ -321,13 +321,13 @@ async def test_restrict_chat_member(bot: Bot, event_loop): assert result is True -async def test_promote_chat_member(bot: Bot, event_loop): +async def test_promote_chat_member(bot: Bot): """ promoteChatMember method test """ from .types.dataset import USER, CHAT user = types.User(**USER) chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): 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, @@ -336,208 +336,208 @@ async def test_promote_chat_member(bot: Bot, event_loop): assert result is True -async def test_export_chat_invite_link(bot: Bot, event_loop): +async def test_export_chat_invite_link(bot: Bot): """ exportChatInviteLink method test """ from .types.dataset import CHAT, INVITE_LINK chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=INVITE_LINK, loop=event_loop): + async with FakeTelegram(message_data=INVITE_LINK): result = await bot.export_chat_invite_link(chat_id=chat.id) assert result == INVITE_LINK -async def test_delete_chat_photo(bot: Bot, event_loop): +async def test_delete_chat_photo(bot: Bot): """ deleteChatPhoto method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.delete_chat_photo(chat_id=chat.id) assert isinstance(result, bool) assert result is True -async def test_set_chat_title(bot: Bot, event_loop): +async def test_set_chat_title(bot: Bot): """ setChatTitle method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.set_chat_title(chat_id=chat.id, title='Test title') assert isinstance(result, bool) assert result is True -async def test_set_chat_description(bot: Bot, event_loop): +async def test_set_chat_description(bot: Bot): """ setChatDescription method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.set_chat_description(chat_id=chat.id, description='Test description') assert isinstance(result, bool) assert result is True -async def test_pin_chat_message(bot: Bot, event_loop): +async def test_pin_chat_message(bot: Bot): """ pinChatMessage method test """ from .types.dataset import MESSAGE message = types.Message(**MESSAGE) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.pin_chat_message(chat_id=message.chat.id, message_id=message.message_id, disable_notification=False) assert isinstance(result, bool) assert result is True -async def test_unpin_chat_message(bot: Bot, event_loop): +async def test_unpin_chat_message(bot: Bot): """ unpinChatMessage method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.unpin_chat_message(chat_id=chat.id) assert isinstance(result, bool) assert result is True -async def test_leave_chat(bot: Bot, event_loop): +async def test_leave_chat(bot: Bot): """ leaveChat method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.leave_chat(chat_id=chat.id) assert isinstance(result, bool) assert result is True -async def test_get_chat(bot: Bot, event_loop): +async def test_get_chat(bot: Bot): """ getChat method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=CHAT, loop=event_loop): + async with FakeTelegram(message_data=CHAT): result = await bot.get_chat(chat_id=chat.id) assert result == chat -async def test_get_chat_administrators(bot: Bot, event_loop): +async def test_get_chat_administrators(bot: Bot): """ getChatAdministrators method test """ from .types.dataset import CHAT, CHAT_MEMBER chat = types.Chat(**CHAT) member = types.ChatMember(**CHAT_MEMBER) - async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER], loop=event_loop): + async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER]): result = await bot.get_chat_administrators(chat_id=chat.id) assert result[0] == member assert len(result) == 2 -async def test_get_chat_members_count(bot: Bot, event_loop): +async def test_get_chat_members_count(bot: Bot): """ getChatMembersCount method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) count = 5 - async with FakeTelegram(message_data=count, loop=event_loop): + async with FakeTelegram(message_data=count): result = await bot.get_chat_members_count(chat_id=chat.id) assert result == count -async def test_get_chat_member(bot: Bot, event_loop): +async def test_get_chat_member(bot: Bot): """ getChatMember method test """ from .types.dataset import CHAT, CHAT_MEMBER chat = types.Chat(**CHAT) member = types.ChatMember(**CHAT_MEMBER) - async with FakeTelegram(message_data=CHAT_MEMBER, loop=event_loop): + async with FakeTelegram(message_data=CHAT_MEMBER): result = await bot.get_chat_member(chat_id=chat.id, user_id=member.user.id) assert isinstance(result, types.ChatMember) assert result == member -async def test_set_chat_sticker_set(bot: Bot, event_loop): +async def test_set_chat_sticker_set(bot: Bot): """ setChatStickerSet method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.set_chat_sticker_set(chat_id=chat.id, sticker_set_name='aiogram_stickers') assert isinstance(result, bool) assert result is True -async def test_delete_chat_sticker_set(bot: Bot, event_loop): +async def test_delete_chat_sticker_set(bot: Bot): """ setChatStickerSet method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.delete_chat_sticker_set(chat_id=chat.id) assert isinstance(result, bool) assert result is True -async def test_answer_callback_query(bot: Bot, event_loop): +async def test_answer_callback_query(bot: Bot): """ answerCallbackQuery method test """ - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.answer_callback_query(callback_query_id='QuERyId', text='Test Answer') assert isinstance(result, bool) assert result is True -async def test_set_my_commands(bot: Bot, event_loop): +async def test_set_my_commands(bot: Bot): """ setMyCommands method test """ from .types.dataset import BOT_COMMAND - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): 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_get_my_commands(bot: Bot, event_loop): +async def test_get_my_commands(bot: Bot): """ 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): + async with FakeTelegram(message_data=commands): 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): +async def test_edit_message_text_by_bot(bot: Bot): """ editMessageText method test """ from .types.dataset import EDITED_MESSAGE msg = types.Message(**EDITED_MESSAGE) # message by bot - async with FakeTelegram(message_data=EDITED_MESSAGE, loop=event_loop): + async with FakeTelegram(message_data=EDITED_MESSAGE): result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id) assert result == msg -async def test_edit_message_text_by_user(bot: Bot, event_loop): +async def test_edit_message_text_by_user(bot: Bot): """ editMessageText method test """ from .types.dataset import EDITED_MESSAGE msg = types.Message(**EDITED_MESSAGE) # message by user - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): 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): +async def test_set_sticker_set_thumb(bot: Bot): """ setStickerSetThumb method test """ - async with FakeTelegram(message_data=True, loop=event_loop): + async with FakeTelegram(message_data=True): result = await bot.set_sticker_set_thumb(name='test', user_id=123456789, thumb='file_id') assert isinstance(result, bool) assert result is True diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 6ebaf472..81ae565c 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -5,11 +5,10 @@ from aiogram import Dispatcher, Bot pytestmark = pytest.mark.asyncio -@pytest.yield_fixture() -async def bot(event_loop): +@pytest.fixture(name='bot') +async def bot_fixture(): """ Bot fixture """ - _bot = Bot(token='123456789:AABBCCDDEEFFaabbccddeeff-1234567890', - loop=event_loop) + _bot = Bot(token='123456789:AABBCCDDEEFFaabbccddeeff-1234567890') yield _bot await _bot.close() diff --git a/tests/test_message.py b/tests/test_message.py index 32168d57..6fca789f 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -8,16 +8,16 @@ from . import FakeTelegram, TOKEN pytestmark = pytest.mark.asyncio -@pytest.yield_fixture() -async def bot(event_loop): +@pytest.fixture(name='bot') +async def bot_fixture(): """ Bot fixture """ - _bot = Bot(TOKEN, loop=event_loop, parse_mode=types.ParseMode.HTML) + _bot = Bot(TOKEN, parse_mode=types.ParseMode.HTML) yield _bot await _bot.close() -@pytest.yield_fixture() -async def message(bot, event_loop): +@pytest.fixture() +async def message(bot): """ Message fixture :param bot: Telegram bot fixture @@ -28,7 +28,7 @@ async def message(bot, event_loop): from .types.dataset import MESSAGE msg = types.Message(**MESSAGE) - async with FakeTelegram(message_data=MESSAGE, loop=event_loop): + async with FakeTelegram(message_data=MESSAGE): _message = await bot.send_message(chat_id=msg.chat.id, text=msg.text) yield _message From 1349554dfd1f5c34523b5277427bb5f75f744125 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Tue, 12 Jan 2021 14:31:08 -0600 Subject: [PATCH 054/118] Update installation instruction for Arch (#490) * Update installation instruction for Arch The package is now available in the official repository. * Add pacman command example * Add an additional empty line --- docs/source/install.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index e4717a42..74c12865 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -13,9 +13,13 @@ Using Pipenv $ pipenv install aiogram -Using AUR +Using Pacman --------- -*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. +*aiogram* is also available in Arch Linux Repository, so you can install this framework on any Arch-based distribution like Arch Linux, Antergos, Manjaro, etc. To do this, just use pacman to install the `python-aiogram `_ package: + + .. code-block:: bash + + $ pacman -S python-aiogram From sources ------------ From ad4b215e7140df8b0cb6cfc4b8f0615f3f266de2 Mon Sep 17 00:00:00 2001 From: Mykola Solodukha Date: Tue, 12 Jan 2021 21:21:56 +0200 Subject: [PATCH 055/118] Add ability to `.answer_*` and `.reply_*` live location --- aiogram/types/message.py | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 4b5a47bc..dd356085 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -957,7 +957,10 @@ class Message(base.TelegramObject): self, latitude: base.Float, longitude: base.Float, + horizontal_accuracy: typing.Optional[base.Float] = None, live_period: typing.Optional[base.Integer] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ @@ -980,9 +983,22 @@ class Message(base.TelegramObject): :param longitude: Longitude of the location :type longitude: :obj:`base.Float` + :param horizontal_accuracy: The radius of uncertainty for the location, + measured in meters; 0-1500 + :type horizontal_accuracy: :obj:`typing.Optional[base.Float]` + :param live_period: Period in seconds for which the location will be updated :type live_period: :obj:`typing.Optional[base.Integer]` + :param heading: For live locations, a direction in which the user is moving, + in degrees. Must be between 1 and 360 if specified. + :type heading: :obj:`typing.Optional[base.Integer]` + + :param proximity_alert_radius: For live locations, a maximum distance for + proximity alerts about approaching another chat member, in meters. Must + be between 1 and 100000 if specified. + :type proximity_alert_radius: :obj:`typing.Optional[base.Integer]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` @@ -1005,7 +1021,10 @@ class Message(base.TelegramObject): chat_id=self.chat.id, latitude=latitude, longitude=longitude, + horizontal_accuracy=horizontal_accuracy, live_period=live_period, + heading=heading, + proximity_alert_radius=proximity_alert_radius, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, allow_sending_without_reply=allow_sending_without_reply, @@ -2055,7 +2074,10 @@ class Message(base.TelegramObject): self, latitude: base.Float, longitude: base.Float, + horizontal_accuracy: typing.Optional[base.Float] = None, live_period: typing.Optional[base.Integer] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -2073,18 +2095,37 @@ class Message(base.TelegramObject): :param latitude: Latitude of the location :type latitude: :obj:`base.Float` + :param longitude: Longitude of the location :type longitude: :obj:`base.Float` + + :param horizontal_accuracy: The radius of uncertainty for the location, + measured in meters; 0-1500 + :type horizontal_accuracy: :obj:`typing.Optional[base.Float]` + :param live_period: Period in seconds for which the location will be updated :type live_period: :obj:`typing.Optional[base.Integer]` + + :param heading: For live locations, a direction in which the user is moving, + in degrees. Must be between 1 and 360 if specified. + :type heading: :obj:`typing.Optional[base.Integer]` + + :param proximity_alert_radius: For live locations, a maximum distance for + proximity alerts about approaching another chat member, in meters. Must + be between 1 and 100000 if specified. + :type proximity_alert_radius: :obj:`typing.Optional[base.Integer]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Optional[base.Boolean]` + :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' :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -2092,7 +2133,10 @@ class Message(base.TelegramObject): chat_id=self.chat.id, latitude=latitude, longitude=longitude, + horizontal_accuracy=horizontal_accuracy, live_period=live_period, + heading=heading, + proximity_alert_radius=proximity_alert_radius, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup, From 8656c6bc241a1a3e255fa6e37e14ebbe9deea519 Mon Sep 17 00:00:00 2001 From: Mykola Solodukha Date: Fri, 15 Jan 2021 05:12:32 +0200 Subject: [PATCH 056/118] Fix `args` order in `.reply_*` and `.answer_location()` --- aiogram/types/message.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index dd356085..ee6cd7c2 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -957,10 +957,7 @@ class Message(base.TelegramObject): self, latitude: base.Float, longitude: base.Float, - horizontal_accuracy: typing.Optional[base.Float] = None, live_period: typing.Optional[base.Integer] = None, - heading: typing.Optional[base.Integer] = None, - proximity_alert_radius: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ @@ -971,6 +968,9 @@ class Message(base.TelegramObject): None, ] = None, reply: base.Boolean = False, + horizontal_accuracy: typing.Optional[base.Float] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, ) -> Message: """ Use this method to send point on the map. @@ -2074,10 +2074,7 @@ class Message(base.TelegramObject): self, latitude: base.Float, longitude: base.Float, - horizontal_accuracy: typing.Optional[base.Float] = None, live_period: typing.Optional[base.Integer] = None, - heading: typing.Optional[base.Integer] = None, - proximity_alert_radius: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, @@ -2087,6 +2084,9 @@ class Message(base.TelegramObject): None, ] = None, reply: base.Boolean = True, + horizontal_accuracy: typing.Optional[base.Float] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, ) -> Message: """ Use this method to send point on the map. From adba85c4bea392ea059d31d888b90518c5f030c8 Mon Sep 17 00:00:00 2001 From: nacknime <34421603+nacknime-official@users.noreply.github.com> Date: Fri, 22 Jan 2021 09:49:46 +0200 Subject: [PATCH 057/118] docs(install): typo "form" -> "from" (#498) --- 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 74c12865..4eaa351f 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -32,7 +32,7 @@ From sources $ cd aiogram $ python setup.py install - Or if you want to install stable version (The same with version form PyPi): + Or if you want to install stable version (The same with version from PyPi): .. code-block:: bash From cd5a77211008ccbf94e12248a1a6b31bc3926811 Mon Sep 17 00:00:00 2001 From: Mykola Solodukha Date: Sat, 23 Jan 2021 19:04:30 +0200 Subject: [PATCH 058/118] Logically order args --- aiogram/types/message.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index ee6cd7c2..b018ea11 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -960,6 +960,9 @@ class Message(base.TelegramObject): live_period: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, + horizontal_accuracy: typing.Optional[base.Float] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -968,9 +971,6 @@ class Message(base.TelegramObject): None, ] = None, reply: base.Boolean = False, - horizontal_accuracy: typing.Optional[base.Float] = None, - heading: typing.Optional[base.Integer] = None, - proximity_alert_radius: typing.Optional[base.Integer] = None, ) -> Message: """ Use this method to send point on the map. @@ -2076,6 +2076,9 @@ class Message(base.TelegramObject): longitude: base.Float, live_period: typing.Optional[base.Integer] = None, disable_notification: typing.Optional[base.Boolean] = None, + horizontal_accuracy: typing.Optional[base.Float] = None, + heading: typing.Optional[base.Integer] = None, + proximity_alert_radius: typing.Optional[base.Integer] = None, reply_markup: typing.Union[ InlineKeyboardMarkup, ReplyKeyboardMarkup, @@ -2084,9 +2087,6 @@ class Message(base.TelegramObject): None, ] = None, reply: base.Boolean = True, - horizontal_accuracy: typing.Optional[base.Float] = None, - heading: typing.Optional[base.Integer] = None, - proximity_alert_radius: typing.Optional[base.Integer] = None, ) -> Message: """ Use this method to send point on the map. From 4701b852bde1f0541c303e93fea8a3f6c1524a0b Mon Sep 17 00:00:00 2001 From: Yan Khachko Date: Tue, 9 Feb 2021 11:38:51 +0000 Subject: [PATCH 059/118] Update chat_type_filter.py --- examples/chat_type_filter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/chat_type_filter.py b/examples/chat_type_filter.py index 08bb1858..1224e3f1 100644 --- a/examples/chat_type_filter.py +++ b/examples/chat_type_filter.py @@ -22,8 +22,8 @@ dp = Dispatcher(bot) @dp.message_handler(chat_type=[ChatType.PRIVATE, ChatType.CHANNEL]) async def send_welcome(message: types.Message): """ - This handler will be called when user sends `/start` or `/help` command - """ + This handler will be called when user sends message in private chat or channel + """ await message.reply("Hi!\nI'm hearing your messages in private chats and channels") # propagate message to the next handler @@ -33,7 +33,7 @@ async def send_welcome(message: types.Message): @dp.message_handler(chat_type=ChatType.PRIVATE) async def send_welcome(message: types.Message): """ - This handler will be called when user sends `/start` or `/help` command + This handler will be called when user sends message in private chat """ await message.reply("Hi!\nI'm hearing your messages only in private chats") From c625b7beb06c6695b78aea4d081c9a2ee4852bf2 Mon Sep 17 00:00:00 2001 From: Yan Khachko Date: Tue, 9 Feb 2021 15:31:46 +0000 Subject: [PATCH 060/118] Update chat_type_filter.py --- examples/chat_type_filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/chat_type_filter.py b/examples/chat_type_filter.py index 1224e3f1..e57f8825 100644 --- a/examples/chat_type_filter.py +++ b/examples/chat_type_filter.py @@ -22,8 +22,8 @@ dp = Dispatcher(bot) @dp.message_handler(chat_type=[ChatType.PRIVATE, ChatType.CHANNEL]) async def send_welcome(message: types.Message): """ - This handler will be called when user sends message in private chat or channel - """ + This handler will be called when user sends message in private chat or channel + """ await message.reply("Hi!\nI'm hearing your messages in private chats and channels") # propagate message to the next handler From 75222b8af020506b61af5227af2a51c7cd32d190 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 7 Mar 2021 15:27:41 +0200 Subject: [PATCH 061/118] #516: Fix updating chat --- aiogram/__init__.py | 2 +- aiogram/types/base.py | 3 ++- aiogram/types/chat.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index a266ce1b..b04113ca 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.11.2' +__version__ = '2.11.3' __api_version__ = '5.0' diff --git a/aiogram/types/base.py b/aiogram/types/base.py index e64d3398..373f47be 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -212,7 +212,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): """ if item in self.props: return self.props[item].get_value(self) - raise KeyError(item) + return self.values[item] def __setitem__(self, key: str, value: typing.Any) -> None: """ @@ -224,6 +224,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): """ if key in self.props: return self.props[key].set_value(self, value, self.conf.get('parent', None)) + self.values[key] = value raise KeyError(key) def __contains__(self, item: str) -> bool: diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 1ff5dc75..4f062b49 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -113,7 +113,7 @@ class Chat(base.TelegramObject): async def update_chat(self): """ - User this method to update Chat data + Use this method to update Chat data :return: None """ From 24fb07d3fec290ae4bec37553e82e8ae15c901fb Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 14 Mar 2021 19:05:17 +0300 Subject: [PATCH 062/118] Bot API 5.1 (#519) * version update * added ChatMemberUpdated class * added ChatInviteLink class * 2.x version update * update types added * added methods createChatInviteLink, editChatInviteLink, revokeChatInviteLink * Voice Chat types added * added Message fields: voice_chat_started, voice_chat_ended, voice_chat_participants_invited * can_manage_voice_chats added * chat links shortcuts added * bowling dice support * reordered ChatMembers params (no changes) * Added can_manage_chat to the class ChatMember and parameter can_manage_chat to the method promoteChatMember * kick_chat_member refactored + docs update * Added the parameter revoke_messages to the method kickChatMember * updated kick_chat_member shortcut for Chat * Added the type MessageAutoDeleteTimerChanged and the field message_auto_delete_timer_changed to the class Message * feat: add methods to register my_chat_member and chat_member handlers * Updated filters for new event types Co-authored-by: Alex Root Junior --- aiogram/__init__.py | 4 +- aiogram/bot/api.py | 5 +- aiogram/bot/bot.py | 145 ++++++++++++++++-- aiogram/contrib/middlewares/logging.py | 20 +++ aiogram/dispatcher/dispatcher.py | 127 +++++++++++++++ aiogram/dispatcher/filters/builtin.py | 23 ++- aiogram/types/__init__.py | 12 ++ aiogram/types/chat.py | 81 ++++++++-- aiogram/types/chat_invite_link.py | 20 +++ aiogram/types/chat_member.py | 10 +- aiogram/types/chat_member_updated.py | 22 +++ aiogram/types/dice.py | 5 +- aiogram/types/message.py | 28 +++- .../message_auto_delete_timer_changed.py | 11 ++ aiogram/types/update.py | 5 + aiogram/types/voice_chat_ended.py | 13 ++ .../types/voice_chat_participants_invited.py | 16 ++ aiogram/types/voice_chat_started.py | 12 ++ 18 files changed, 510 insertions(+), 49 deletions(-) create mode 100644 aiogram/types/chat_invite_link.py create mode 100644 aiogram/types/chat_member_updated.py create mode 100644 aiogram/types/message_auto_delete_timer_changed.py create mode 100644 aiogram/types/voice_chat_ended.py create mode 100644 aiogram/types/voice_chat_participants_invited.py create mode 100644 aiogram/types/voice_chat_started.py diff --git a/aiogram/__init__.py b/aiogram/__init__.py index b04113ca..ef832be9 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.11.3' -__api_version__ = '5.0' +__version__ = '2.12' +__api_version__ = '5.1' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 9b86e7ca..e3d3bf9a 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -189,7 +189,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 5.0 + List is updated to Bot API 5.1 """ mode = HelperMode.lowerCamelCase @@ -231,6 +231,9 @@ class Methods(Helper): SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE = Item() # setChatAdministratorCustomTitle SET_CHAT_PERMISSIONS = Item() # setChatPermissions EXPORT_CHAT_INVITE_LINK = Item() # exportChatInviteLink + CREATE_CHAT_INVITE_LINK = Item() # createChatInviteLink + EDIT_CHAT_INVITE_LINK = Item() # editChatInviteLink + REVOKE_CHAT_INVITE_LINK = Item() # revokeChatInviteLink SET_CHAT_PHOTO = Item() # setChatPhoto DELETE_CHAT_PHOTO = Item() # deleteChatPhoto SET_CHAT_TITLE = Item() # setChatTitle diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index ab244252..5db66758 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1550,28 +1550,43 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_FILE, payload) 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, datetime.datetime, datetime.timedelta, None] = None) -> base.Boolean: + async def kick_chat_member(self, + chat_id: typing.Union[base.Integer, base.String], + user_id: base.Integer, + until_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None] = None, + revoke_messages: typing.Optional[base.Boolean] = 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 - on their own using invite links, etc., unless unbanned first. + In the case of supergroups and channels, the user will not be able to return + to the chat on their own using invite links, etc., unless unbanned first. - The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. - - Note: In regular groups (non-supergroups), this method will only work if the โ€˜All Members Are Adminsโ€™ setting - is off in the target group. - Otherwise members may only be removed by the group's creator or by the member that added them. + The bot must be an administrator in the chat for this to work and must have + the appropriate admin rights. Source: https://core.telegram.org/bots/api#kickchatmember - :param chat_id: Unique identifier for the target group or username of the target supergroup or channel + :param chat_id: Unique identifier for the target group or username of the + target supergroup or channel (in the format @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` - :param until_date: Date when the user will be unbanned, unix time - :type until_date: :obj:`typing.Optional[base.Integer]` + + :param until_date: Date when the user will be unbanned. If user is banned + for more than 366 days or less than 30 seconds from the current time they + are considered to be banned forever. Applied for supergroups and channels + only. + :type until_date: :obj:`typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None` + + :param revoke_messages: Pass True to delete all messages from the chat for + the user that is being removed. If False, the user will be able to see + messages in the group that were sent before the user was removed. Always + True for supergroups and channels. + :type revoke_messages: :obj:`typing.Optional[base.Boolean]` + :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -1675,10 +1690,12 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer, is_anonymous: typing.Optional[base.Boolean] = None, + can_manage_chat: typing.Optional[base.Boolean] = None, can_change_info: typing.Optional[base.Boolean] = None, can_post_messages: typing.Optional[base.Boolean] = None, can_edit_messages: typing.Optional[base.Boolean] = None, can_delete_messages: typing.Optional[base.Boolean] = None, + can_manage_voice_chats: typing.Optional[base.Boolean] = None, can_invite_users: typing.Optional[base.Boolean] = None, can_restrict_members: typing.Optional[base.Boolean] = None, can_pin_messages: typing.Optional[base.Boolean] = None, @@ -1700,6 +1717,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param is_anonymous: Pass True, if the administrator's presence in the chat is hidden :type is_anonymous: :obj:`typing.Optional[base.Boolean]` + :param can_manage_chat: Pass True, if the administrator can access the chat event log, chat statistics, + message statistics in channels, see channel members, see anonymous administrators in supergroups + and ignore slow mode. Implied by any other administrator privilege + :type can_manage_chat: :obj:`typing.Optional[base.Boolean]` + :param can_change_info: Pass True, if the administrator can change chat title, photo and other settings :type can_change_info: :obj:`typing.Optional[base.Boolean]` @@ -1712,6 +1734,9 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param can_delete_messages: Pass True, if the administrator can delete messages of other users :type can_delete_messages: :obj:`typing.Optional[base.Boolean]` + :param can_manage_voice_chats: Pass True, if the administrator can manage voice chats, supergroups only + :type can_manage_voice_chats: :obj:`typing.Optional[base.Boolean]` + :param can_invite_users: Pass True, if the administrator can invite new users to the chat :type can_invite_users: :obj:`typing.Optional[base.Boolean]` @@ -1789,6 +1814,100 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.EXPORT_CHAT_INVITE_LINK, payload) return result + async def create_chat_invite_link(self, + chat_id: typing.Union[base.Integer, base.String], + expire_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None], + member_limit: typing.Optional[base.Integer], + ) -> types.ChatInviteLink: + """ + Use this method to create an additional invite link for a chat. + The bot must be an administrator in the chat for this to work and must have + the appropriate admin rights. The link can be revoked using the method + revokeChatInviteLink. + + Source: https://core.telegram.org/bots/api#createchatinvitelink + + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + + :param expire_date: Point in time when the link will expire + :type expire_date: :obj:`typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None]` + + :param member_limit: Maximum number of users that can be members of the chat + simultaneously after joining the chat via this invite link; 1-99999 + :type member_limit: :obj:`typing.Optional[base.Integer]` + + :return: the new invite link as ChatInviteLink object. + :rtype: :obj:`types.ChatInviteLink` + """ + expire_date = prepare_arg(expire_date) + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload) + return result + + async def edit_chat_invite_link(self, + chat_id: typing.Union[base.Integer, base.String], + invite_link: base.String, + expire_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None], + member_limit: typing.Optional[base.Integer], + ) -> types.ChatInviteLink: + """ + Use this method to edit a non-primary invite link created by the bot. + The bot must be an administrator in the chat for this to work and must have + the appropriate admin rights. + + Source: https://core.telegram.org/bots/api#editchatinvitelink + + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + + :param invite_link: The invite link to edit + :type invite_link: :obj:`base.String` + + :param expire_date: Point in time (Unix timestamp) when the link will expire + :type expire_date: :obj:`typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None]` + + :param member_limit: Maximum number of users that can be members of the chat + simultaneously after joining the chat via this invite link; 1-99999 + :type member_limit: :obj:`typing.Optional[base.Integer]` + + :return: edited invite link as a ChatInviteLink object. + """ + expire_date = prepare_arg(expire_date) + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload) + return result + + async def revoke_chat_invite_link(self, + chat_id: typing.Union[base.Integer, base.String], + invite_link: base.String, + ) -> types.ChatInviteLink: + """ + Use this method to revoke an invite link created by the bot. + If the primary link is revoked, a new link is automatically generated. + The bot must be an administrator in the chat for this to work and must have + the appropriate admin rights. + + Source: https://core.telegram.org/bots/api#revokechatinvitelink + + :param chat_id: Unique identifier for the target chat or username of the + target channel (in the format @channelusername) + :param invite_link: The invite link to revoke + :return: the revoked invite link as ChatInviteLink object + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload) + return result + async def set_chat_photo(self, chat_id: typing.Union[base.Integer, base.String], photo: base.InputFile) -> base.Boolean: """ diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index 308d0e10..82c2b50a 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -160,6 +160,26 @@ class LoggingMiddleware(BaseMiddleware): self.logger.debug(f"{HANDLED_STR[bool(len(results))]} poll answer [ID:{poll_answer.poll_id}] " f"from user [ID:{poll_answer.user.id}]") + async def on_pre_process_my_chat_member(self, my_chat_member_update, data): + self.logger.info(f"Received chat member update " + f"for user [ID:{my_chat_member_update.from_user.id}]. " + f"Old state: {my_chat_member_update.old_chat_member.to_python()} " + f"New state: {my_chat_member_update.new_chat_member.to_python()} ") + + async def on_post_process_my_chat_member(self, my_chat_member_update, results, data): + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} my_chat_member " + f"for user [ID:{my_chat_member_update.from_user.id}]") + + async def on_pre_process_chat_member(self, chat_member_update, data): + self.logger.info(f"Received chat member update " + f"for user [ID:{chat_member_update.from_user.id}]. " + f"Old state: {chat_member_update.old_chat_member.to_python()} " + f"New state: {chat_member_update.new_chat_member.to_python()} ") + + async def on_post_process_chat_member(self, chat_member_update, results, data): + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} chat_member " + f"for user [ID:{chat_member_update.from_user.id}]") + class LoggingFilter(logging.Filter): """ diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index b38d3af1..050f148a 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -78,6 +78,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): 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.my_chat_member_handlers = Handler(self, middleware_key='my_chat_member') + self.chat_member_handlers = Handler(self, middleware_key='chat_member') self.errors_handlers = Handler(self, once=False, middleware_key='error') self.middleware = MiddlewareManager(self) @@ -163,6 +165,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.edited_channel_post_handlers, self.callback_query_handlers, self.inline_query_handlers, + self.chat_member_handlers, ]) filters_factory.bind(IDFilter, event_handlers=[ self.message_handlers, @@ -171,6 +174,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.edited_channel_post_handlers, self.callback_query_handlers, self.inline_query_handlers, + self.chat_member_handlers, + self.my_chat_member_handlers, ]) filters_factory.bind(IsReplyFilter, event_handlers=[ self.message_handlers, @@ -196,6 +201,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.channel_post_handlers, self.edited_channel_post_handlers, self.callback_query_handlers, + self.my_chat_member_handlers, + self.chat_member_handlers ]) def __del__(self): @@ -286,6 +293,14 @@ class Dispatcher(DataMixin, ContextInstanceMixin): 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) + if update.my_chat_member: + types.ChatMemberUpdated.set_current(update.my_chat_member) + types.User.set_current(update.my_chat_member.from_user) + return await self.my_chat_member_handlers.notify(update.my_chat_member) + if update.chat_member: + types.ChatMemberUpdated.set_current(update.chat_member) + types.User.set_current(update.chat_member.from_user) + return await self.chat_member_handlers.notify(update.chat_member) except Exception as e: err = await self.errors_handlers.notify(update, e) if err: @@ -1005,6 +1020,118 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return decorator + def register_my_chat_member_handler(self, + callback: typing.Callable, + *custom_filters, + run_task: typing.Optional[bool] = None, + **kwargs) -> None: + """ + Register handler for my_chat_member + + Example: + + .. code-block:: python3 + + dp.register_my_chat_member_handler(some_my_chat_member_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.my_chat_member_handlers, + *custom_filters, + **kwargs, + ) + self.my_chat_member_handlers.register( + handler=self._wrap_async_task(callback, run_task), + filters=filters_set, + ) + + def my_chat_member_handler(self, *custom_filters, run_task=None, **kwargs): + """ + Decorator for my_chat_member handler + + Example: + + .. code-block:: python3 + + @dp.my_chat_member_handler() + async def some_handler(my_chat_member: types.ChatMemberUpdated) + + :param custom_filters: + :param run_task: run callback in task (no wait results) + :param kwargs: + """ + + def decorator(callback): + self.register_my_chat_member_handler( + callback, + *custom_filters, + run_task=run_task, + **kwargs, + ) + return callback + + return decorator + + def register_chat_member_handler(self, + callback: typing.Callable, + *custom_filters, + run_task: typing.Optional[bool] = None, + **kwargs) -> None: + """ + Register handler for chat_member + + Example: + + .. code-block:: python3 + + dp.register_chat_member_handler(some_chat_member_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.chat_member_handlers, + *custom_filters, + **kwargs, + ) + self.chat_member_handlers.register( + handler=self._wrap_async_task(callback, run_task), + filters=filters_set, + ) + + def chat_member_handler(self, *custom_filters, run_task=None, **kwargs): + """ + Decorator for chat_member handler + + Example: + + .. code-block:: python3 + + @dp.chat_member_handler() + async def some_handler(chat_member: types.ChatMemberUpdated) + + :param custom_filters: + :param run_task: run callback in task (no wait results) + :param kwargs: + """ + + def decorator(callback): + self.register_chat_member_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): """ Register handler for errors diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 20317f57..ded3e9fd 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -10,7 +10,7 @@ from babel.support import LazyProxy from aiogram import types from aiogram.dispatcher.filters.filters import BoundFilter, Filter -from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll +from aiogram.types import CallbackQuery, ChatType, InlineQuery, Message, Poll, ChatMemberUpdated ChatIDArgumentType = typing.Union[typing.Iterable[typing.Union[int, str]], str, int] @@ -604,7 +604,7 @@ class IDFilter(Filter): return result - async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]): + async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, ChatMemberUpdated]): if isinstance(obj, Message): user_id = None if obj.from_user is not None: @@ -619,6 +619,9 @@ class IDFilter(Filter): elif isinstance(obj, InlineQuery): user_id = obj.from_user.id chat_id = None + elif isinstance(obj, ChatMemberUpdated): + user_id = obj.from_user.id + chat_id = obj.chat.id else: return False @@ -663,19 +666,21 @@ class AdminFilter(Filter): return result - async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]) -> bool: + async def check(self, obj: Union[Message, CallbackQuery, InlineQuery, ChatMemberUpdated]) -> bool: user_id = obj.from_user.id if self._check_current: if isinstance(obj, Message): - message = obj + chat = obj.chat elif isinstance(obj, CallbackQuery) and obj.message: - message = obj.message + chat = obj.message.chat + elif isinstance(obj, ChatMemberUpdated): + chat = obj.chat else: return False - if message.chat.type == ChatType.PRIVATE: # there is no admin in private chats + if chat.type == ChatType.PRIVATE: # there is no admin in private chats return False - chat_ids = [message.chat.id] + chat_ids = [chat.id] else: chat_ids = self._chat_ids @@ -719,11 +724,13 @@ class ChatTypeFilter(BoundFilter): self.chat_type: typing.Set[str] = set(chat_type) - async def check(self, obj: Union[Message, CallbackQuery]): + async def check(self, obj: Union[Message, CallbackQuery, ChatMemberUpdated]): if isinstance(obj, Message): obj = obj.chat elif isinstance(obj, CallbackQuery): obj = obj.message.chat + elif isinstance(obj, ChatMemberUpdated): + obj = obj.chat else: warnings.warn("ChatTypeFilter doesn't support %s as input", type(obj)) return False diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 26201130..1dfa519f 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -7,8 +7,10 @@ from .bot_command import BotCommand from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType +from .chat_invite_link import ChatInviteLink from .chat_location import ChatLocation from .chat_member import ChatMember, ChatMemberStatus +from .chat_member_updated import ChatMemberUpdated from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto from .chosen_inline_result import ChosenInlineResult @@ -40,6 +42,7 @@ from .location import Location from .login_url import LoginUrl from .mask_position import MaskPosition from .message import ContentType, ContentTypes, Message, ParseMode +from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged from .message_entity import MessageEntity, MessageEntityType from .message_id import MessageId from .order_info import OrderInfo @@ -67,6 +70,9 @@ from .venue import Venue from .video import Video from .video_note import VideoNote from .voice import Voice +from .voice_chat_ended import VoiceChatEnded +from .voice_chat_participants_invited import VoiceChatParticipantsInvited +from .voice_chat_started import VoiceChatStarted from .webhook_info import WebhookInfo __all__ = ( @@ -79,9 +85,11 @@ __all__ = ( 'CallbackQuery', 'Chat', 'ChatActions', + 'ChatInviteLink', 'ChatLocation', 'ChatMember', 'ChatMemberStatus', + 'ChatMemberUpdated', 'ChatPermissions', 'ChatPhoto', 'ChatType', @@ -143,6 +151,7 @@ __all__ = ( 'MaskPosition', 'MediaGroup', 'Message', + 'MessageAutoDeleteTimerChanged', 'MessageEntity', 'MessageEntityType', 'MessageId', @@ -180,6 +189,9 @@ __all__ = ( 'Video', 'VideoNote', 'Voice', + 'VoiceChatEnded', + 'VoiceChatParticipantsInvited', + 'VoiceChatStarted', 'WebhookInfo', 'base', 'fields', diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 4f062b49..957ee78b 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -5,6 +5,7 @@ import datetime import typing from . import base, fields +from .chat_invite_link import ChatInviteLink from .chat_location import ChatLocation from .chat_member import ChatMember from .chat_permissions import ChatPermissions @@ -185,30 +186,47 @@ class Chat(base.TelegramObject): """ 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: + async def kick(self, + user_id: base.Integer, + until_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None] = None, + revoke_messages: typing.Optional[base.Boolean] = 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 - on their own using invite links, etc., unless unbanned first. + In the case of supergroups and channels, the user will not be able to return + to the chat on their own using invite links, etc., unless unbanned first. - The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. - - Note: In regular groups (non-supergroups), this method will only work if the โ€˜All Members Are Adminsโ€™ setting - is off in the target group. - Otherwise members may only be removed by the group's creator or by the member that added them. + The bot must be an administrator in the chat for this to work and must have + the appropriate admin rights. Source: https://core.telegram.org/bots/api#kickchatmember :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` - :param until_date: Date when the user will be unbanned, unix time. - :type until_date: :obj:`typing.Optional[base.Integer]` - :return: Returns True on success. + + :param until_date: Date when the user will be unbanned. If user is banned + for more than 366 days or less than 30 seconds from the current time they + are considered to be banned forever. Applied for supergroups and channels + only. + :type until_date: :obj:`typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None` + + :param revoke_messages: Pass True to delete all messages from the chat for + the user that is being removed. If False, the user will be able to see + messages in the group that were sent before the user was removed. Always + True for supergroups and channels. + :type revoke_messages: :obj:`typing.Optional[base.Boolean]` + + :return: Returns True on success :rtype: :obj:`base.Boolean` """ - return await self.bot.kick_chat_member(self.id, user_id=user_id, until_date=until_date) + return await self.bot.kick_chat_member( + chat_id=self.id, + user_id=user_id, + until_date=until_date, + revoke_messages=revoke_messages, + ) async def unban(self, user_id: base.Integer, @@ -554,6 +572,41 @@ class Chat(base.TelegramObject): return self.invite_link + async def create_invite_link(self, + expire_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None], + member_limit: typing.Optional[base.Integer], + ) -> ChatInviteLink: + """ Shortcut for createChatInviteLink method. """ + return await self.bot.create_chat_invite_link( + chat_id=self.id, + expire_date=expire_date, + member_limit=member_limit, + ) + + async def edit_invite_link(self, + invite_link: base.String, + expire_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None], + member_limit: typing.Optional[base.Integer], + ) -> ChatInviteLink: + """ Shortcut for editChatInviteLink method. """ + return await self.bot.edit_chat_invite_link( + chat_id=self.id, + invite_link=invite_link, + expire_date=expire_date, + member_limit=member_limit, + ) + + async def revoke_invite_link(self, + invite_link: base.String, + ) -> ChatInviteLink: + """ Shortcut for revokeChatInviteLink method. """ + return await self.bot.revoke_chat_invite_link( + chat_id=self.id, + invite_link=invite_link, + ) + def __int__(self): return self.id diff --git a/aiogram/types/chat_invite_link.py b/aiogram/types/chat_invite_link.py new file mode 100644 index 00000000..55794780 --- /dev/null +++ b/aiogram/types/chat_invite_link.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from . import base +from . import fields +from .user import User + + +class ChatInviteLink(base.TelegramObject): + """ + Represents an invite link for a chat. + + https://core.telegram.org/bots/api#chatinvitelink + """ + + invite_link: base.String = fields.Field() + creator: User = fields.Field(base=User) + is_primary: base.Boolean = fields.Field() + is_revoked: base.Boolean = fields.Field() + expire_date: datetime = fields.DateTimeField() + member_limit: base.Integer = fields.Field() diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 4aa52b80..c48a91d0 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -16,22 +16,24 @@ class ChatMember(base.TelegramObject): status: base.String = fields.Field() custom_title: base.String = fields.Field() is_anonymous: base.Boolean = fields.Field() - until_date: datetime.datetime = fields.DateTimeField() can_be_edited: base.Boolean = fields.Field() - can_change_info: base.Boolean = fields.Field() + can_manage_chat: base.Boolean = fields.Field() can_post_messages: base.Boolean = fields.Field() can_edit_messages: base.Boolean = fields.Field() can_delete_messages: base.Boolean = fields.Field() - can_invite_users: base.Boolean = fields.Field() + can_manage_voice_chats: base.Boolean = fields.Field() can_restrict_members: base.Boolean = fields.Field() - can_pin_messages: base.Boolean = fields.Field() can_promote_members: base.Boolean = fields.Field() + can_change_info: base.Boolean = fields.Field() + can_invite_users: base.Boolean = fields.Field() + can_pin_messages: base.Boolean = fields.Field() is_member: base.Boolean = fields.Field() can_send_messages: base.Boolean = fields.Field() can_send_media_messages: base.Boolean = fields.Field() can_send_polls: base.Boolean = fields.Field() can_send_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() + until_date: datetime.datetime = fields.DateTimeField() def is_chat_creator(self) -> bool: return ChatMemberStatus.is_chat_creator(self.status) diff --git a/aiogram/types/chat_member_updated.py b/aiogram/types/chat_member_updated.py new file mode 100644 index 00000000..7c6a124f --- /dev/null +++ b/aiogram/types/chat_member_updated.py @@ -0,0 +1,22 @@ +import datetime + +from . import base +from . import fields +from .chat import Chat +from .chat_invite_link import ChatInviteLink +from .chat_member import ChatMember +from .user import User + + +class ChatMemberUpdated(base.TelegramObject): + """ + This object represents changes in the status of a chat member. + + https://core.telegram.org/bots/api#chatmemberupdated + """ + chat: Chat = fields.Field(base=Chat) + from_user: User = fields.Field(base=User) + date: datetime.datetime = fields.DateTimeField() + old_chat_member: ChatMember = fields.Field(base=ChatMember) + new_chat_member: ChatMember = fields.Field(base=ChatMember) + invite_link: ChatInviteLink = fields.Field(base=ChatInviteLink) diff --git a/aiogram/types/dice.py b/aiogram/types/dice.py index 70c50e09..c4f2725e 100644 --- a/aiogram/types/dice.py +++ b/aiogram/types/dice.py @@ -3,9 +3,7 @@ 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!) + This object represents an animated emoji that displays a random value. https://core.telegram.org/bots/api#dice """ @@ -19,3 +17,4 @@ class DiceEmoji: BASKETBALL = '๐Ÿ€' FOOTBALL = 'โšฝ' SLOT_MACHINE = '๐ŸŽฐ' + BOWLING = '๐ŸŽณ' diff --git a/aiogram/types/message.py b/aiogram/types/message.py index b018ea11..4b7e4475 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -4,10 +4,6 @@ import datetime import functools import typing -from ..utils import helper -from ..utils import markdown as md -from ..utils.deprecated import deprecated -from ..utils.text_decorations import html_decoration, markdown_decoration from . import base, fields from .animation import Animation from .audio import Audio @@ -21,6 +17,7 @@ from .inline_keyboard import InlineKeyboardMarkup from .input_media import InputMedia, MediaGroup from .invoice import Invoice from .location import Location +from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged from .message_entity import MessageEntity from .message_id import MessageId from .passport_data import PassportData @@ -35,6 +32,13 @@ from .venue import Venue from .video import Video from .video_note import VideoNote from .voice import Voice +from .voice_chat_ended import VoiceChatEnded +from .voice_chat_participants_invited import VoiceChatParticipantsInvited +from .voice_chat_started import VoiceChatStarted +from ..utils import helper +from ..utils import markdown as md +from ..utils.deprecated import deprecated +from ..utils.text_decorations import html_decoration, markdown_decoration class Message(base.TelegramObject): @@ -86,6 +90,7 @@ class Message(base.TelegramObject): group_chat_created: base.Boolean = fields.Field() supergroup_chat_created: base.Boolean = fields.Field() channel_chat_created: base.Boolean = fields.Field() + message_auto_delete_timer_changed: MessageAutoDeleteTimerChanged = fields.Field(base=MessageAutoDeleteTimerChanged) migrate_to_chat_id: base.Integer = fields.Field() migrate_from_chat_id: base.Integer = fields.Field() pinned_message: Message = fields.Field(base="Message") @@ -94,6 +99,9 @@ class Message(base.TelegramObject): connected_website: base.String = fields.Field() passport_data: PassportData = fields.Field(base=PassportData) proximity_alert_triggered: ProximityAlertTriggered = fields.Field(base=ProximityAlertTriggered) + voice_chat_started: VoiceChatStarted = fields.Field(base=VoiceChatStarted) + voice_chat_ended: VoiceChatEnded = fields.Field(base=VoiceChatEnded) + voice_chat_participants_invited: VoiceChatParticipantsInvited = fields.Field(base=VoiceChatParticipantsInvited) reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup) @property @@ -139,6 +147,8 @@ class Message(base.TelegramObject): return ContentType.SUCCESSFUL_PAYMENT if self.connected_website: return ContentType.CONNECTED_WEBSITE + if self.message_auto_delete_timer_changed: + return ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED if self.migrate_from_chat_id: return ContentType.MIGRATE_FROM_CHAT_ID if self.migrate_to_chat_id: @@ -157,6 +167,12 @@ class Message(base.TelegramObject): return ContentType.PASSPORT_DATA if self.proximity_alert_triggered: return ContentType.PROXIMITY_ALERT_TRIGGERED + if self.voice_chat_started: + return ContentType.VOICE_CHAT_STARTED + if self.voice_chat_ended: + return ContentType.VOICE_CHAT_ENDED + if self.voice_chat_participants_invited: + return ContentType.VOICE_CHAT_PARTICIPANTS_INVITED return ContentType.UNKNOWN @@ -2980,6 +2996,7 @@ class ContentType(helper.Helper): INVOICE = helper.Item() # invoice SUCCESSFUL_PAYMENT = helper.Item() # successful_payment CONNECTED_WEBSITE = helper.Item() # connected_website + MESSAGE_AUTO_DELETE_TIMER_CHANGED = helper.Item() # message_auto_delete_timer_changed MIGRATE_TO_CHAT_ID = helper.Item() # migrate_to_chat_id MIGRATE_FROM_CHAT_ID = helper.Item() # migrate_from_chat_id PINNED_MESSAGE = helper.Item() # pinned_message @@ -2989,6 +3006,9 @@ class ContentType(helper.Helper): GROUP_CHAT_CREATED = helper.Item() # group_chat_created PASSPORT_DATA = helper.Item() # passport_data PROXIMITY_ALERT_TRIGGERED = helper.Item() # proximity_alert_triggered + VOICE_CHAT_STARTED = helper.Item() # voice_chat_started + VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended + VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited UNKNOWN = helper.Item() # unknown ANY = helper.Item() # any diff --git a/aiogram/types/message_auto_delete_timer_changed.py b/aiogram/types/message_auto_delete_timer_changed.py new file mode 100644 index 00000000..8b882d1b --- /dev/null +++ b/aiogram/types/message_auto_delete_timer_changed.py @@ -0,0 +1,11 @@ +from . import base +from . import fields + + +class MessageAutoDeleteTimerChanged(base.TelegramObject): + """ + This object represents a service message about a change in auto-delete timer settings. + + https://core.telegram.org/bots/api#messageautodeletetimerchanged + """ + message_auto_delete_time: base.Integer = fields.Field() diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 9d1afacc..c8c4b58d 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -3,6 +3,7 @@ from __future__ import annotations from . import base from . import fields from .callback_query import CallbackQuery +from .chat_member_updated import ChatMemberUpdated from .chosen_inline_result import ChosenInlineResult from .inline_query import InlineQuery from .message import Message @@ -31,6 +32,8 @@ class Update(base.TelegramObject): pre_checkout_query: PreCheckoutQuery = fields.Field(base=PreCheckoutQuery) poll: Poll = fields.Field(base=Poll) poll_answer: PollAnswer = fields.Field(base=PollAnswer) + my_chat_member: ChatMemberUpdated = fields.Field(base=ChatMemberUpdated) + chat_member: ChatMemberUpdated = fields.Field(base=ChatMemberUpdated) def __hash__(self): return self.update_id @@ -61,6 +64,8 @@ class AllowedUpdates(helper.Helper): PRE_CHECKOUT_QUERY = helper.ListItem() # pre_checkout_query POLL = helper.ListItem() # poll POLL_ANSWER = helper.ListItem() # poll_answer + MY_CHAT_MEMBER = helper.ListItem() # my_chat_member + CHAT_MEMBER = helper.ListItem() # chat_member CHOSEN_INLINE_QUERY = deprecated.DeprecatedReadOnlyClassVar( "`CHOSEN_INLINE_QUERY` is a deprecated value for allowed update. " diff --git a/aiogram/types/voice_chat_ended.py b/aiogram/types/voice_chat_ended.py new file mode 100644 index 00000000..f1bb1f05 --- /dev/null +++ b/aiogram/types/voice_chat_ended.py @@ -0,0 +1,13 @@ +from . import base +from . import fields +from . import mixins + + +class VoiceChatEnded(base.TelegramObject, mixins.Downloadable): + """ + This object represents a service message about a voice chat ended in the chat. + + https://core.telegram.org/bots/api#voicechatended + """ + + duration: base.Integer = fields.Field() diff --git a/aiogram/types/voice_chat_participants_invited.py b/aiogram/types/voice_chat_participants_invited.py new file mode 100644 index 00000000..fbd0a457 --- /dev/null +++ b/aiogram/types/voice_chat_participants_invited.py @@ -0,0 +1,16 @@ +import typing + +from . import base +from . import fields +from . import mixins +from .user import User + + +class VoiceChatParticipantsInvited(base.TelegramObject, mixins.Downloadable): + """ + This object represents a service message about new members invited to a voice chat. + + https://core.telegram.org/bots/api#voicechatparticipantsinvited + """ + + users: typing.List[User] = fields.ListField(base=User) diff --git a/aiogram/types/voice_chat_started.py b/aiogram/types/voice_chat_started.py new file mode 100644 index 00000000..3cd76322 --- /dev/null +++ b/aiogram/types/voice_chat_started.py @@ -0,0 +1,12 @@ +from . import base +from . import mixins + + +class VoiceChatStarted(base.TelegramObject, mixins.Downloadable): + """ + This object represents a service message about a voice chat started in the chat. + Currently holds no information. + + https://core.telegram.org/bots/api#voicechatstarted + """ + pass From 1354f8d58c5af1070cef4ed198a406efe561e969 Mon Sep 17 00:00:00 2001 From: Arseniy Kulikov <62447812+klkvr@users.noreply.github.com> Date: Sun, 14 Mar 2021 19:06:55 +0300 Subject: [PATCH 063/118] added answer_chat_action() method for types.Message (#501) --- aiogram/types/message.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 4b7e4475..e97231b6 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1416,6 +1416,30 @@ class Message(base.TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) + + async def answer_chat_action( + 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 + (when a message arrives from your bot, Telegram clients clear its typing status). + + We only recommend using this method when a response from the bot will take + a noticeable amount of time to arrive. + + Source: https://core.telegram.org/bots/api#sendchataction + + :param action: Type of action to broadcast + :type action: :obj:`base.String` + :return: Returns True on success + :rtype: :obj:`base.Boolean` + """ + return await self.bot.send_chat_action( + chat_id=self.chat.id, + action=action, + ) async def reply( self, From 5b1bed79428218e5c21db28543bfbdce34d99581 Mon Sep 17 00:00:00 2001 From: darksidecat <58224121+darksidecat@users.noreply.github.com> Date: Sun, 14 Mar 2021 18:10:40 +0200 Subject: [PATCH 064/118] fix bug in getting user_id, chat_id from context (#520) * fix bug in getting user_id, chat_id from context (need User.id for future use, not User object) * requested changes: get current() can return None add check this case --- aiogram/dispatcher/dispatcher.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 050f148a..a2736cf7 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -1211,8 +1211,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin): if rate is None: rate = self.throttling_rate_limit if user_id is None and chat_id is None: - user_id = types.User.get_current().id - chat_id = types.Chat.get_current().id + chat_obj = types.Chat.get_current() + chat_id = chat_obj.id if chat_obj else None + + user_obj = types.User.get_current() + user_id = user_obj.id if user_obj else None # Detect current time now = time.time() @@ -1263,8 +1266,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin): raise RuntimeError('This storage does not provide Leaky Bucket') if user_id is None and chat_id is None: - user_id = types.User.get_current() - chat_id = types.Chat.get_current() + chat_obj = types.Chat.get_current() + chat_id = chat_obj.id if chat_obj else None + + user_obj = types.User.get_current() + user_id = user_obj.id if user_obj else None bucket = await self.storage.get_bucket(chat=chat_id, user=user_id) data = bucket.get(key, {}) @@ -1285,8 +1291,11 @@ class Dispatcher(DataMixin, ContextInstanceMixin): raise RuntimeError('This storage does not provide Leaky Bucket') if user_id is None and chat_id is None: - user_id = types.User.get_current() - chat_id = types.Chat.get_current() + chat_obj = types.Chat.get_current() + chat_id = chat_obj.id if chat_obj else None + + user_obj = types.User.get_current() + user_id = user_obj.id if user_obj else None bucket = await self.storage.get_bucket(chat=chat_id, user=user_id) if bucket and key in bucket: From d9de7994798c48dad30d86a9636271a225540535 Mon Sep 17 00:00:00 2001 From: Fenicu Date: Sun, 14 Mar 2021 19:11:48 +0300 Subject: [PATCH 065/118] Add new types in message.send_copy method (#511) --- aiogram/types/message.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index e97231b6..2e8402e7 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -2931,6 +2931,14 @@ class Message(base.TelegramObject): return await self.bot.send_poll( question=self.poll.question, options=[option.text for option in self.poll.options], + is_anonymous=self.poll.is_anonymous, + allows_multiple_answers=self.poll.allows_multiple_answers + **kwargs, + ) + elif self.dice: + kwargs.pop("parse_mode") + return await self.bot.send_dice( + emoji=self.dice.emoji, **kwargs, ) else: From 5254329c2bc68f97334ca409de24535c9349743f Mon Sep 17 00:00:00 2001 From: Daniil Kovalenko <40635760+WhiteMemory99@users.noreply.github.com> Date: Sun, 14 Mar 2021 23:13:57 +0700 Subject: [PATCH 066/118] Fix types.MediaGroup methods (#514) --- aiogram/types/input_media.py | 56 ++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 2d325d89..4d03daec 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -311,76 +311,96 @@ class MediaGroup(base.TelegramObject): self.attach(animation) ''' - def attach_audio(self, audio: base.InputFile, + def attach_audio(self, audio: typing.Union[InputMediaAudio, 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): + parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None): """ - Attach animation + Attach audio :param audio: :param thumb: :param caption: - :param width: - :param height: :param duration: :param performer: :param title: :param parse_mode: + :param caption_entities: """ if not isinstance(audio, InputMedia): audio = InputMediaAudio(media=audio, thumb=thumb, caption=caption, - width=width, height=height, duration=duration, + duration=duration, performer=performer, title=title, - parse_mode=parse_mode) + parse_mode=parse_mode, + caption_entities=caption_entities) self.attach(audio) - def attach_document(self, document: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None, - caption: base.String = None, parse_mode: base.String = None): + def attach_document(self, document: typing.Union[InputMediaDocument, base.InputFile], + thumb: typing.Union[base.InputFile, base.String] = None, + caption: base.String = None, parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + disable_content_type_detection: typing.Optional[base.Boolean] = None): """ Attach document - - :param parse_mode: + + :param document: :param caption: :param thumb: - :param document: + :param parse_mode: + :param caption_entities: + :param disable_content_type_detection: """ if not isinstance(document, InputMedia): - document = InputMediaDocument(media=document, thumb=thumb, caption=caption, parse_mode=parse_mode) + document = InputMediaDocument(media=document, thumb=thumb, caption=caption, + parse_mode=parse_mode, caption_entities=caption_entities, + disable_content_type_detection=disable_content_type_detection) self.attach(document) def attach_photo(self, photo: typing.Union[InputMediaPhoto, base.InputFile], - caption: base.String = None): + caption: base.String = None, parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None): """ Attach photo :param photo: :param caption: + :param parse_mode: + :param caption_entities: """ if not isinstance(photo, InputMedia): - photo = InputMediaPhoto(media=photo, caption=caption) + photo = InputMediaPhoto(media=photo, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities) self.attach(photo) def attach_video(self, video: typing.Union[InputMediaVideo, 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): + width: base.Integer = None, height: base.Integer = None, + duration: base.Integer = None, parse_mode: base.String = None, + caption_entities: typing.Optional[typing.List[MessageEntity]] = None, + supports_streaming: base.Boolean = None): """ Attach video :param video: + :param thumb: :param caption: :param width: :param height: :param duration: + :param parse_mode: + :param caption_entities: + :param supports_streaming: """ if not isinstance(video, InputMedia): video = InputMediaVideo(media=video, thumb=thumb, caption=caption, - width=width, height=height, duration=duration) + width=width, height=height, duration=duration, + parse_mode=parse_mode, caption_entities=caption_entities, + supports_streaming=supports_streaming) self.attach(video) def to_python(self) -> typing.List: From 9bb3a5dccfb4511a700d670118cb3e9485e9f70e Mon Sep 17 00:00:00 2001 From: Andrew <11490628+andrew000@users.noreply.github.com> Date: Sun, 14 Mar 2021 18:14:40 +0200 Subject: [PATCH 067/118] ADD Exceptions: (#512) * CantRestrictChatOwner * UserIsAnAdministratorOfTheChat --- aiogram/utils/exceptions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index d38ff009..9a1606a6 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -494,6 +494,20 @@ class MethodIsNotAvailable(BadRequest): match = "Method is available only for supergroups" +class CantRestrictChatOwner(BadRequest): + """ + Raises when bot restricts the chat owner + """ + match = 'Can\'t remove chat owner' + + +class UserIsAnAdministratorOfTheChat(BadRequest): + """ + Raises when bot restricts the chat admin + """ + match = 'User is an administrator of the chat' + + class NotFound(TelegramAPIError, _MatchErrorMixin): __group = True From b2b7ddb6396fe6e415e4461648466cb7e8148861 Mon Sep 17 00:00:00 2001 From: Egor Date: Sun, 14 Mar 2021 19:20:37 +0300 Subject: [PATCH 068/118] feat: an example of intergration between externally created Application and dispatcher (#433) * feat: an example of intergration between externally created Application and dispatcher * fix: imports * chore: fix EOF * chore: fix comment --- examples/separate_api_route_example.py | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 examples/separate_api_route_example.py diff --git a/examples/separate_api_route_example.py b/examples/separate_api_route_example.py new file mode 100644 index 00000000..2f3cc61c --- /dev/null +++ b/examples/separate_api_route_example.py @@ -0,0 +1,34 @@ +# NOTE: This is an example of an integration between +# externally created Application object and the aiogram's dispatcher +# This can be used for a custom route, for instance + +from aiogram import Bot, Dispatcher, types +from aiogram.dispatcher.webhook import configure_app +from aiohttp import web + + +bot = Bot(token=config.bot_token) +dp = Dispatcher(bot) + + + +@dp.message_handler(commands=["start"]) +async def cmd_start(message: types.Message): + await message.reply("start!") + + +# handle /api route +async def api_handler(request): + return web.json_response({"status": "OK"}, status=200) + + +app = web.Application() +# add a custom route +app.add_routes([web.post('/api', api_handler)]) +# every request to /bot route will be retransmitted to dispatcher to be handled +# as a bot update +configure_app(dp, app, "/bot") + + +if __name__ == '__main__': + web.run_app(app, port=9000) From cd047e8d017d86e4d3fa926fd7e747d2396f4cef Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 14 Mar 2021 18:27:38 +0200 Subject: [PATCH 069/118] Removed deprecation warning from Message.send_copy --- aiogram/types/message.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 2e8402e7..7f083119 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -37,7 +37,6 @@ from .voice_chat_participants_invited import VoiceChatParticipantsInvited from .voice_chat_started import VoiceChatStarted from ..utils import helper from ..utils import markdown as md -from ..utils.deprecated import deprecated from ..utils.text_decorations import html_decoration, markdown_decoration @@ -2824,11 +2823,6 @@ class Message(base.TelegramObject): message_id=self.message_id, ) - @deprecated( - "This method deprecated since Bot API 4.5. Use method `copy_to` instead. \n" - "Read more: https://core.telegram.org/bots/api#copymessage", - stacklevel=3 - ) async def send_copy( self: Message, chat_id: typing.Union[str, int], From 76955bf8f1b0e67d6ada302ffd7c3ffe4d9c7a18 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 14 Mar 2021 20:17:40 +0200 Subject: [PATCH 070/118] Fixed optional arguments in new methods (API 5.1) --- aiogram/bot/bot.py | 10 +++++----- aiogram/types/chat.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 5db66758..85be7fd0 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1579,7 +1579,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): are considered to be banned forever. Applied for supergroups and channels only. :type until_date: :obj:`typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None` + datetime.timedelta, None]` :param revoke_messages: Pass True to delete all messages from the chat for the user that is being removed. If False, the user will be able to see @@ -1817,8 +1817,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def create_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String], expire_date: typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None], - member_limit: typing.Optional[base.Integer], + datetime.timedelta, None] = None, + member_limit: typing.Optional[base.Integer] = None, ) -> types.ChatInviteLink: """ Use this method to create an additional invite link for a chat. @@ -1853,8 +1853,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): chat_id: typing.Union[base.Integer, base.String], invite_link: base.String, expire_date: typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None], - member_limit: typing.Optional[base.Integer], + datetime.timedelta, None] = None, + member_limit: typing.Optional[base.Integer] = None, ) -> types.ChatInviteLink: """ Use this method to edit a non-primary invite link created by the bot. diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 957ee78b..b9e03983 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -210,7 +210,7 @@ class Chat(base.TelegramObject): are considered to be banned forever. Applied for supergroups and channels only. :type until_date: :obj:`typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None` + datetime.timedelta, None]` :param revoke_messages: Pass True to delete all messages from the chat for the user that is being removed. If False, the user will be able to see @@ -574,8 +574,8 @@ class Chat(base.TelegramObject): async def create_invite_link(self, expire_date: typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None], - member_limit: typing.Optional[base.Integer], + datetime.timedelta, None] = None, + member_limit: typing.Optional[base.Integer] = None, ) -> ChatInviteLink: """ Shortcut for createChatInviteLink method. """ return await self.bot.create_chat_invite_link( @@ -587,8 +587,8 @@ class Chat(base.TelegramObject): async def edit_invite_link(self, invite_link: base.String, expire_date: typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None], - member_limit: typing.Optional[base.Integer], + datetime.timedelta, None] = None, + member_limit: typing.Optional[base.Integer] = None, ) -> ChatInviteLink: """ Shortcut for editChatInviteLink method. """ return await self.bot.edit_chat_invite_link( From 6c423d2b92ee8fbbabd3a97ca3682624741422bc Mon Sep 17 00:00:00 2001 From: Stefan <67127306+MEDIOFF@users.noreply.github.com> Date: Sun, 14 Mar 2021 21:39:29 +0300 Subject: [PATCH 071/118] Update safe_split_text function, added split_separator param (#515) Co-authored-by: Stefan Vasilenko --- aiogram/utils/parts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/parts.py b/aiogram/utils/parts.py index e03f7bcc..b4bb9d67 100644 --- a/aiogram/utils/parts.py +++ b/aiogram/utils/parts.py @@ -15,12 +15,13 @@ def split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]: return [text[i:i + length] for i in range(0, len(text), length)] -def safe_split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]: +def safe_split_text(text: str, length: int = MAX_MESSAGE_LENGTH, split_separator: str = ' ') -> typing.List[str]: """ Split long text :param text: :param length: + :param split_separator :return: """ # TODO: More informative description @@ -30,7 +31,7 @@ def safe_split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[ while temp_text: if len(temp_text) > length: try: - split_pos = temp_text[:length].rindex(' ') + split_pos = temp_text[:length].rindex(split_separator) except ValueError: split_pos = length if split_pos < length // 4 * 3: From 8612a64b3c7b5c6c58c56a8fb9a6069627ffd017 Mon Sep 17 00:00:00 2001 From: monte-monte <6649967+monte-monte@users.noreply.github.com> Date: Sun, 14 Mar 2021 20:40:52 +0200 Subject: [PATCH 072/118] Update builtin.py (#510) * Update builtin.py In StateFilter check if object is CallbackQuery because it has different structure compared to simple message. This change prevents https://github.com/aiogram/aiogram/issues/508 bug. * Update aiogram/dispatcher/filters/builtin.py Co-authored-by: Alex Root Junior --- 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 ded3e9fd..762c8505 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -529,6 +529,8 @@ class StateFilter(BoundFilter): self.states = states def get_target(self, obj): + if isinstance(obj, CallbackQuery): + return getattr(getattr(getattr(obj, 'message', None),'chat', None), 'id', None), getattr(getattr(obj, 'from_user', None), 'id', None) return getattr(getattr(obj, 'chat', None), 'id', None), getattr(getattr(obj, 'from_user', None), 'id', None) async def check(self, obj): From cce3a5fbd0bba955e9d8bb1ac3bfb56243c8e9ea Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 14 Mar 2021 21:36:18 +0200 Subject: [PATCH 073/118] Bump badge --- README.md | 2 +- README.rst | 2 +- docs/source/index.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3c43b881..5205646d 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-5.0-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.1-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 09072c81..3ec899c2 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-5.0-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.1-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/docs/source/index.rst b/docs/source/index.rst index e08e8830..809e195e 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-5.0-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.1-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From 5715e4466f0d5a5ed6717540a51c1cc48cd09690 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Wed, 17 Mar 2021 15:28:42 +0300 Subject: [PATCH 074/118] Added alias to from_user field, fixed TypeError (#527) --- aiogram/types/chat_member_updated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/chat_member_updated.py b/aiogram/types/chat_member_updated.py index 7c6a124f..67c75616 100644 --- a/aiogram/types/chat_member_updated.py +++ b/aiogram/types/chat_member_updated.py @@ -15,7 +15,7 @@ class ChatMemberUpdated(base.TelegramObject): https://core.telegram.org/bots/api#chatmemberupdated """ chat: Chat = fields.Field(base=Chat) - from_user: User = fields.Field(base=User) + from_user: User = fields.Field(alias="from", base=User) date: datetime.datetime = fields.DateTimeField() old_chat_member: ChatMember = fields.Field(base=ChatMember) new_chat_member: ChatMember = fields.Field(base=ChatMember) From c153d681fb3b6af4dd1bde5e494cbd4f7497c3a8 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 20 Mar 2021 17:49:17 +0300 Subject: [PATCH 075/118] fix: add Chat.message_auto_delete_time field (#535) --- aiogram/types/chat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index b9e03983..0a444fd8 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -35,6 +35,7 @@ class Chat(base.TelegramObject): pinned_message: 'Message' = fields.Field(base='Message') permissions: ChatPermissions = fields.Field(base=ChatPermissions) slow_mode_delay: base.Integer = fields.Field() + message_auto_delete_time: base.Integer = fields.Field() sticker_set_name: base.String = fields.Field() can_set_sticker_set: base.Boolean = fields.Field() linked_chat_id: base.Integer = fields.Field() From 901270449f5a9a52a546da0eaba9b9d4bdee6d16 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Sat, 20 Mar 2021 20:23:30 +0300 Subject: [PATCH 076/118] Changed ChatType from CHANNEL to SUPERGROUP to avoid confusion (#533) Channels require a separate channel_post_handler, so this example is currently broken and confusing --- examples/chat_type_filter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/chat_type_filter.py b/examples/chat_type_filter.py index e57f8825..9db13cb8 100644 --- a/examples/chat_type_filter.py +++ b/examples/chat_type_filter.py @@ -19,12 +19,12 @@ bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) -@dp.message_handler(chat_type=[ChatType.PRIVATE, ChatType.CHANNEL]) +@dp.message_handler(chat_type=[ChatType.PRIVATE, ChatType.SUPERGROUP]) async def send_welcome(message: types.Message): """ - This handler will be called when user sends message in private chat or channel + This handler will be called when user sends message in private chat or supergroup """ - await message.reply("Hi!\nI'm hearing your messages in private chats and channels") + await message.reply("Hi!\nI'm hearing your messages in private chats and supergroups") # propagate message to the next handler raise SkipHandler From b287a9f5f525eb4e036775130ce287fcc56d3b31 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 20 Mar 2021 19:37:39 +0200 Subject: [PATCH 077/118] Update FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c4430ef6..d643b3f0 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ open_collective: aiogram +patreon: aiogram From 7cce8b63976d1d87461643d8d7f52433e57fa257 Mon Sep 17 00:00:00 2001 From: JWSong Date: Sun, 21 Mar 2021 02:51:32 +0900 Subject: [PATCH 078/118] Removed redundant definitions (#531) This closes #456. Co-authored-by: Jungwoo Song --- aiogram/bot/bot.py | 114 ++++++++++-------------------- aiogram/dispatcher/handler.py | 3 +- aiogram/dispatcher/webhook.py | 3 +- aiogram/utils/text_decorations.py | 3 +- 4 files changed, 41 insertions(+), 82 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 85be7fd0..4b6c4c0b 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -161,8 +161,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): files = {} prepare_file(payload, files, 'certificate', certificate) - result = await self.request(api.Methods.SET_WEBHOOK, payload, files) - return result + return await self.request(api.Methods.SET_WEBHOOK, payload, files) async def delete_webhook(self, drop_pending_updates: typing.Optional[base.Boolean] = None, @@ -181,8 +180,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_WEBHOOK, payload) - return result + return await self.request(api.Methods.DELETE_WEBHOOK, payload) async def get_webhook_info(self) -> types.WebhookInfo: """ @@ -232,8 +230,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.LOG_OUT, payload) - return result + return await self.request(api.Methods.LOG_OUT, payload) @deprecated("This method will be renamed to `close` in aiogram v3.0") async def close_bot(self) -> base.Boolean: @@ -251,8 +248,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.CLOSE, payload) - return result + return await self.request(api.Methods.CLOSE, payload) async def send_message(self, chat_id: typing.Union[base.Integer, base.String], @@ -1506,8 +1502,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SEND_CHAT_ACTION, payload) - return result + return await self.request(api.Methods.SEND_CHAT_ACTION, payload) async def get_user_profile_photos(self, user_id: base.Integer, offset: typing.Optional[base.Integer] = None, limit: typing.Optional[base.Integer] = None) -> types.UserProfilePhotos: @@ -1593,8 +1588,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): until_date = prepare_arg(until_date) payload = generate_payload(**locals()) - result = await self.request(api.Methods.KICK_CHAT_MEMBER, payload) - return result + return await self.request(api.Methods.KICK_CHAT_MEMBER, payload) async def unban_chat_member(self, chat_id: typing.Union[base.Integer, base.String], @@ -1627,8 +1621,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.UNBAN_CHAT_MEMBER, payload) - return result + return await self.request(api.Methods.UNBAN_CHAT_MEMBER, payload) async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer, @@ -1683,8 +1676,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): f"passing regular argument {payload[permission]}", DeprecationWarning, stacklevel=2) - result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload) - return result + return await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload) async def promote_chat_member(self, chat_id: typing.Union[base.Integer, base.String], @@ -1756,8 +1748,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload) - return result + return await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload) 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: @@ -1775,8 +1766,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload) - return result + return await self.request(api.Methods.SET_CHAT_ADMINISTRATOR_CUSTOM_TITLE, payload) async def set_chat_permissions(self, chat_id: typing.Union[base.Integer, base.String], permissions: types.ChatPermissions) -> base.Boolean: @@ -1794,8 +1784,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): permissions = prepare_arg(permissions) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_PERMISSIONS, payload) - return result + return await self.request(api.Methods.SET_CHAT_PERMISSIONS, payload) async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String: """ @@ -1811,8 +1800,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.EXPORT_CHAT_INVITE_LINK, payload) - return result + return await self.request(api.Methods.EXPORT_CHAT_INVITE_LINK, payload) async def create_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String], @@ -1846,8 +1834,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): expire_date = prepare_arg(expire_date) payload = generate_payload(**locals()) - result = await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload) - return result + return await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload) async def edit_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String], @@ -1883,8 +1870,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): expire_date = prepare_arg(expire_date) payload = generate_payload(**locals()) - result = await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload) - return result + return await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload) async def revoke_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String], @@ -1905,8 +1891,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload) - return result + return await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload) async def set_chat_photo(self, chat_id: typing.Union[base.Integer, base.String], photo: base.InputFile) -> base.Boolean: @@ -1931,8 +1916,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): files = {} prepare_file(payload, files, 'photo', photo) - result = await self.request(api.Methods.SET_CHAT_PHOTO, payload, files) - return result + return await self.request(api.Methods.SET_CHAT_PHOTO, payload, files) async def delete_chat_photo(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: """ @@ -1951,8 +1935,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_CHAT_PHOTO, payload) - return result + return await self.request(api.Methods.DELETE_CHAT_PHOTO, payload) async def set_chat_title(self, chat_id: typing.Union[base.Integer, base.String], title: base.String) -> base.Boolean: @@ -1974,8 +1957,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_TITLE, payload) - return result + return await self.request(api.Methods.SET_CHAT_TITLE, payload) async def set_chat_description(self, chat_id: typing.Union[base.Integer, base.String], description: typing.Optional[base.String] = None) -> base.Boolean: @@ -1994,8 +1976,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_DESCRIPTION, payload) - return result + return await self.request(api.Methods.SET_CHAT_DESCRIPTION, payload) async def pin_chat_message(self, chat_id: typing.Union[base.Integer, base.String], @@ -2027,8 +2008,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.PIN_CHAT_MESSAGE, payload) - return result + return await self.request(api.Methods.PIN_CHAT_MESSAGE, payload) async def unpin_chat_message(self, chat_id: typing.Union[base.Integer, base.String], @@ -2056,8 +2036,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.UNPIN_CHAT_MESSAGE, payload) - return result + return await self.request(api.Methods.UNPIN_CHAT_MESSAGE, payload) async def unpin_all_chat_messages(self, chat_id: typing.Union[base.Integer, base.String], @@ -2079,8 +2058,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.UNPIN_ALL_CHAT_MESSAGES, payload) - return result + return await self.request(api.Methods.UNPIN_ALL_CHAT_MESSAGES, payload) async def leave_chat(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: """ @@ -2095,8 +2073,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.LEAVE_CHAT, payload) - return result + return await self.request(api.Methods.LEAVE_CHAT, payload) async def get_chat(self, chat_id: typing.Union[base.Integer, base.String]) -> types.Chat: """ @@ -2148,8 +2125,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_CHAT_MEMBERS_COUNT, payload) - return result + return await self.request(api.Methods.GET_CHAT_MEMBERS_COUNT, payload) async def get_chat_member(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer) -> types.ChatMember: @@ -2190,8 +2166,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_STICKER_SET, payload) - return result + return await self.request(api.Methods.SET_CHAT_STICKER_SET, payload) async def delete_chat_sticker_set(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: """ @@ -2210,8 +2185,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_CHAT_STICKER_SET, payload) - return result + return await self.request(api.Methods.DELETE_CHAT_STICKER_SET, payload) async def answer_callback_query(self, callback_query_id: base.String, text: typing.Optional[base.String] = None, @@ -2245,8 +2219,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload) - return result + return await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload) async def set_my_commands(self, commands: typing.List[types.BotCommand]) -> base.Boolean: """ @@ -2263,8 +2236,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): commands = prepare_arg(commands) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_MY_COMMANDS, payload) - return result + return await self.request(api.Methods.SET_MY_COMMANDS, payload) async def get_my_commands(self) -> typing.List[types.BotCommand]: """ @@ -2510,8 +2482,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_MESSAGE, payload) - return result + return await self.request(api.Methods.DELETE_MESSAGE, payload) # === Stickers === # https://core.telegram.org/bots/api#stickers @@ -2652,8 +2623,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): 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 + return await self.request(api.Methods.CREATE_NEW_STICKER_SET, payload, files) async def add_sticker_to_set(self, user_id: base.Integer, @@ -2698,8 +2668,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): prepare_file(payload, files, 'png_sticker', png_sticker) prepare_file(payload, files, 'tgs_sticker', tgs_sticker) - result = await self.request(api.Methods.ADD_STICKER_TO_SET, payload, files) - return result + return await self.request(api.Methods.ADD_STICKER_TO_SET, payload, files) async def set_sticker_position_in_set(self, sticker: base.String, position: base.Integer) -> base.Boolean: """ @@ -2715,9 +2684,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_STICKER_POSITION_IN_SET, payload) - return result + return await self.request(api.Methods.SET_STICKER_POSITION_IN_SET, payload) async def delete_sticker_from_set(self, sticker: base.String) -> base.Boolean: """ @@ -2732,8 +2700,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_STICKER_FROM_SET, payload) - return result + return await self.request(api.Methods.DELETE_STICKER_FROM_SET, payload) async def set_sticker_set_thumb(self, name: base.String, @@ -2765,8 +2732,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): files = {} prepare_file(payload, files, 'thumb', thumb) - result = await self.request(api.Methods.SET_STICKER_SET_THUMB, payload, files) - return result + return await self.request(api.Methods.SET_STICKER_SET_THUMB, payload, files) async def answer_inline_query(self, inline_query_id: base.String, results: typing.List[types.InlineQueryResult], @@ -2809,8 +2775,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): results = prepare_arg(results) payload = generate_payload(**locals()) - result = await self.request(api.Methods.ANSWER_INLINE_QUERY, payload) - return result + return await self.request(api.Methods.ANSWER_INLINE_QUERY, payload) # === Payments === # https://core.telegram.org/bots/api#payments @@ -2958,8 +2923,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): for shipping_option in shipping_options]) payload = generate_payload(**locals()) - result = await self.request(api.Methods.ANSWER_SHIPPING_QUERY, payload) - return result + return await self.request(api.Methods.ANSWER_SHIPPING_QUERY, payload) async def answer_pre_checkout_query(self, pre_checkout_query_id: base.String, ok: base.Boolean, error_message: typing.Optional[base.String] = None) -> base.Boolean: @@ -2986,8 +2950,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.ANSWER_PRE_CHECKOUT_QUERY, payload) - return result + return await self.request(api.Methods.ANSWER_PRE_CHECKOUT_QUERY, payload) # === Games === # https://core.telegram.org/bots/api#games @@ -3018,8 +2981,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): errors = prepare_arg(errors) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_PASSPORT_DATA_ERRORS, payload) - return result + return await self.request(api.Methods.SET_PASSPORT_DATA_ERRORS, payload) # === Games === # https://core.telegram.org/bots/api#games diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 38219012..10a94924 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -25,8 +25,7 @@ class CancelHandler(Exception): def _get_spec(func: callable): while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks func = func.__wrapped__ - spec = inspect.getfullargspec(func) - return spec + return inspect.getfullargspec(func) def _check_spec(spec: inspect.FullArgSpec, kwargs: dict): diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index b6280ed1..52191870 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -116,8 +116,7 @@ class WebhookRequestHandler(web.View): :return: :class:`aiogram.types.Update` """ data = await self.request.json() - update = types.Update(**data) - return update + return types.Update(**data) async def post(self): """ diff --git a/aiogram/utils/text_decorations.py b/aiogram/utils/text_decorations.py index 4de8d69a..09484e3c 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -55,12 +55,11 @@ class TextDecoration(ABC): :param entities: Array of MessageEntities :return: """ - result = "".join( + return "".join( self._unparse_entities( self._add_surrogates(text), sorted(entities, key=lambda item: item.offset) if entities else [] ) ) - return result def _unparse_entities( self, From eed147e2e35a6186dad957207c42ed69b90eea29 Mon Sep 17 00:00:00 2001 From: Daniil Kovalenko <40635760+WhiteMemory99@users.noreply.github.com> Date: Sun, 21 Mar 2021 00:52:35 +0700 Subject: [PATCH 079/118] Add MediaGroup Filter (#528) * Add MediaGroupFilter * Update __init__.py * Register MediaGroupFilter * Update docs * Fix a typo --- aiogram/dispatcher/dispatcher.py | 8 +++++++- aiogram/dispatcher/filters/__init__.py | 3 ++- aiogram/dispatcher/filters/builtin.py | 17 +++++++++++++++++ docs/source/dispatcher/filters.rst | 8 ++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index a2736cf7..d471fe86 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -11,7 +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, ForwardedMessageFilter, \ - IsSenderContact, ChatTypeFilter, AbstractFilter + IsSenderContact, ChatTypeFilter, MediaGroupFilter, AbstractFilter from .handler import Handler from .middlewares import MiddlewareManager from .storage import BaseStorage, DELTA, DisabledStorage, EXCEEDED_COUNT, FSMContext, \ @@ -204,6 +204,12 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.my_chat_member_handlers, self.chat_member_handlers ]) + filters_factory.bind(MediaGroupFilter, 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 d64a2667..d07d953b 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,7 +1,7 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, \ Text, IDFilter, AdminFilter, IsReplyFilter, IsSenderContact, ForwardedMessageFilter, \ - ChatTypeFilter + ChatTypeFilter, MediaGroupFilter from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -25,6 +25,7 @@ __all__ = ( 'IsSenderContact', 'ForwardedMessageFilter', 'ChatTypeFilter', + 'MediaGroupFilter', 'FiltersFactory', 'AbstractFilter', 'BoundFilter', diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 762c8505..c32c53be 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -738,3 +738,20 @@ class ChatTypeFilter(BoundFilter): return False return obj.type in self.chat_type + + +class MediaGroupFilter(BoundFilter): + """ + Check if message is part of a media group. + + `is_media_group=True` - the message is part of a media group + `is_media_group=False` - the message is NOT part of a media group + """ + + key = "is_media_group" + + def __init__(self, is_media_group: bool): + self.is_media_group = is_media_group + + async def check(self, message: types.Message) -> bool: + return bool(getattr(message, "media_group_id")) is self.is_media_group diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index b8f4962e..a47e396e 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -155,6 +155,14 @@ ChatTypeFilter .. autoclass:: aiogram.dispatcher.filters.ChatTypeFilter :members: :show-inheritance: + + +MediaGroupFilter +------------- + +.. autoclass:: aiogram.dispatcher.filters.MediaGroupFilter + :members: + :show-inheritance: Making own filters (Custom filters) From ee0ceee70420caab5ce1345b925685a918e7c9d5 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 20 Mar 2021 20:57:07 +0300 Subject: [PATCH 080/118] feat: delete_message shortcut for Chat (#526) --- aiogram/types/chat.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 0a444fd8..3b0a7b9d 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -608,6 +608,15 @@ class Chat(base.TelegramObject): invite_link=invite_link, ) + async def delete_message(self, + message_id: base.Integer, + ) -> base.Boolean: + """ Shortcut for deleteMessage method. """ + return await self.bot.delete_message( + chat_id=self.id, + message_id=message_id, + ) + def __int__(self): return self.id From b0f2fe3ed496fba6645a0e9c2931282e9c72b937 Mon Sep 17 00:00:00 2001 From: Andrew <11490628+andrew000@users.noreply.github.com> Date: Sun, 21 Mar 2021 09:37:00 +0200 Subject: [PATCH 081/118] FIX TelegramObject.__setitem__ (#532) * FIX TelegramObject.__setitem__ Removed 'raise KeyError(key)' * Add warning and log in TelegramObject.__setitem__ When Telegram adds a new field -> Aiogram will warn about this. * Removed warnings.warn * Set logger to 'aiogram' * Removed 'f' before string --- aiogram/types/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 373f47be..0ed8579c 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -1,6 +1,7 @@ from __future__ import annotations import io +import logging import typing from typing import TypeVar @@ -26,6 +27,9 @@ Float = TypeVar('Float', bound=float) Boolean = TypeVar('Boolean', bound=bool) T = TypeVar('T') +# Main aiogram logger +log = logging.getLogger('aiogram') + class MetaTelegramObject(type): """ @@ -225,7 +229,9 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): if key in self.props: return self.props[key].set_value(self, value, self.conf.get('parent', None)) self.values[key] = value - raise KeyError(key) + + # Log warning when Telegram silently adds new Fields + log.warning("Field '%s' doesn't exist in %s", key, self.__class__) def __contains__(self, item: str) -> bool: """ From e2273e6c31aaf4eb70554239493547a13e486398 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 21 Mar 2021 10:41:03 +0300 Subject: [PATCH 082/118] new: add mime types parsing (#431) --- aiogram/types/document.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/aiogram/types/document.py b/aiogram/types/document.py index e15b745d..c3d19fa9 100644 --- a/aiogram/types/document.py +++ b/aiogram/types/document.py @@ -2,6 +2,7 @@ from . import base from . import fields from . import mixins from .photo_size import PhotoSize +from ..utils import helper class Document(base.TelegramObject, mixins.Downloadable): @@ -16,3 +17,34 @@ class Document(base.TelegramObject, mixins.Downloadable): file_name: base.String = fields.Field() mime_type: base.String = fields.Field() file_size: base.Integer = fields.Field() + + @property + def mime_base(self) -> str: + base_type, _, _ = self.mime_type.partition('/') + return base_type + + @property + def mime_subtype(self) -> str: + _, _, subtype = self.mime_type.partition('/') + return subtype + + +class MimeBase(helper.Helper): + """ + List of mime base types registered in IANA + + https://www.iana.org/assignments/media-types/media-types.xhtml + """ + + mode = helper.HelperMode.lowercase + + APPLICATION = helper.Item() # application + AUDIO = helper.Item() # audio + EXAMPLE = helper.Item() # example + FONT = helper.Item() # font + IMAGE = helper.Item() # image + MESSAGE = helper.Item() # message + MODEL = helper.Item() # model + MULTIPART = helper.Item() # multipart + TEXT = helper.Item() # text + VIDEO = helper.Item() # video From 15a6e94135f86777af1ff96a7e40c1e5923558e3 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 22 Mar 2021 22:01:44 +0200 Subject: [PATCH 083/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index ef832be9..590afc50 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.12' +__version__ = '2.12.1' __api_version__ = '5.1' From 79c59b34f9141427d221842662dcef2eb90a558f Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 27 Mar 2021 15:09:08 +0300 Subject: [PATCH 084/118] Fixed example description (#545) --- examples/text_filter_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/text_filter_example.py b/examples/text_filter_example.py index 60d631e3..7a90d6f0 100644 --- a/examples/text_filter_example.py +++ b/examples/text_filter_example.py @@ -18,7 +18,7 @@ bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) -# if the text from user in the list +# if the text is equal to any string in the list @dp.message_handler(text=['text1', 'text2']) async def text_in_handler(message: types.Message): await message.answer("The message text equals to one of in the list!") From 69e4ecc6061e22b121cbcd580268045aa39c0ffc Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 12 Apr 2021 00:32:40 +0300 Subject: [PATCH 085/118] #564: Added possibility to use `allowed_updates` argument in Polling mode --- aiogram/__init__.py | 2 +- aiogram/dispatcher/dispatcher.py | 14 +++++++++++--- aiogram/types/update.py | 8 ++++++++ aiogram/utils/executor.py | 21 ++++++++++++++++----- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 590afc50..455aebe8 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.12.1' +__version__ = '2.12.2' __api_version__ = '5.1' diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index d471fe86..8231c4f7 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -340,7 +340,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): limit=None, reset_webhook=None, fast: typing.Optional[bool] = True, - error_sleep: int = 5): + error_sleep: int = 5, + allowed_updates: typing.Optional[typing.List[str]] = None): """ Start long-polling @@ -349,6 +350,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): :param limit: :param reset_webhook: :param fast: + :param error_sleep: + :param allowed_updates: :return: """ if self._polling: @@ -377,10 +380,15 @@ class Dispatcher(DataMixin, ContextInstanceMixin): while self._polling: try: with self.bot.request_timeout(request_timeout): - updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout) + updates = await self.bot.get_updates( + limit=limit, + offset=offset, + timeout=timeout, + allowed_updates=allowed_updates + ) except asyncio.CancelledError: break - except: + except Exception as e: log.exception('Cause exception while getting updates.') await asyncio.sleep(error_sleep) continue diff --git a/aiogram/types/update.py b/aiogram/types/update.py index c8c4b58d..7cf616bb 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -1,5 +1,7 @@ from __future__ import annotations +from functools import lru_cache + from . import base from . import fields from .callback_query import CallbackQuery @@ -72,3 +74,9 @@ class AllowedUpdates(helper.Helper): "Use `CHOSEN_INLINE_RESULT`", new_value_getter=lambda cls: cls.CHOSEN_INLINE_RESULT, ) + + @classmethod + @lru_cache(1) + def default(cls): + excluded = cls.CHAT_MEMBER + cls.MY_CHAT_MEMBER + return list(filter(lambda item: item not in excluded, cls.all())) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 35107975..c74827b0 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -2,7 +2,7 @@ import asyncio import datetime import functools import secrets -from typing import Callable, Union, Optional, Any +from typing import Callable, Union, Optional, Any, List from warnings import warn from aiohttp import web @@ -23,7 +23,8 @@ def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None): def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True, - on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True): + on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True, + allowed_updates: Optional[List[str]] = None): """ Start bot in long-polling mode @@ -34,11 +35,20 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr :param on_startup: :param on_shutdown: :param timeout: + :param relax: + :param fast: + :param allowed_updates: """ executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) _setup_callbacks(executor, on_startup, on_shutdown) - executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast) + executor.start_polling( + reset_webhook=reset_webhook, + timeout=timeout, + relax=relax, + fast=fast, + allowed_updates=allowed_updates + ) def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -295,7 +305,8 @@ class Executor: self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name) self.run_app(**kwargs) - def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True): + def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True, + allowed_updates: Optional[List[str]] = None): """ Start bot in long-polling mode @@ -308,7 +319,7 @@ class Executor: try: loop.run_until_complete(self._startup_polling()) loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout, - relax=relax, fast=fast)) + relax=relax, fast=fast, allowed_updates=allowed_updates)) loop.run_forever() except (KeyboardInterrupt, SystemExit): # loop.stop() From df294e579f104e2ae7e9f37b0c69490782d33091 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Mon, 12 Apr 2021 10:24:13 +0300 Subject: [PATCH 086/118] Replace deprecated 'is_private' method (#553) --- 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 7f083119..d0aed602 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -267,7 +267,8 @@ class Message(base.TelegramObject): :return: str """ - if ChatType.is_private(self.chat): + + if self.chat.type == ChatType.PRIVATE: raise TypeError("Invalid chat type!") url = "https://t.me/" if self.chat.username: From cce29ba532b4956557dead60edf14e5d0e5340e0 Mon Sep 17 00:00:00 2001 From: nthacks Date: Mon, 19 Apr 2021 03:59:07 +0530 Subject: [PATCH 087/118] Update documented caption limits to the current limit. (#565) --- aiogram/dispatcher/webhook.py | 10 +++++----- aiogram/types/message.py | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 52191870..bc21e22c 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -619,7 +619,7 @@ class SendPhoto(BaseResponse, ReplyToMixin, DisableNotificationMixin): a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. :param caption: String (Optional) - Photo caption (may also be used when resending photos by file_id), - 0-200 characters + 0-1024 characters after entities parsing :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message @@ -672,7 +672,7 @@ class SendAudio(BaseResponse, ReplyToMixin, DisableNotificationMixin): to send an audio file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an audio file from the Internet, or upload a new one using multipart/form-data. - :param caption: String (Optional) - Audio caption, 0-200 characters + :param caption: String (Optional) - Audio caption, 0-1024 characters after entities parsing :param duration: Integer (Optional) - Duration of the audio in seconds :param performer: String (Optional) - Performer :param title: String (Optional) - Track name @@ -731,7 +731,7 @@ class SendDocument(BaseResponse, ReplyToMixin, DisableNotificationMixin): as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :param caption: String (Optional) - Document caption - (may also be used when resending documents by file_id), 0-200 characters + (may also be used when resending documents by file_id), 0-1024 characters after entities parsing :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message @@ -788,7 +788,7 @@ class SendVideo(BaseResponse, ReplyToMixin, DisableNotificationMixin): :param width: Integer (Optional) - Video width :param height: Integer (Optional) - Video height :param caption: String (Optional) - Video caption (may also be used when resending videos by file_id), - 0-200 characters + 0-1024 characters after entities parsing :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. :param reply_to_message_id: Integer (Optional) - If the message is a reply, ID of the original message @@ -845,7 +845,7 @@ class SendVoice(BaseResponse, ReplyToMixin, DisableNotificationMixin): to send a file that exists on the Telegram servers (recommended), 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. - :param caption: String (Optional) - Voice message caption, 0-200 characters + :param caption: String (Optional) - Voice message caption, 0-1024 characters after entities parsing :param duration: Integer (Optional) - Duration of the voice message in seconds :param disable_notification: Boolean (Optional) - Sends the message silently. Users will receive a notification with no sound. diff --git a/aiogram/types/message.py b/aiogram/types/message.py index d0aed602..c9cc3945 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -462,7 +462,7 @@ class Message(base.TelegramObject): :param audio: Audio file to send. :type audio: :obj:`typing.Union[base.InputFile, base.String]` - :param caption: Audio caption, 0-200 characters + :param caption: Audio caption, 0-1024 characters after entities parsing :type caption: :obj:`typing.Optional[base.String]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -739,7 +739,7 @@ class Message(base.TelegramObject): A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` - :param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing :type caption: :obj:`typing.Optional[base.String]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -818,7 +818,7 @@ class Message(base.TelegramObject): :param voice: Audio file to send. :type voice: :obj:`typing.Union[base.InputFile, base.String]` - :param caption: Voice message caption, 0-200 characters + :param caption: Voice message caption, 0-1024 characters after entities parsing :type caption: :obj:`typing.Optional[base.String]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -1603,7 +1603,7 @@ class Message(base.TelegramObject): :param audio: Audio file to send. :type audio: :obj:`typing.Union[base.InputFile, base.String]` - :param caption: Audio caption, 0-200 characters + :param caption: Audio caption, 0-1024 characters after entities parsing :type caption: :obj:`typing.Optional[base.String]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -1880,7 +1880,7 @@ class Message(base.TelegramObject): A thumbnailโ€˜s width and height should not exceed 320. :type thumb: :obj:`typing.Union[base.InputFile, base.String, None]` - :param caption: Video caption (may also be used when resending videos by file_id), 0-200 characters + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing :type caption: :obj:`typing.Optional[base.String]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, @@ -1959,7 +1959,7 @@ class Message(base.TelegramObject): :param voice: Audio file to send. :type voice: :obj:`typing.Union[base.InputFile, base.String]` - :param caption: Voice message caption, 0-200 characters + :param caption: Voice message caption, 0-1024 characters after entities parsing :type caption: :obj:`typing.Optional[base.String]` :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, From ea28e2a77a80658341bad52cd37d2750b551fd82 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 28 Apr 2021 01:22:57 +0300 Subject: [PATCH 088/118] Telegram API 5.2 support (#572) * feat: version number update * feat: add InputInvoiceMessageContent type * refactor: every param on a new line * feat: add `max_tip_amount` and `suggested_tip_amounts` to `sendInvoice` * feat: `start_parameter` of `sendInvoice` became optional * refactor: reorder params * feat: add `chat_type` to `InlineQuery` * feat: add `VoiceChatScheduled` * feat: add `voice_chat_scheduled` to `Message` * fix: sendChatAction documentation update * feat: add `record_voice` and `upload_voice` to `ChatActions` * feat: allow sending invoices to group, supergroup and channel --- README.md | 2 +- README.rst | 2 +- aiogram/__init__.py | 4 +- aiogram/bot/api.py | 2 +- aiogram/bot/bot.py | 87 ++++++++++++++++++++------ aiogram/types/__init__.py | 5 +- aiogram/types/chat.py | 22 +++++++ aiogram/types/inline_query.py | 3 +- aiogram/types/input_message_content.py | 62 ++++++++++++++++++ aiogram/types/message.py | 5 ++ aiogram/types/voice_chat_scheduled.py | 15 +++++ docs/source/index.rst | 2 +- 12 files changed, 185 insertions(+), 26 deletions(-) create mode 100644 aiogram/types/voice_chat_scheduled.py diff --git a/README.md b/README.md index 5205646d..ae44c524 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-5.1-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.2-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 3ec899c2..caf6149c 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-5.1-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.2-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 455aebe8..a77ecdc0 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.12.2' -__api_version__ = '5.1' +__version__ = '2.13.0' +__api_version__ = '5.2' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index e3d3bf9a..38cbee89 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -189,7 +189,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 5.1 + List is updated to Bot API 5.2 """ mode = HelperMode.lowerCamelCase diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 4b6c4c0b..0c72c050 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1484,19 +1484,36 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String], 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 - (when a message arrives from your bot, Telegram clients clear its typing status). + 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 (when a message arrives from your bot, Telegram clients + clear its typing status). Returns True on success. - We only recommend using this method when a response from the bot will take - a noticeable amount of time to arrive. + Example: The ImageBot needs some time to process a request and + upload the image. Instead of sending a text message along the + lines of โ€œRetrieving image, please waitโ€ฆโ€, the bot may use + sendChatAction with action = upload_photo. The user will see a + โ€œsending photoโ€ status for the bot. + + We only recommend using this method when a response from the bot + will take a noticeable amount of time to arrive. Source: https://core.telegram.org/bots/api#sendchataction - :param chat_id: Unique identifier for the target chat or username of the target channel + :param chat_id: Unique identifier for the target chat or + username of the target channel (in the format + @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` - :param action: Type of action to broadcast + + :param action: Type of action to broadcast. Choose one, + depending on what the user is about to receive: `typing` for + text messages, `upload_photo` for photos, `record_video` or + `upload_video` for videos, `record_voice` or `upload_voice` + for voice notes, `upload_document` for general files, + `find_location` for location data, `record_video_note` or + `upload_video_note` for video notes. :type action: :obj:`base.String` + :return: Returns True on success :rtype: :obj:`base.Boolean` """ @@ -2780,10 +2797,19 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): # === Payments === # https://core.telegram.org/bots/api#payments - async def send_invoice(self, chat_id: base.Integer, title: base.String, - description: base.String, payload: base.String, - provider_token: base.String, start_parameter: base.String, - currency: base.String, prices: typing.List[types.LabeledPrice], + async def send_invoice(self, + chat_id: typing.Union[base.Integer, base.String], + title: base.String, + description: base.String, + payload: base.String, + provider_token: base.String, + currency: base.String, + prices: typing.List[types.LabeledPrice], + max_tip_amount: typing.Optional[base.Integer] = None, + suggested_tip_amounts: typing.Optional[ + typing.List[base.Integer] + ] = None, + start_parameter: typing.Optional[base.String] = None, provider_data: typing.Optional[typing.Dict] = None, photo_url: typing.Optional[base.String] = None, photo_size: typing.Optional[base.Integer] = None, @@ -2799,14 +2825,17 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): disable_notification: typing.Optional[base.Boolean] = None, reply_to_message_id: typing.Optional[base.Integer] = None, allow_sending_without_reply: typing.Optional[base.Boolean] = None, - reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None) -> types.Message: + reply_markup: typing.Optional[types.InlineKeyboardMarkup] = None, + ) -> types.Message: """ Use this method to send invoices. Source: https://core.telegram.org/bots/api#sendinvoice - :param chat_id: Unique identifier for the target private chat - :type chat_id: :obj:`base.Integer` + :param chat_id: Unique identifier for the target chat or + username of the target channel (in the format + @channelusername) + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` :param title: Product name, 1-32 characters :type title: :obj:`base.String` @@ -2821,10 +2850,6 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param provider_token: Payments provider token, obtained via Botfather :type provider_token: :obj:`base.String` - :param start_parameter: Unique deep-linking parameter that can be used to generate this - invoice when used as a start parameter - :type start_parameter: :obj:`base.String` - :param currency: Three-letter ISO 4217 currency code, see more on currencies :type currency: :obj:`base.String` @@ -2832,6 +2857,32 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) :type prices: :obj:`typing.List[types.LabeledPrice]` + :param max_tip_amount: The maximum accepted amount for tips in + the smallest units of the currency (integer, not + float/double). For example, for a maximum tip of US$ 1.45 + pass max_tip_amount = 145. See the exp parameter in + currencies.json, it shows the number of digits past the + decimal point for each currency (2 for the majority of + currencies). Defaults to 0 + :type max_tip_amount: :obj:`typing.Optional[base.Integer]` + + :param suggested_tip_amounts: A JSON-serialized array of suggested + amounts of tips in the smallest units of the currency + (integer, not float/double). At most 4 suggested tip amounts + can be specified. The suggested tip amounts must be + positive, passed in a strictly increased order and must not + exceed max_tip_amount. + :type suggested_tip_amounts: :obj:`typing.Optional[typing.List[base.Integer]]` + + :param start_parameter: Unique deep-linking parameter. If left + empty, forwarded copies of the sent message will have a Pay + button, allowing multiple users to pay directly from the + forwarded message, using the same invoice. If non-empty, + forwarded copies of the sent message will have a URL button + with a deep link to the bot (instead of a Pay button), with + the value used as the start parameter + :type start_parameter: :obj:`typing.Optional[base.String]` + :param provider_data: JSON-encoded data about the invoice, which will be shared with the payment provider :type provider_data: :obj:`typing.Optional[typing.Dict]` diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 1dfa519f..90909e81 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -35,7 +35,7 @@ from .input_file import InputFile from .input_media import InputMedia, InputMediaAnimation, InputMediaAudio, InputMediaDocument, InputMediaPhoto, \ InputMediaVideo, MediaGroup from .input_message_content import InputContactMessageContent, InputLocationMessageContent, InputMessageContent, \ - InputTextMessageContent, InputVenueMessageContent + InputTextMessageContent, InputVenueMessageContent, InputInvoiceMessageContent from .invoice import Invoice from .labeled_price import LabeledPrice from .location import Location @@ -72,6 +72,7 @@ from .video_note import VideoNote from .voice import Voice from .voice_chat_ended import VoiceChatEnded from .voice_chat_participants_invited import VoiceChatParticipantsInvited +from .voice_chat_scheduled import VoiceChatScheduled from .voice_chat_started import VoiceChatStarted from .webhook_info import WebhookInfo @@ -131,6 +132,7 @@ __all__ = ( 'InlineQueryResultVideo', 'InlineQueryResultVoice', 'InputContactMessageContent', + 'InputInvoiceMessageContent', 'InputFile', 'InputLocationMessageContent', 'InputMedia', @@ -191,6 +193,7 @@ __all__ = ( 'Voice', 'VoiceChatEnded', 'VoiceChatParticipantsInvited', + 'VoiceChatScheduled', 'VoiceChatStarted', 'WebhookInfo', 'base', diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 3b0a7b9d..5b3b315a 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -732,6 +732,8 @@ class ChatActions(helper.Helper): UPLOAD_VIDEO: str = helper.Item() # upload_video RECORD_AUDIO: str = helper.Item() # record_audio UPLOAD_AUDIO: str = helper.Item() # upload_audio + RECORD_VOICE: str = helper.Item() # record_voice + UPLOAD_VOICE: str = helper.Item() # upload_voice UPLOAD_DOCUMENT: str = helper.Item() # upload_document FIND_LOCATION: str = helper.Item() # find_location RECORD_VIDEO_NOTE: str = helper.Item() # record_video_note @@ -817,6 +819,26 @@ class ChatActions(helper.Helper): """ await cls._do(cls.UPLOAD_AUDIO, sleep) + @classmethod + async def record_voice(cls, sleep=None): + """ + Do record voice + + :param sleep: sleep timeout + :return: + """ + await cls._do(cls.RECORD_VOICE, sleep) + + @classmethod + async def upload_voice(cls, sleep=None): + """ + Do upload voice + + :param sleep: sleep timeout + :return: + """ + await cls._do(cls.UPLOAD_VOICE, sleep) + @classmethod async def upload_document(cls, sleep=None): """ diff --git a/aiogram/types/inline_query.py b/aiogram/types/inline_query.py index 436c11b0..63f4ab32 100644 --- a/aiogram/types/inline_query.py +++ b/aiogram/types/inline_query.py @@ -17,9 +17,10 @@ class InlineQuery(base.TelegramObject): """ id: base.String = fields.Field() from_user: User = fields.Field(alias='from', base=User) - location: Location = fields.Field(base=Location) query: base.String = fields.Field() offset: base.String = fields.Field() + chat_type: base.String = fields.Field() + location: Location = fields.Field(base=Location) async def answer(self, results: typing.List[InlineQueryResult], diff --git a/aiogram/types/input_message_content.py b/aiogram/types/input_message_content.py index 0008a2ee..f0c452cd 100644 --- a/aiogram/types/input_message_content.py +++ b/aiogram/types/input_message_content.py @@ -3,6 +3,8 @@ import typing from . import base from . import fields from .message_entity import MessageEntity +from .labeled_price import LabeledPrice +from ..utils.payload import generate_payload class InputMessageContent(base.TelegramObject): @@ -44,6 +46,66 @@ class InputContactMessageContent(InputMessageContent): ) +class InputInvoiceMessageContent(InputMessageContent): + """ + Represents the content of an invoice message to be sent as the + result of an inline query. + + https://core.telegram.org/bots/api#inputinvoicemessagecontent + """ + + title: base.String = fields.Field() + description: base.String = fields.Field() + payload: base.String = fields.Field() + provider_token: base.String = fields.Field() + currency: base.String = fields.Field() + prices: typing.List[LabeledPrice] = fields.ListField(base=LabeledPrice) + max_tip_amount: typing.Optional[base.Integer] = fields.Field() + suggested_tip_amounts: typing.Optional[ + typing.List[base.Integer] + ] = fields.ListField(base=base.Integer) + provider_data: typing.Optional[base.String] = fields.Field() + photo_url: typing.Optional[base.String] = fields.Field() + photo_size: typing.Optional[base.Integer] = fields.Field() + photo_width: typing.Optional[base.Integer] = fields.Field() + photo_height: typing.Optional[base.Integer] = fields.Field() + need_name: typing.Optional[base.Boolean] = fields.Field() + need_phone_number: typing.Optional[base.Boolean] = fields.Field() + need_email: typing.Optional[base.Boolean] = fields.Field() + need_shipping_address: typing.Optional[base.Boolean] = fields.Field() + send_phone_number_to_provider: typing.Optional[base.Boolean] = fields.Field() + send_email_to_provider: typing.Optional[base.Boolean] = fields.Field() + is_flexible: typing.Optional[base.Boolean] = fields.Field() + + def __init__( + self, + title: base.String, + description: base.String, + payload: base.String, + provider_token: base.String, + currency: base.String, + prices: typing.List[LabeledPrice] = None, + max_tip_amount: typing.Optional[base.Integer] = None, + suggested_tip_amounts: typing.Optional[typing.List[base.Integer]] = None, + provider_data: typing.Optional[base.String] = None, + photo_url: typing.Optional[base.String] = None, + photo_size: typing.Optional[base.Integer] = None, + photo_width: typing.Optional[base.Integer] = None, + photo_height: typing.Optional[base.Integer] = None, + need_name: typing.Optional[base.Boolean] = None, + need_phone_number: typing.Optional[base.Boolean] = None, + need_email: typing.Optional[base.Boolean] = None, + need_shipping_address: typing.Optional[base.Boolean] = None, + send_phone_number_to_provider: typing.Optional[base.Boolean] = None, + send_email_to_provider: typing.Optional[base.Boolean] = None, + is_flexible: typing.Optional[base.Boolean] = None, + ): + if prices is None: + prices = [] + payload = generate_payload(**locals()) + super().__init__(**payload) + + class InputLocationMessageContent(InputMessageContent): """ Represents the content of a location message to be sent as the result of an inline query. diff --git a/aiogram/types/message.py b/aiogram/types/message.py index c9cc3945..ce8395d2 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -34,6 +34,7 @@ from .video_note import VideoNote from .voice import Voice from .voice_chat_ended import VoiceChatEnded from .voice_chat_participants_invited import VoiceChatParticipantsInvited +from .voice_chat_scheduled import VoiceChatScheduled from .voice_chat_started import VoiceChatStarted from ..utils import helper from ..utils import markdown as md @@ -98,6 +99,7 @@ class Message(base.TelegramObject): connected_website: base.String = fields.Field() passport_data: PassportData = fields.Field(base=PassportData) proximity_alert_triggered: ProximityAlertTriggered = fields.Field(base=ProximityAlertTriggered) + voice_chat_scheduled: VoiceChatScheduled = fields.Field(base=VoiceChatScheduled) voice_chat_started: VoiceChatStarted = fields.Field(base=VoiceChatStarted) voice_chat_ended: VoiceChatEnded = fields.Field(base=VoiceChatEnded) voice_chat_participants_invited: VoiceChatParticipantsInvited = fields.Field(base=VoiceChatParticipantsInvited) @@ -166,6 +168,8 @@ class Message(base.TelegramObject): return ContentType.PASSPORT_DATA if self.proximity_alert_triggered: return ContentType.PROXIMITY_ALERT_TRIGGERED + if self.voice_chat_scheduled: + return ContentType.VOICE_CHAT_SCHEDULED if self.voice_chat_started: return ContentType.VOICE_CHAT_STARTED if self.voice_chat_ended: @@ -3033,6 +3037,7 @@ class ContentType(helper.Helper): GROUP_CHAT_CREATED = helper.Item() # group_chat_created PASSPORT_DATA = helper.Item() # passport_data PROXIMITY_ALERT_TRIGGERED = helper.Item() # proximity_alert_triggered + VOICE_CHAT_SCHEDULED = helper.Item() # voice_chat_scheduled VOICE_CHAT_STARTED = helper.Item() # voice_chat_started VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited diff --git a/aiogram/types/voice_chat_scheduled.py b/aiogram/types/voice_chat_scheduled.py new file mode 100644 index 00000000..c134eb0f --- /dev/null +++ b/aiogram/types/voice_chat_scheduled.py @@ -0,0 +1,15 @@ +from datetime import datetime + +from . import base +from . import fields +from .user import User + + +class VoiceChatScheduled(base.TelegramObject): + """ + This object represents a service message about a voice chat scheduled in the chat. + + https://core.telegram.org/bots/api#voicechatscheduled + """ + + start_date: datetime = fields.DateTimeField() diff --git a/docs/source/index.rst b/docs/source/index.rst index 809e195e..3631b150 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-5.1-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.2-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API From 08f0635afe07e0b9855f05526924af93ba45dd20 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 28 Apr 2021 01:24:34 +0300 Subject: [PATCH 089/118] Deep linking util fix (#569) * fix: deep linking util fixed and refactored * fix: wrong payload split * feat: check payload length --- aiogram/utils/deep_linking.py | 131 +++++++++++++++++--------- tests/test_utils/test_deep_linking.py | 37 +++++--- 2 files changed, 110 insertions(+), 58 deletions(-) diff --git a/aiogram/utils/deep_linking.py b/aiogram/utils/deep_linking.py index acb105da..e8035d9a 100644 --- a/aiogram/utils/deep_linking.py +++ b/aiogram/utils/deep_linking.py @@ -1,10 +1,10 @@ """ 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. +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 @@ -16,86 +16,123 @@ Basic link example: .. 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' + link = await get_start_link('foo') + + # result: 'https://t.me/MyBot?start=foo' Encoded link example: .. 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 aiogram.utils.deep_linking import get_start_link + + link = await get_start_link('foo', encode=True) + # result: 'https://t.me/MyBot?start=Zm9v' + +Decode it back example: + .. code-block:: python + + from aiogram.utils.deep_linking import decode_payload + from aiogram.types import Message + + @dp.message_handler(commands=["start"]) + async def handler(message: Message): + args = message.get_args() + payload = decode_payload(args) + await message.answer(f"Your payload: {payload}") """ +import re +from base64 import urlsafe_b64decode, urlsafe_b64encode + +from ..bot import Bot + +BAD_PATTERN = re.compile(r"[^_A-z0-9-]") 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 + 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) + return await _create_link( + link_type="start", + payload=payload, + encode=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 + 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) + return await _create_link( + link_type="startgroup", + payload=payload, + encode=encode, + ) async def _create_link(link_type, payload: str, encode=False): + """ + Create deep link. + + :param link_type: `start` or `startgroup` + :param payload: any string-convertible data + :param encode: pass True to encode the payload + :return: deeplink + """ 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.') + if encode: + payload = encode_payload(payload) + + if re.search(BAD_PATTERN, payload): + message = ( + "Wrong payload! Only A-Z, a-z, 0-9, _ and - are allowed. " + "Pass `encode=True` or encode payload manually." + ) raise ValueError(message) - return payload + if len(payload) > 64: + message = "Payload must be up to 64 characters long." + raise ValueError(message) + + return f"https://t.me/{bot.username}?{link_type}={payload}" + + +def encode_payload(payload: str) -> str: + """Encode payload with URL-safe base64url.""" + payload = str(payload) + bytes_payload: bytes = urlsafe_b64encode(payload.encode()) + str_payload = bytes_payload.decode() + return str_payload.replace("=", "") + + +def decode_payload(payload: str) -> str: + """Decode payload with URL-safe base64url.""" + payload += "=" * (4 - len(payload) % 4) + result: bytes = urlsafe_b64decode(payload) + return result.decode() async def _get_bot_user(): - """ Get current user of bot. """ - from ..bot import Bot + """Get current user of bot.""" bot = Bot.get_current() return await bot.me diff --git a/tests/test_utils/test_deep_linking.py b/tests/test_utils/test_deep_linking.py index a1d01e4e..f4aa14f1 100644 --- a/tests/test_utils/test_deep_linking.py +++ b/tests/test_utils/test_deep_linking.py @@ -1,7 +1,11 @@ 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 aiogram.utils.deep_linking import ( + decode_payload, + encode_payload, + get_start_link, + get_startgroup_link, +) from tests.types import dataset # enable asyncio mode @@ -17,9 +21,11 @@ PAYLOADS = [ WRONG_PAYLOADS = [ '@BotFather', + "Some:special$characters#=", 'spaces spaces spaces', 1234567890123456789.0, ] +USERNAME = dataset.USER["username"] @pytest.fixture(params=PAYLOADS, name='payload') @@ -47,7 +53,7 @@ def get_bot_user_fixture(monkeypatch): 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}' + assert link == f'https://t.me/{USERNAME}?start={payload}' async def test_wrong_symbols(self, wrong_payload): with pytest.raises(ValueError): @@ -55,20 +61,29 @@ class TestDeepLinking: 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}' + assert link == f'https://t.me/{USERNAME}?startgroup={payload}' async def test_filter_encode_and_decode(self, payload): - _payload = filter_payload(payload) - encoded = encode_payload(_payload) + encoded = encode_payload(payload) decoded = decode_payload(encoded) assert decoded == str(payload) - async def test_get_start_link_with_encoding(self, payload): + async def test_get_start_link_with_encoding(self, wrong_payload): # define link - link = await get_start_link(payload, encode=True) + link = await get_start_link(wrong_payload, encode=True) # define reference link - payload = filter_payload(payload) - encoded_payload = encode_payload(payload) + encoded_payload = encode_payload(wrong_payload) - assert link == f'https://t.me/{dataset.USER["username"]}?start={encoded_payload}' + assert link == f'https://t.me/{USERNAME}?start={encoded_payload}' + + async def test_64_len_payload(self): + payload = "p" * 64 + link = await get_start_link(payload) + assert link + + async def test_too_long_payload(self): + payload = "p" * 65 + print(payload, len(payload)) + with pytest.raises(ValueError): + await get_start_link(payload) From 75e88f173ced1f29a4153c1f6f1446c7fb2787ee Mon Sep 17 00:00:00 2001 From: darksidecat <58224121+darksidecat@users.noreply.github.com> Date: Wed, 28 Apr 2021 01:25:31 +0300 Subject: [PATCH 090/118] Closes #548 (#549) Bot.create_chat_invite_link() Bot.edit_chat_invite_link() Bot.revoke_chat_invite_link() need to return types.ChatInviteLink, not dict --- aiogram/bot/bot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 0c72c050..68f4ff27 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1851,7 +1851,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): expire_date = prepare_arg(expire_date) payload = generate_payload(**locals()) - return await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload) + result = await self.request(api.Methods.CREATE_CHAT_INVITE_LINK, payload) + return types.ChatInviteLink(**result) async def edit_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String], @@ -1887,7 +1888,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): expire_date = prepare_arg(expire_date) payload = generate_payload(**locals()) - return await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload) + result = await self.request(api.Methods.EDIT_CHAT_INVITE_LINK, payload) + return types.ChatInviteLink(**result) async def revoke_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String], @@ -1908,7 +1910,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - return await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload) + result = await self.request(api.Methods.REVOKE_CHAT_INVITE_LINK, payload) + return types.ChatInviteLink(**result) async def set_chat_photo(self, chat_id: typing.Union[base.Integer, base.String], photo: base.InputFile) -> base.Boolean: From ba095f0b9f1d0aa61538815b1babd4066205514f Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 28 Apr 2021 01:28:34 +0300 Subject: [PATCH 091/118] i18n get_locale without User (#546) * fix: #544 return locale None if User is absent * fix: #544 fixed typing * fix: #544 User is Optional * style: minor docs styling * fix: explicit None return + typing * fix: typing --- aiogram/contrib/middlewares/i18n.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index bb6d8003..651b77de 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -1,7 +1,7 @@ import gettext import os from contextvars import ContextVar -from typing import Any, Dict, Tuple +from typing import Any, Dict, Tuple, Optional from babel import Locale from babel.support import LazyProxy @@ -119,22 +119,24 @@ class I18nMiddleware(BaseMiddleware): return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache) # noinspection PyMethodMayBeStatic,PyUnusedLocal - async def get_user_locale(self, action: str, args: Tuple[Any]) -> str: + async def get_user_locale(self, action: str, args: Tuple[Any]) -> Optional[str]: """ User locale getter - You can override the method if you want to use different way of getting user language. + You can override the method if you want to use different way of + getting user language. :param action: event name :param args: event arguments - :return: locale name + :return: locale name or None """ - user: types.User = types.User.get_current() - locale: Locale = user.locale + user: Optional[types.User] = types.User.get_current() + locale: Optional[Locale] = user.locale if user else None if locale: *_, data = args language = data['locale'] = locale.language return language + return None async def trigger(self, action, args): """ From 4120408aa314bd53306805da508bd292323c3706 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 28 Apr 2021 01:28:53 +0300 Subject: [PATCH 092/118] Set state via storage (#542) * refactor: simplified check_address (removed redundant states check) * refactor: FSM resolve_state become public, removed redundant elif * fix: resolve `filters.State` on `set_state` * refactor: moved state resolution to storage * fix: return default state on get_state --- aiogram/contrib/fsm_storage/memory.py | 4 +-- aiogram/contrib/fsm_storage/mongo.py | 15 +++++--- aiogram/contrib/fsm_storage/redis.py | 11 +++--- aiogram/contrib/fsm_storage/rethinkdb.py | 13 +++++-- aiogram/dispatcher/storage.py | 44 ++++++++++++++---------- 5 files changed, 54 insertions(+), 33 deletions(-) diff --git a/aiogram/contrib/fsm_storage/memory.py b/aiogram/contrib/fsm_storage/memory.py index 2940f3fa..e1d6bdc0 100644 --- a/aiogram/contrib/fsm_storage/memory.py +++ b/aiogram/contrib/fsm_storage/memory.py @@ -35,7 +35,7 @@ class MemoryStorage(BaseStorage): user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Optional[str]: chat, user = self.resolve_address(chat=chat, user=user) - return self.data[chat][user]['state'] + return self.data[chat][user].get("state", self.resolve_state(default)) async def get_data(self, *, chat: typing.Union[str, int, None] = None, @@ -58,7 +58,7 @@ class MemoryStorage(BaseStorage): user: typing.Union[str, int, None] = None, state: typing.AnyStr = None): chat, user = self.resolve_address(chat=chat, user=user) - self.data[chat][user]['state'] = state + self.data[chat][user]['state'] = self.resolve_state(state) async def set_data(self, *, chat: typing.Union[str, int, None] = None, diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index f810a3eb..992e2e70 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -65,7 +65,7 @@ class MongoStorage(BaseStorage): try: self._mongo = AsyncIOMotorClient(self._uri) except pymongo.errors.ConfigurationError as e: - if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]: + if "query() got an unexpected keyword argument 'lifetime'" in e.args[0]: import logging logger = logging.getLogger("aiogram") logger.warning("Run `pip install dnspython==1.16.0` in order to fix ConfigurationError. More information: https://github.com/mongodb/mongo-python-driver/pull/423#issuecomment-528998245") @@ -114,7 +114,9 @@ class MongoStorage(BaseStorage): async def wait_closed(self): return True - async def set_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, + async def set_state(self, *, + chat: Union[str, int, None] = None, + user: Union[str, int, None] = None, state: Optional[AnyStr] = None): chat, user = self.check_address(chat=chat, user=user) db = await self.get_db() @@ -122,8 +124,11 @@ class MongoStorage(BaseStorage): if state is None: await db[STATE].delete_one(filter={'chat': chat, 'user': user}) else: - await db[STATE].update_one(filter={'chat': chat, 'user': user}, - update={'$set': {'state': state}}, upsert=True) + await db[STATE].update_one( + filter={'chat': chat, 'user': user}, + update={'$set': {'state': self.resolve_state(state)}}, + upsert=True, + ) async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, default: Optional[str] = None) -> Optional[str]: @@ -131,7 +136,7 @@ class MongoStorage(BaseStorage): db = await self.get_db() result = await db[STATE].find_one(filter={'chat': chat, 'user': user}) - return result.get('state') if result else default + return result.get('state') if result else self.resolve_state(default) async def set_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, data: Dict = None): diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 74dd736c..01a0fe5c 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -118,16 +118,19 @@ class RedisStorage(BaseStorage): async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Optional[str]: record = await self.get_record(chat=chat, user=user) - return record['state'] + return record.get('state', self.resolve_state(default)) async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Dict: record = await self.get_record(chat=chat, user=user) return record['data'] - async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, + async def set_state(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, state: typing.Optional[typing.AnyStr] = None): record = await self.get_record(chat=chat, user=user) + state = self.resolve_state(state) await self.set_record(chat=chat, user=user, state=state, data=record['data']) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, @@ -274,7 +277,7 @@ class RedisStorage2(BaseStorage): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) redis = await self.redis() - return await redis.get(key, encoding='utf8') or None + return await redis.get(key, encoding='utf8') or self.resolve_state(default) async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[dict] = None) -> typing.Dict: @@ -294,7 +297,7 @@ class RedisStorage2(BaseStorage): if state is None: await redis.delete(key) else: - await redis.set(key, state, expire=self._state_ttl) + await redis.set(key, self.resolve_state(state), expire=self._state_ttl) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None): diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index 5bb9062a..c600074e 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -95,7 +95,9 @@ class RethinkDBStorage(BaseStorage): default: typing.Optional[str] = None) -> typing.Optional[str]: chat, user = map(str, self.check_address(chat=chat, user=user)) async with self.connection() as conn: - return await r.table(self._table).get(chat)[user]['state'].default(default or None).run(conn) + return await r.table(self._table).get(chat)[user]['state'].default( + self.resolve_state(default) or None + ).run(conn) async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Dict: @@ -103,11 +105,16 @@ class RethinkDBStorage(BaseStorage): async with self.connection() as conn: return await r.table(self._table).get(chat)[user]['data'].default(default or {}).run(conn) - async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, + async def set_state(self, *, + chat: typing.Union[str, int, None] = None, + user: typing.Union[str, int, None] = None, state: typing.Optional[typing.AnyStr] = None): chat, user = map(str, self.check_address(chat=chat, user=user)) async with self.connection() as conn: - await r.table(self._table).insert({'id': chat, user: {'state': state}}, conflict="update").run(conn) + await r.table(self._table).insert( + {'id': chat, user: {'state': self.resolve_state(state)}}, + conflict="update", + ).run(conn) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None): diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index 74492361..eb248e34 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -40,24 +40,27 @@ class BaseStorage: @classmethod def check_address(cls, *, chat: typing.Union[str, int, None] = None, - user: typing.Union[str, int, None] = None) -> (typing.Union[str, int], typing.Union[str, int]): + user: typing.Union[str, int, None] = None, + ) -> (typing.Union[str, int], typing.Union[str, int]): """ In all storage's methods chat or user is always required. If one of them is not provided, you have to set missing value based on the provided one. This method performs the check described above. - :param chat: - :param user: + :param chat: chat_id + :param user: user_id :return: """ if chat is None and user is None: raise ValueError('`user` or `chat` parameter is required but no one is provided!') - if user is None and chat is not None: + if user is None: user = chat - elif user is not None and chat is None: + + elif chat is None: chat = user + return chat, user async def get_state(self, *, @@ -270,6 +273,21 @@ class BaseStorage: """ await self.set_data(chat=chat, user=user, data={}) + @staticmethod + def resolve_state(value): + from .filters.state import State + + if value is None: + return + + if isinstance(value, str): + return value + + if isinstance(value, State): + return value.state + + return str(value) + class FSMContext: def __init__(self, storage, chat, user): @@ -279,20 +297,8 @@ class FSMContext: def proxy(self): return FSMContextProxy(self) - @staticmethod - def _resolve_state(value): - from .filters.state import State - - if value is None: - return - elif isinstance(value, str): - return value - elif isinstance(value, State): - return value.state - return str(value) - async def get_state(self, default: typing.Optional[str] = None) -> typing.Optional[str]: - return await self.storage.get_state(chat=self.chat, user=self.user, default=self._resolve_state(default)) + return await self.storage.get_state(chat=self.chat, user=self.user, default=default) async def get_data(self, default: typing.Optional[str] = None) -> typing.Dict: return await self.storage.get_data(chat=self.chat, user=self.user, default=default) @@ -301,7 +307,7 @@ class FSMContext: await self.storage.update_data(chat=self.chat, user=self.user, data=data, **kwargs) async def set_state(self, state: typing.Optional[typing.AnyStr] = None): - await self.storage.set_state(chat=self.chat, user=self.user, state=self._resolve_state(state)) + await self.storage.set_state(chat=self.chat, user=self.user, state=state) async def set_data(self, data: typing.Dict = None): await self.storage.set_data(chat=self.chat, user=self.user, data=data) From 35bf18cf5ac4fdf75eccd6d23873309589beef35 Mon Sep 17 00:00:00 2001 From: Googleplex Date: Thu, 29 Apr 2021 04:37:59 +0800 Subject: [PATCH 093/118] fix: builtin command filter args (#556) (#558) * fix: builtin command filter args * fix: use string for command arguments * fix: text property of command object Co-authored-by: evgfilim1 Co-authored-by: evgfilim1 --- aiogram/dispatcher/filters/builtin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index c32c53be..457de182 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -4,7 +4,7 @@ import typing import warnings from contextvars import ContextVar from dataclasses import dataclass, field -from typing import Any, Dict, Iterable, Optional, Union +from typing import Any, Dict, Iterable, List, Optional, Union from babel.support import LazyProxy @@ -110,7 +110,8 @@ class Command(Filter): if not text: return False - full_command = text.split()[0] + full_command, *args_list = text.split(maxsplit=1) + args = args_list[0] if args_list else None prefix, (command, _, mention) = full_command[0], full_command[1:].partition('@') if not ignore_mention and mention and (await message.bot.me).username.lower() != mention.lower(): @@ -120,7 +121,7 @@ class Command(Filter): if (command.lower() if ignore_case else command) not in commands: return False - return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention)} + return {'command': cls.CommandObj(command=command, prefix=prefix, mention=mention, args=args)} @dataclass class CommandObj: From 1f57a40c45f6d94d010e928bd18ab16a9720df69 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 28 Apr 2021 23:39:36 +0300 Subject: [PATCH 094/118] Correctly use `provider_data` argument --- aiogram/bot/bot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 68f4ff27..07d4b963 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -2941,6 +2941,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ prices = prepare_arg([price.to_python() if hasattr(price, 'to_python') else price for price in prices]) reply_markup = prepare_arg(reply_markup) + provider_data = prepare_arg(provider_data) payload_ = generate_payload(**locals()) result = await self.request(api.Methods.SEND_INVOICE, payload_) From d5a4c0c4afc6f03b9de19f7f68f852c9188cf072 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 28 Apr 2021 23:41:07 +0300 Subject: [PATCH 095/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index a77ecdc0..3471ddcd 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.13.0' +__version__ = '2.13' __api_version__ = '5.2' From 405add31abf76de6896bf43ee3826306053d4916 Mon Sep 17 00:00:00 2001 From: dashedman <64865196+dashedman@users.noreply.github.com> Date: Tue, 11 May 2021 23:39:17 +0300 Subject: [PATCH 096/118] fix get_full_command for messages with caption (#576) * fix get_full_command for messages with caption * change to more cleaner method --- aiogram/types/message.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index ce8395d2..5eb96618 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -194,7 +194,8 @@ class Message(base.TelegramObject): :return: bool """ - return self.text and self.text.startswith("/") + text = self.text or self.caption + return text and text.startswith("/") def get_full_command(self) -> typing.Optional[typing.Tuple[str, str]]: """ @@ -203,8 +204,9 @@ class Message(base.TelegramObject): :return: tuple of (command, args) """ if self.is_command(): - command, *args = self.text.split(maxsplit=1) - args = args[-1] if args else "" + text = self.text or self.caption + command, *args = text.split(maxsplit=1) + args = args[0] if args else "" return command, args def get_command(self, pure=False) -> typing.Optional[str]: @@ -271,7 +273,7 @@ class Message(base.TelegramObject): :return: str """ - + if self.chat.type == ChatType.PRIVATE: raise TypeError("Invalid chat type!") url = "https://t.me/" @@ -1420,7 +1422,7 @@ class Message(base.TelegramObject): allow_sending_without_reply=allow_sending_without_reply, reply_markup=reply_markup, ) - + async def answer_chat_action( self, action: base.String, From 0e28756a103c63d807ec55d8afe0de92b300d856 Mon Sep 17 00:00:00 2001 From: pigeonburger <70826123+pigeonburger@users.noreply.github.com> Date: Mon, 17 May 2021 20:17:48 +1000 Subject: [PATCH 097/118] Update README.md (#586) --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae44c524..2d3c0545 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,10 @@ import asyncio from aiogram import Bot +BOT_TOKEN = "" async def main(): - bot = Bot(token=BOT-TOKEN) + bot = Bot(token=BOT_TOKEN) try: me = await bot.get_me() @@ -48,6 +49,8 @@ asyncio.run(main()) import asyncio from aiogram import Bot, Dispatcher, types +BOT_TOKEN = "" + async def start_handler(event: types.Message): await event.answer( f"Hello, {event.from_user.get_mention(as_html=True)} ๐Ÿ‘‹!", @@ -55,7 +58,7 @@ async def start_handler(event: types.Message): ) async def main(): - bot = Bot(token=BOT-TOKEN) + bot = Bot(token=BOT_TOKEN) try: disp = Dispatcher(bot=bot) disp.register_message_handler(start_handler, commands={"start", "restart"}) From 5f6e5a646b9a86bb1146244548a0d688bfaf4867 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 25 May 2021 10:12:52 +0300 Subject: [PATCH 098/118] Fix default updates (#592) * fix: default updates * fix: removed redundant cache import --- aiogram/types/update.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 7cf616bb..e2fd3a55 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -1,7 +1,5 @@ from __future__ import annotations -from functools import lru_cache - from . import base from . import fields from .callback_query import CallbackQuery @@ -76,7 +74,5 @@ class AllowedUpdates(helper.Helper): ) @classmethod - @lru_cache(1) def default(cls): - excluded = cls.CHAT_MEMBER + cls.MY_CHAT_MEMBER - return list(filter(lambda item: item not in excluded, cls.all())) + return [] From 02cd42a3397b635080131b3875b55952606c3099 Mon Sep 17 00:00:00 2001 From: p82o <64755699+p82o@users.noreply.github.com> Date: Sun, 6 Jun 2021 23:08:59 +0300 Subject: [PATCH 099/118] Update text_decorations.py (#597) --- 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 09484e3c..40fe296b 100644 --- a/aiogram/utils/text_decorations.py +++ b/aiogram/utils/text_decorations.py @@ -185,7 +185,7 @@ class MarkdownDecoration(TextDecoration): return f"`{value}`" def pre(self, value: str) -> str: - return f"```{value}```" + return f"```\n{value}\n```" def pre_language(self, value: str, language: str) -> str: return f"```{language}\n{value}\n```" From 7eb32785f6c8bbc464eea5233bd3ea84cd645fb1 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 13 Jun 2021 01:20:26 +0300 Subject: [PATCH 100/118] fix: input media caption_entities (#583) --- aiogram/types/input_media.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 4d03daec..6804b460 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -28,6 +28,7 @@ class InputMedia(base.TelegramObject): thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed') caption: base.String = fields.Field() parse_mode: base.String = fields.Field() + caption_entities: typing.List[MessageEntity] = fields.ListField(base=MessageEntity) def __init__(self, *args, **kwargs): self._thumb_file = None From 0b1c22b7b0c317eec8130cd223f422c72736a195 Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Sun, 13 Jun 2021 01:22:47 +0300 Subject: [PATCH 101/118] =?UTF-8?q?=D0=A1leanup=20storage=20(#587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove key from storage, when not needed * fix redis1 delete * fix new line * check reset * fix redis store close check --- aiogram/contrib/fsm_storage/memory.py | 9 +++ aiogram/contrib/fsm_storage/redis.py | 20 ++++-- dev_requirements.txt | 1 + tests/contrib/fsm_storage/test_redis.py | 33 ---------- tests/contrib/fsm_storage/test_storage.py | 79 +++++++++++++++++++++++ 5 files changed, 103 insertions(+), 39 deletions(-) delete mode 100644 tests/contrib/fsm_storage/test_redis.py create mode 100644 tests/contrib/fsm_storage/test_storage.py diff --git a/aiogram/contrib/fsm_storage/memory.py b/aiogram/contrib/fsm_storage/memory.py index e1d6bdc0..8950aa8e 100644 --- a/aiogram/contrib/fsm_storage/memory.py +++ b/aiogram/contrib/fsm_storage/memory.py @@ -66,6 +66,7 @@ class MemoryStorage(BaseStorage): data: typing.Dict = None): chat, user = self.resolve_address(chat=chat, user=user) self.data[chat][user]['data'] = copy.deepcopy(data) + self._cleanup(chat, user) async def reset_state(self, *, chat: typing.Union[str, int, None] = None, @@ -74,6 +75,7 @@ class MemoryStorage(BaseStorage): await self.set_state(chat=chat, user=user, state=None) if with_data: await self.set_data(chat=chat, user=user, data={}) + self._cleanup(chat, user) def has_bucket(self): return True @@ -91,6 +93,7 @@ class MemoryStorage(BaseStorage): bucket: typing.Dict = None): chat, user = self.resolve_address(chat=chat, user=user) self.data[chat][user]['bucket'] = copy.deepcopy(bucket) + self._cleanup(chat, user) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, @@ -100,3 +103,9 @@ class MemoryStorage(BaseStorage): bucket = {} chat, user = self.resolve_address(chat=chat, user=user) self.data[chat][user]['bucket'].update(bucket, **kwargs) + + def _cleanup(self, chat, user): + if self.data[chat][user] == {'state': None, 'data': {}, 'bucket': {}}: + del self.data[chat][user] + if not self.data[chat]: + del self.data[chat] diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 01a0fe5c..5d0b762c 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -110,10 +110,12 @@ class RedisStorage(BaseStorage): chat, user = self.check_address(chat=chat, user=user) addr = f"fsm:{chat}:{user}" - record = {'state': state, 'data': data, 'bucket': bucket} - conn = await self.redis() - await conn.execute('SET', addr, json.dumps(record)) + if state is None and data == bucket == {}: + await conn.execute('DEL', addr) + else: + record = {'state': state, 'data': data, 'bucket': bucket} + await conn.execute('SET', addr, json.dumps(record)) async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Optional[str]: @@ -222,7 +224,7 @@ class RedisStorage2(BaseStorage): await dp.storage.wait_closed() """ - def __init__(self, host: str = 'localhost', port=6379, db=None, password=None, + 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, @@ -304,7 +306,10 @@ class RedisStorage2(BaseStorage): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_DATA_KEY) redis = await self.redis() - await redis.set(key, json.dumps(data), expire=self._data_ttl) + if data: + await redis.set(key, json.dumps(data), expire=self._data_ttl) + else: + await redis.delete(key) async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None, **kwargs): @@ -332,7 +337,10 @@ class RedisStorage2(BaseStorage): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_BUCKET_KEY) redis = await self.redis() - await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl) + if bucket: + await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl) + else: + await redis.delete(key) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, diff --git a/dev_requirements.txt b/dev_requirements.txt index ef5272af..26e410aa 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -16,3 +16,4 @@ aiohttp-socks>=0.3.4 rethinkdb>=2.4.1 coverage==4.5.3 motor>=2.2.0 +pytest-lazy-fixture==0.6.* diff --git a/tests/contrib/fsm_storage/test_redis.py b/tests/contrib/fsm_storage/test_redis.py deleted file mode 100644 index 527c905e..00000000 --- a/tests/contrib/fsm_storage/test_redis.py +++ /dev/null @@ -1,33 +0,0 @@ -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 diff --git a/tests/contrib/fsm_storage/test_storage.py b/tests/contrib/fsm_storage/test_storage.py new file mode 100644 index 00000000..0cde2de2 --- /dev/null +++ b/tests/contrib/fsm_storage/test_storage.py @@ -0,0 +1,79 @@ +import pytest + +from aiogram.contrib.fsm_storage.memory import MemoryStorage +from aiogram.contrib.fsm_storage.redis import RedisStorage2, RedisStorage + + +@pytest.fixture() +@pytest.mark.redis +async def redis_store(redis_options): + s = RedisStorage(**redis_options) + try: + yield s + finally: + conn = await s.redis() + await conn.execute('FLUSHDB') + await s.close() + await s.wait_closed() + + +@pytest.fixture() +@pytest.mark.redis +async def redis_store2(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.fixture() +async def memory_store(): + yield MemoryStorage() + + +@pytest.mark.parametrize( + "store", [ + pytest.lazy_fixture('redis_store'), + pytest.lazy_fixture('redis_store2'), + pytest.lazy_fixture('memory_store'), + ] +) +class TestStorage: + @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_reset(self, store): + await store.set_data(chat='1234', data={'foo': 'bar'}) + await store.reset_data(chat='1234') + assert await store.get_data(chat='1234') == {} + + @pytest.mark.asyncio + async def test_reset_empty(self, store): + await store.reset_data(chat='1234') + assert await store.get_data(chat='1234') == {} + + +@pytest.mark.parametrize( + "store", [ + pytest.lazy_fixture('redis_store'), + pytest.lazy_fixture('redis_store2'), + ] +) +class TestRedisStorage2: + @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 f20e6ca0bde0453924fe4882b1f999ea91a1df1d Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 13 Jun 2021 01:22:57 +0300 Subject: [PATCH 102/118] fix: GroupDeactivated exception text update (#598) --- aiogram/utils/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 9a1606a6..e3a1f313 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -368,7 +368,7 @@ class WrongFileIdentifier(BadRequest): class GroupDeactivated(BadRequest): - match = 'group is deactivated' + match = 'Group chat was deactivated' class PhotoAsInputFileRequired(BadRequest): From c42b7e4b0d72503ea1b63a64ff9e0159bc4b68ae Mon Sep 17 00:00:00 2001 From: Biorobot1337 <83316072+genagorkin1@users.noreply.github.com> Date: Mon, 14 Jun 2021 02:26:27 +0500 Subject: [PATCH 103/118] Update message.py (#603) Fixed syntax bug, added comma --- 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 5eb96618..c95b14b1 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -2933,7 +2933,7 @@ class Message(base.TelegramObject): question=self.poll.question, options=[option.text for option in self.poll.options], is_anonymous=self.poll.is_anonymous, - allows_multiple_answers=self.poll.allows_multiple_answers + allows_multiple_answers=self.poll.allows_multiple_answers, **kwargs, ) elif self.dice: From e70a76ff63c0d3db0d1d15e26c6f0edbaaad9670 Mon Sep 17 00:00:00 2001 From: Biorobot1337 <83316072+genagorkin1@users.noreply.github.com> Date: Tue, 22 Jun 2021 01:15:01 +0500 Subject: [PATCH 104/118] Mongo storage cleanup (#609) * Update message.py Fixed syntax bug, added comma * Cleanup mongodb storage Cleaning up blank documents in DATA collection --- aiogram/contrib/fsm_storage/mongo.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index 992e2e70..ab7d3176 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -142,9 +142,11 @@ class MongoStorage(BaseStorage): data: Dict = None): chat, user = self.check_address(chat=chat, user=user) db = await self.get_db() - - await db[DATA].update_one(filter={'chat': chat, 'user': user}, - update={'$set': {'data': data}}, upsert=True) + if not data: + await db[DATA].delete_one(filter={'chat': chat, 'user': user}) + else: + await db[DATA].update_one(filter={'chat': chat, 'user': user}, + update={'$set': {'data': data}}, upsert=True) async def get_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, default: Optional[dict] = None) -> Dict: From f18e4491c25823644c926447b5fb42ad697992ad Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sun, 4 Jul 2021 23:52:55 +0300 Subject: [PATCH 105/118] Telegram API 5.3 (#610) * docs: api version update * feat: personalized commands * feat: custom placeholders * refactor: ChatMember split * fix: old names for ChatMemberStatus * refactor: renamed kickChatMember to banChatMember * style: align params * refactor: renamed getChatMembersCount to getChatMemberCount (#614) * feat: resolve ChatMember * refactor: renamed BotCommandScopeTypes (similar to code style) * refactor: resolve is a static method * Construct BotCommandScope from type * Make BotCommandScope classmethod instead of method * Use classmethod for ChatMember resolve method Co-authored-by: Hoi Dmytro Co-authored-by: Alex Root Junior --- README.md | 2 +- README.rst | 2 +- aiogram/__init__.py | 4 +- aiogram/bot/api.py | 7 +- aiogram/bot/bot.py | 160 +++++++++++++++++++------ aiogram/types/__init__.py | 13 ++ aiogram/types/bot_command_scope.py | 121 +++++++++++++++++++ aiogram/types/chat.py | 32 ++--- aiogram/types/chat_member.py | 186 ++++++++++++++++++++++------- aiogram/types/force_reply.py | 25 ++-- aiogram/types/reply_keyboard.py | 17 ++- docs/source/index.rst | 2 +- tests/test_bot.py | 8 +- tests/types/test_chat_member.py | 2 +- 14 files changed, 455 insertions(+), 126 deletions(-) create mode 100644 aiogram/types/bot_command_scope.py diff --git a/README.md b/README.md index 2d3c0545..fca118a0 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-5.2-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-5.3-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 caf6149c..6df651a2 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-5.2-blue.svg?style=flat-square&logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-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 3471ddcd..2d852a7e 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.13' -__api_version__ = '5.2' +__version__ = '2.14' +__api_version__ = '5.3' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 38cbee89..1bf00d47 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -189,7 +189,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 5.2 + List is updated to Bot API 5.3 """ mode = HelperMode.lowerCamelCase @@ -225,6 +225,7 @@ class Methods(Helper): GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos GET_FILE = Item() # getFile KICK_CHAT_MEMBER = Item() # kickChatMember + BAN_CHAT_MEMBER = Item() # banChatMember UNBAN_CHAT_MEMBER = Item() # unbanChatMember RESTRICT_CHAT_MEMBER = Item() # restrictChatMember PROMOTE_CHAT_MEMBER = Item() # promoteChatMember @@ -244,12 +245,14 @@ class Methods(Helper): LEAVE_CHAT = Item() # leaveChat GET_CHAT = Item() # getChat GET_CHAT_ADMINISTRATORS = Item() # getChatAdministrators - GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount + GET_CHAT_MEMBER_COUNT = Item() # getChatMemberCount + GET_CHAT_MEMBERS_COUNT = Item() # getChatMembersCount (renamed to getChatMemberCount) GET_CHAT_MEMBER = Item() # getChatMember SET_CHAT_STICKER_SET = Item() # setChatStickerSet DELETE_CHAT_STICKER_SET = Item() # deleteChatStickerSet ANSWER_CALLBACK_QUERY = Item() # answerCallbackQuery SET_MY_COMMANDS = Item() # setMyCommands + DELETE_MY_COMMANDS = Item() # deleteMyCommands GET_MY_COMMANDS = Item() # getMyCommands # Updating messages diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 07d4b963..435def3e 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1562,41 +1562,42 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_FILE, payload) 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, datetime.datetime, - datetime.timedelta, None] = None, - revoke_messages: typing.Optional[base.Boolean] = None, - ) -> base.Boolean: + async def ban_chat_member(self, + chat_id: typing.Union[base.Integer, base.String], + user_id: base.Integer, + until_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None] = None, + revoke_messages: typing.Optional[base.Boolean] = 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 chat on their own using invite links, etc., unless unbanned first. + Use this method to ban a user in a group, a supergroup or a + channel. In the case of supergroups and channels, the user will + not be able to return to the chat on their own using invite + links, etc., unless unbanned first. The bot must be an + administrator in the chat for this to work and must have the + appropriate admin rights. Returns True on success. - The bot must be an administrator in the chat for this to work and must have - the appropriate admin rights. + Source: https://core.telegram.org/bots/api#banchatmember - Source: https://core.telegram.org/bots/api#kickchatmember - - :param chat_id: Unique identifier for the target group or username of the - target supergroup or channel (in the format @channelusername) + :param chat_id: Unique identifier for the target group or + username of the target supergroup or channel (in the format + @channelusername) :type chat_id: :obj:`typing.Union[base.Integer, base.String]` :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` - :param until_date: Date when the user will be unbanned. If user is banned - for more than 366 days or less than 30 seconds from the current time they - are considered to be banned forever. Applied for supergroups and channels - only. - :type until_date: :obj:`typing.Union[base.Integer, datetime.datetime, - datetime.timedelta, None]` + :param until_date: Date when the user will be unbanned, unix + time. If user is banned for more than 366 days or less than + 30 seconds from the current time they are considered to be + banned forever. Applied for supergroups and channels only. + :type until_date: :obj:`typing.Union[base.Integer, + datetime.datetime, datetime.timedelta, None]` - :param revoke_messages: Pass True to delete all messages from the chat for - the user that is being removed. If False, the user will be able to see - messages in the group that were sent before the user was removed. Always - True for supergroups and channels. + :param revoke_messages: Pass True to delete all messages from + the chat for the user that is being removed. If False, the user + will be able to see messages in the group that were sent before + the user was removed. Always True for supergroups and channels. :type revoke_messages: :obj:`typing.Optional[base.Boolean]` :return: Returns True on success @@ -1605,7 +1606,22 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): until_date = prepare_arg(until_date) payload = generate_payload(**locals()) - return await self.request(api.Methods.KICK_CHAT_MEMBER, payload) + return await self.request(api.Methods.BAN_CHAT_MEMBER, payload) + + async def kick_chat_member(self, + chat_id: typing.Union[base.Integer, base.String], + user_id: base.Integer, + until_date: typing.Union[base.Integer, datetime.datetime, + datetime.timedelta, None] = None, + revoke_messages: typing.Optional[base.Boolean] = None, + ) -> base.Boolean: + """Renamed to ban_chat_member.""" + return await self.ban_chat_member( + chat_id=chat_id, + user_id=user_id, + until_date=until_date, + revoke_messages=revoke_messages, + ) async def unban_chat_member(self, chat_id: typing.Union[base.Integer, base.String], @@ -2130,13 +2146,13 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) result = await self.request(api.Methods.GET_CHAT_ADMINISTRATORS, payload) - return [types.ChatMember(**chatmember) for chatmember in result] + return [types.ChatMember.resolve(**chat_member) for chat_member in result] - async def get_chat_members_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer: + async def get_chat_member_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer: """ Use this method to get the number of members in a chat. - Source: https://core.telegram.org/bots/api#getchatmemberscount + Source: https://core.telegram.org/bots/api#getchatmembercount :param chat_id: Unique identifier for the target chat or username of the target supergroup or channel :type chat_id: :obj:`typing.Union[base.Integer, base.String]` @@ -2145,7 +2161,11 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): """ payload = generate_payload(**locals()) - return await self.request(api.Methods.GET_CHAT_MEMBERS_COUNT, payload) + return await self.request(api.Methods.GET_CHAT_MEMBER_COUNT, payload) + + async def get_chat_members_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer: + """Renamed to get_chat_member_count.""" + return await self.get_chat_member_count(chat_id) async def get_chat_member(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer) -> types.ChatMember: @@ -2164,7 +2184,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): payload = generate_payload(**locals()) result = await self.request(api.Methods.GET_CHAT_MEMBER, payload) - return types.ChatMember(**result) + return types.ChatMember.resolve(**result) async def set_chat_sticker_set(self, chat_id: typing.Union[base.Integer, base.String], sticker_set_name: base.String) -> base.Boolean: @@ -2241,31 +2261,95 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload) - async def set_my_commands(self, commands: typing.List[types.BotCommand]) -> base.Boolean: + async def set_my_commands(self, + commands: typing.List[types.BotCommand], + scope: typing.Optional[types.BotCommandScope] = None, + language_code: typing.Optional[base.String] = None, + ) -> 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. + :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]` + + :param scope: A JSON-serialized object, describing scope of + users for which the commands are relevant. Defaults to + BotCommandScopeDefault. + :type scope: :obj: `typing.Optional[types.BotCommandScope]` + + :param language_code: A two-letter ISO 639-1 language code. If + empty, commands will be applied to all users from the given + scope, for whose language there are no dedicated commands + :type language_code: :obj: `typing.Optional[base.String]` + :return: Returns True on success. :rtype: :obj:`base.Boolean` """ commands = prepare_arg(commands) + scope = prepare_arg(scope) payload = generate_payload(**locals()) return await self.request(api.Methods.SET_MY_COMMANDS, payload) - async def get_my_commands(self) -> typing.List[types.BotCommand]: + async def delete_my_commands(self, + scope: typing.Optional[types.BotCommandScope] = None, + language_code: typing.Optional[base.String] = None, + ) -> base.Boolean: """ - Use this method to get the current list of the bot's commands. + Use this method to delete the list of the bot's commands for the + given scope and user language. After deletion, higher level + commands will be shown to affected users. + + Source: https://core.telegram.org/bots/api#deletemycommands + + :param scope: A JSON-serialized object, describing scope of + users for which the commands are relevant. Defaults to + BotCommandScopeDefault. + :type scope: :obj: `typing.Optional[types.BotCommandScope]` + + :param language_code: A two-letter ISO 639-1 language code. If + empty, commands will be applied to all users from the given + scope, for whose language there are no dedicated commands + :type language_code: :obj: `typing.Optional[base.String]` + + :return: Returns True on success. + :rtype: :obj:`base.Boolean` + """ + scope = prepare_arg(scope) + payload = generate_payload(**locals()) + + return await self.request(api.Methods.DELETE_MY_COMMANDS, payload) + + async def get_my_commands(self, + scope: typing.Optional[types.BotCommandScope] = None, + language_code: typing.Optional[base.String] = None, + ) -> typing.List[types.BotCommand]: + """ + Use this method to get the current list of the bot's commands + for the given scope and user language. Returns Array of + BotCommand on success. If commands aren't set, an empty list is + returned. Source: https://core.telegram.org/bots/api#getmycommands - :return: Returns Array of BotCommand on success. + + :param scope: A JSON-serialized object, describing scope of + users for which the commands are relevant. Defaults to + BotCommandScopeDefault. + :type scope: :obj: `typing.Optional[types.BotCommandScope]` + + :param language_code: A two-letter ISO 639-1 language code. If + empty, commands will be applied to all users from the given + scope, for whose language there are no dedicated commands + :type language_code: :obj: `typing.Optional[base.String]` + + :return: Returns Array of BotCommand on success or empty list. :rtype: :obj:`typing.List[types.BotCommand]` """ + scope = prepare_arg(scope) payload = generate_payload(**locals()) result = await self.request(api.Methods.GET_MY_COMMANDS, payload) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 90909e81..a9e6af8c 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -4,6 +4,10 @@ from .animation import Animation from .audio import Audio from .auth_widget_data import AuthWidgetData from .bot_command import BotCommand +from .bot_command_scope import BotCommandScope, BotCommandScopeAllChatAdministrators, \ + BotCommandScopeAllGroupChats, BotCommandScopeAllPrivateChats, BotCommandScopeChat, \ + BotCommandScopeChatAdministrators, BotCommandScopeChatMember, \ + BotCommandScopeDefault, BotCommandScopeType from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType @@ -82,6 +86,15 @@ __all__ = ( 'Audio', 'AuthWidgetData', 'BotCommand', + 'BotCommandScope', + 'BotCommandScopeAllChatAdministrators', + 'BotCommandScopeAllGroupChats', + 'BotCommandScopeAllPrivateChats', + 'BotCommandScopeChat', + 'BotCommandScopeChatAdministrators', + 'BotCommandScopeChatMember', + 'BotCommandScopeDefault', + 'BotCommandScopeType', 'CallbackGame', 'CallbackQuery', 'Chat', diff --git a/aiogram/types/bot_command_scope.py b/aiogram/types/bot_command_scope.py new file mode 100644 index 00000000..e3091a7e --- /dev/null +++ b/aiogram/types/bot_command_scope.py @@ -0,0 +1,121 @@ +import typing + +from . import base, fields +from ..utils import helper + + +class BotCommandScopeType(helper.Helper): + mode = helper.HelperMode.lowercase + + DEFAULT = helper.Item() # default + ALL_PRIVATE_CHATS = helper.Item() # all_private_chats + ALL_GROUP_CHATS = helper.Item() # all_group_chats + ALL_CHAT_ADMINISTRATORS = helper.Item() # all_chat_administrators + CHAT = helper.Item() # chat + CHAT_ADMINISTRATORS = helper.Item() # chat_administrators + CHAT_MEMBER = helper.Item() # chat_member + + +class BotCommandScope(base.TelegramObject): + """ + This object represents the scope to which bot commands are applied. + Currently, the following 7 scopes are supported: + BotCommandScopeDefault + BotCommandScopeAllPrivateChats + BotCommandScopeAllGroupChats + BotCommandScopeAllChatAdministrators + BotCommandScopeChat + BotCommandScopeChatAdministrators + BotCommandScopeChatMember + + https://core.telegram.org/bots/api#botcommandscope + """ + type: base.String = fields.Field() + + @classmethod + def from_type(cls, type: str, **kwargs: typing.Any): + if type == BotCommandScopeType.DEFAULT: + return BotCommandScopeDefault(type=type, **kwargs) + if type == BotCommandScopeType.ALL_PRIVATE_CHATS: + return BotCommandScopeAllPrivateChats(type=type, **kwargs) + if type == BotCommandScopeType.ALL_GROUP_CHATS: + return BotCommandScopeAllGroupChats(type=type, **kwargs) + if type == BotCommandScopeType.ALL_CHAT_ADMINISTRATORS: + return BotCommandScopeAllChatAdministrators(type=type, **kwargs) + if type == BotCommandScopeType.CHAT: + return BotCommandScopeChat(type=type, **kwargs) + if type == BotCommandScopeType.CHAT_ADMINISTRATORS: + return BotCommandScopeChatAdministrators(type=type, **kwargs) + if type == BotCommandScopeType.CHAT_MEMBER: + return BotCommandScopeChatMember(type=type, **kwargs) + raise ValueError(f"Unknown BotCommandScope type {type!r}") + + +class BotCommandScopeDefault(BotCommandScope): + """ + Represents the default scope of bot commands. + Default commands are used if no commands with a narrower scope are + specified for the user. + """ + type = fields.Field(default=BotCommandScopeType.DEFAULT) + + +class BotCommandScopeAllPrivateChats(BotCommandScope): + """ + Represents the scope of bot commands, covering all private chats. + """ + type = fields.Field(default=BotCommandScopeType.ALL_PRIVATE_CHATS) + + +class BotCommandScopeAllGroupChats(BotCommandScope): + """ + Represents the scope of bot commands, covering all group and + supergroup chats. + """ + type = fields.Field(default=BotCommandScopeType.ALL_GROUP_CHATS) + + +class BotCommandScopeAllChatAdministrators(BotCommandScope): + """ + Represents the scope of bot commands, covering all group and + supergroup chat administrators. + """ + type = fields.Field(default=BotCommandScopeType.ALL_CHAT_ADMINISTRATORS) + + +class BotCommandScopeChat(BotCommandScope): + """ + Represents the scope of bot commands, covering a specific chat. + """ + type = fields.Field(default=BotCommandScopeType.CHAT) + chat_id: typing.Union[base.String, base.Integer] = fields.Field() + + def __init__(self, chat_id: typing.Union[base.String, base.Integer], **kwargs): + super().__init__(chat_id=chat_id, **kwargs) + + +class BotCommandScopeChatAdministrators(BotCommandScopeChat): + """ + Represents the scope of bot commands, covering all administrators + of a specific group or supergroup chat. + """ + type = fields.Field(default=BotCommandScopeType.CHAT_ADMINISTRATORS) + chat_id: typing.Union[base.String, base.Integer] = fields.Field() + + +class BotCommandScopeChatMember(BotCommandScopeChat): + """ + Represents the scope of bot commands, covering a specific member of + a group or supergroup chat. + """ + type = fields.Field(default=BotCommandScopeType.CHAT_MEMBER) + chat_id: typing.Union[base.String, base.Integer] = fields.Field() + user_id: base.Integer = fields.Field() + + def __init__( + self, + chat_id: typing.Union[base.String, base.Integer], + user_id: base.Integer, + **kwargs, + ): + super().__init__(chat_id=chat_id, user_id=user_id, **kwargs) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 5b3b315a..2cd19a0f 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -301,7 +301,7 @@ class Chat(base.TelegramObject): can_send_other_messages=can_send_other_messages, can_add_web_page_previews=can_add_web_page_previews) - async def promote(self, + async def promote(self, user_id: base.Integer, is_anonymous: typing.Optional[base.Boolean] = None, can_change_info: typing.Optional[base.Boolean] = None, @@ -321,36 +321,36 @@ class Chat(base.TelegramObject): :param user_id: Unique identifier of the target user :type user_id: :obj:`base.Integer` - + :param is_anonymous: Pass True, if the administrator's presence in the chat is hidden :type is_anonymous: :obj:`typing.Optional[base.Boolean]` - + :param can_change_info: Pass True, if the administrator can change chat title, photo and other settings :type can_change_info: :obj:`typing.Optional[base.Boolean]` - + :param can_post_messages: Pass True, if the administrator can create channel posts, channels only :type can_post_messages: :obj:`typing.Optional[base.Boolean]` - + :param can_edit_messages: Pass True, if the administrator can edit messages of other users, channels only :type can_edit_messages: :obj:`typing.Optional[base.Boolean]` - + :param can_delete_messages: Pass True, if the administrator can delete messages of other users :type can_delete_messages: :obj:`typing.Optional[base.Boolean]` - + :param can_invite_users: Pass True, if the administrator can invite new users to the chat :type can_invite_users: :obj:`typing.Optional[base.Boolean]` - + :param can_restrict_members: Pass True, if the administrator can restrict, ban or unban chat members :type can_restrict_members: :obj:`typing.Optional[base.Boolean]` - + :param can_pin_messages: Pass True, if the administrator can pin messages, supergroups only :type can_pin_messages: :obj:`typing.Optional[base.Boolean]` - + :param can_promote_members: Pass True, if the administrator can add new administrators with a subset of his own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by him) :type can_promote_members: :obj:`typing.Optional[base.Boolean]` - + :return: Returns True on success. :rtype: :obj:`base.Boolean` """ @@ -484,16 +484,20 @@ class Chat(base.TelegramObject): """ return await self.bot.get_chat_administrators(self.id) - async def get_members_count(self) -> base.Integer: + async def get_member_count(self) -> base.Integer: """ Use this method to get the number of members in a chat. - Source: https://core.telegram.org/bots/api#getchatmemberscount + Source: https://core.telegram.org/bots/api#getchatmembercount :return: Returns Int on success. :rtype: :obj:`base.Integer` """ - return await self.bot.get_chat_members_count(self.id) + return await self.bot.get_chat_member_count(self.id) + + async def get_members_count(self) -> base.Integer: + """Renamed to get_member_count.""" + return await self.get_member_count(self.id) async def get_member(self, user_id: base.Integer) -> ChatMember: """ diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index c48a91d0..58e4cb62 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,53 +1,11 @@ import datetime +from typing import Optional -from . import base -from . import fields +from . import base, fields from .user import User from ..utils import helper -class ChatMember(base.TelegramObject): - """ - This object contains information about one member of a chat. - - https://core.telegram.org/bots/api#chatmember - """ - user: User = fields.Field(base=User) - status: base.String = fields.Field() - custom_title: base.String = fields.Field() - is_anonymous: base.Boolean = fields.Field() - can_be_edited: base.Boolean = fields.Field() - can_manage_chat: base.Boolean = fields.Field() - can_post_messages: base.Boolean = fields.Field() - can_edit_messages: base.Boolean = fields.Field() - can_delete_messages: base.Boolean = fields.Field() - can_manage_voice_chats: base.Boolean = fields.Field() - can_restrict_members: base.Boolean = fields.Field() - can_promote_members: base.Boolean = fields.Field() - can_change_info: base.Boolean = fields.Field() - can_invite_users: base.Boolean = fields.Field() - can_pin_messages: base.Boolean = fields.Field() - is_member: base.Boolean = fields.Field() - can_send_messages: base.Boolean = fields.Field() - can_send_media_messages: base.Boolean = fields.Field() - can_send_polls: base.Boolean = fields.Field() - can_send_other_messages: base.Boolean = fields.Field() - can_add_web_page_previews: base.Boolean = fields.Field() - until_date: datetime.datetime = fields.DateTimeField() - - def is_chat_creator(self) -> bool: - return ChatMemberStatus.is_chat_creator(self.status) - - def is_chat_admin(self) -> bool: - return ChatMemberStatus.is_chat_admin(self.status) - - def is_chat_member(self) -> bool: - return ChatMemberStatus.is_chat_member(self.status) - - def __int__(self) -> int: - return self.user.id - - class ChatMemberStatus(helper.Helper): """ Chat member status @@ -55,11 +13,13 @@ class ChatMemberStatus(helper.Helper): mode = helper.HelperMode.lowercase CREATOR = helper.Item() # creator + OWNER = CREATOR # creator ADMINISTRATOR = helper.Item() # administrator MEMBER = helper.Item() # member RESTRICTED = helper.Item() # restricted LEFT = helper.Item() # left KICKED = helper.Item() # kicked + BANNED = KICKED # kicked @classmethod def is_chat_creator(cls, role: str) -> bool: @@ -72,3 +32,141 @@ class ChatMemberStatus(helper.Helper): @classmethod def is_chat_member(cls, role: str) -> bool: return role in (cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR, cls.RESTRICTED) + + @classmethod + def get_class_by_status(cls, status: str) -> Optional["ChatMember"]: + return { + cls.OWNER: ChatMemberOwner, + cls.ADMINISTRATOR: ChatMemberAdministrator, + cls.MEMBER: ChatMemberMember, + cls.RESTRICTED: ChatMemberRestricted, + cls.LEFT: ChatMemberLeft, + cls.BANNED: ChatMemberBanned, + }.get(status) + + +class ChatMember(base.TelegramObject): + """ + This object contains information about one member of a chat. + Currently, the following 6 types of chat members are supported: + ChatMemberOwner + ChatMemberAdministrator + ChatMemberMember + ChatMemberRestricted + ChatMemberLeft + ChatMemberBanned + + https://core.telegram.org/bots/api#chatmember + """ + status: base.String = fields.Field() + user: User = fields.Field(base=User) + + def __int__(self) -> int: + return self.user.id + + @classmethod + def resolve(cls, **kwargs) -> "ChatMember": + status = kwargs.get("status") + mapping = { + ChatMemberStatus.OWNER: ChatMemberOwner, + ChatMemberStatus.ADMINISTRATOR: ChatMemberAdministrator, + ChatMemberStatus.MEMBER: ChatMemberMember, + ChatMemberStatus.RESTRICTED: ChatMemberRestricted, + ChatMemberStatus.LEFT: ChatMemberLeft, + ChatMemberStatus.BANNED: ChatMemberBanned, + } + class_ = mapping.get(status) + if class_ is None: + raise ValueError(f"Can't find `ChatMember` class for status `{status}`") + + return class_(**kwargs) + + +class ChatMemberOwner(ChatMember): + """ + Represents a chat member that owns the chat and has all + administrator privileges. + https://core.telegram.org/bots/api#chatmemberowner + """ + status: base.String = fields.Field(default=ChatMemberStatus.OWNER) + user: User = fields.Field(base=User) + custom_title: base.String = fields.Field() + is_anonymous: base.Boolean = fields.Field() + + +class ChatMemberAdministrator(ChatMember): + """ + Represents a chat member that has some additional privileges. + + https://core.telegram.org/bots/api#chatmemberadministrator + """ + status: base.String = fields.Field(default=ChatMemberStatus.ADMINISTRATOR) + user: User = fields.Field(base=User) + can_be_edited: base.Boolean = fields.Field() + custom_title: base.String = fields.Field() + is_anonymous: base.Boolean = fields.Field() + can_manage_chat: base.Boolean = fields.Field() + can_post_messages: base.Boolean = fields.Field() + can_edit_messages: base.Boolean = fields.Field() + can_delete_messages: base.Boolean = fields.Field() + can_manage_voice_chats: base.Boolean = fields.Field() + can_restrict_members: base.Boolean = fields.Field() + can_promote_members: base.Boolean = fields.Field() + can_change_info: base.Boolean = fields.Field() + can_invite_users: base.Boolean = fields.Field() + can_pin_messages: base.Boolean = fields.Field() + + +class ChatMemberMember(ChatMember): + """ + Represents a chat member that has no additional privileges or + restrictions. + + https://core.telegram.org/bots/api#chatmembermember + """ + status: base.String = fields.Field(default=ChatMemberStatus.MEMBER) + user: User = fields.Field(base=User) + + +class ChatMemberRestricted(ChatMember): + """ + Represents a chat member that is under certain restrictions in the + chat. Supergroups only. + + https://core.telegram.org/bots/api#chatmemberrestricted + """ + status: base.String = fields.Field(default=ChatMemberStatus.RESTRICTED) + user: User = fields.Field(base=User) + is_member: base.Boolean = fields.Field() + can_change_info: base.Boolean = fields.Field() + can_invite_users: base.Boolean = fields.Field() + can_pin_messages: base.Boolean = fields.Field() + can_send_messages: base.Boolean = fields.Field() + can_send_media_messages: base.Boolean = fields.Field() + can_send_polls: base.Boolean = fields.Field() + can_send_other_messages: base.Boolean = fields.Field() + can_add_web_page_previews: base.Boolean = fields.Field() + until_date: datetime.datetime = fields.DateTimeField() + + +class ChatMemberLeft(ChatMember): + """ + Represents a chat member that isn't currently a member of the chat, + but may join it themselves. + + https://core.telegram.org/bots/api#chatmemberleft + """ + status: base.String = fields.Field(default=ChatMemberStatus.LEFT) + user: User = fields.Field(base=User) + + +class ChatMemberBanned(ChatMember): + """ + Represents a chat member that was banned in the chat and can't + return to the chat or view chat messages. + + https://core.telegram.org/bots/api#chatmemberbanned + """ + status: base.String = fields.Field(default=ChatMemberStatus.BANNED) + user: User = fields.Field(base=User) + until_date: datetime.datetime = fields.DateTimeField() diff --git a/aiogram/types/force_reply.py b/aiogram/types/force_reply.py index 97ec16c6..d6b4f19f 100644 --- a/aiogram/types/force_reply.py +++ b/aiogram/types/force_reply.py @@ -6,31 +6,28 @@ from . import fields class ForceReply(base.TelegramObject): """ - Upon receiving a message with this object, - Telegram clients will display a reply interface to the user - (act as if the user has selected the botโ€˜s message and tapped โ€™Reply'). - This can be extremely useful if you want to create user-friendly step-by-step + Upon receiving a message with this object, Telegram clients will + display a reply interface to the user (act as if the user has + selected the bot's message and tapped 'Reply'). This can be + extremely useful if you want to create user-friendly step-by-step interfaces without having to sacrifice privacy mode. - Example: A poll bot for groups runs in privacy mode - (only receives commands, replies to its messages and mentions). - There could be two ways to create a new poll - - The last option is definitely more attractive. - And if you use ForceReply in your botโ€˜s questions, it will receive the userโ€™s answers even - if it only receives replies, commands and mentions โ€” without any extra work for the user. - https://core.telegram.org/bots/api#forcereply """ force_reply: base.Boolean = fields.Field(default=True) + input_field_placeholder: base.String = fields.Field() selective: base.Boolean = fields.Field() @classmethod - def create(cls, selective: typing.Optional[base.Boolean] = None): + def create(cls, + input_field_placeholder: typing.Optional[base.String] = None, + selective: typing.Optional[base.Boolean] = None, + ) -> 'ForceReply': """ Create new force reply :param selective: + :param input_field_placeholder: :return: """ - return cls(selective=selective) + return cls(selective=selective, input_field_placeholder=input_field_placeholder) diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index ffe07ae1..e648e036 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -18,23 +18,32 @@ class KeyboardButtonPollType(base.TelegramObject): class ReplyKeyboardMarkup(base.TelegramObject): """ - This object represents a custom keyboard with reply options (see Introduction to bots for details and examples). + This object represents a custom keyboard with reply options + (see https://core.telegram.org/bots#keyboards to bots for details + and examples). https://core.telegram.org/bots/api#replykeyboardmarkup """ keyboard: 'typing.List[typing.List[KeyboardButton]]' = fields.ListOfLists(base='KeyboardButton', default=[]) resize_keyboard: base.Boolean = fields.Field() one_time_keyboard: base.Boolean = fields.Field() + input_field_placeholder: base.String = fields.Field() selective: base.Boolean = fields.Field() def __init__(self, keyboard: 'typing.List[typing.List[KeyboardButton]]' = None, resize_keyboard: base.Boolean = None, one_time_keyboard: base.Boolean = None, + input_field_placeholder: base.String = None, selective: base.Boolean = None, row_width: base.Integer = 3): - super(ReplyKeyboardMarkup, self).__init__(keyboard=keyboard, resize_keyboard=resize_keyboard, - one_time_keyboard=one_time_keyboard, selective=selective, - conf={'row_width': row_width}) + super().__init__( + keyboard=keyboard, + resize_keyboard=resize_keyboard, + one_time_keyboard=one_time_keyboard, + input_field_placeholder=input_field_placeholder, + selective=selective, + conf={'row_width': row_width}, + ) @property def row_width(self): diff --git a/docs/source/index.rst b/docs/source/index.rst index 3631b150..cd4b99d0 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-5.2-blue.svg?style=flat-square&logo=telegram + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.3-blue.svg?style=flat-square&logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API diff --git a/tests/test_bot.py b/tests/test_bot.py index 224666ec..61abe962 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -427,7 +427,7 @@ async def test_get_chat_administrators(bot: Bot): """ getChatAdministrators method test """ from .types.dataset import CHAT, CHAT_MEMBER chat = types.Chat(**CHAT) - member = types.ChatMember(**CHAT_MEMBER) + member = types.ChatMember.resolve(**CHAT_MEMBER) async with FakeTelegram(message_data=[CHAT_MEMBER, CHAT_MEMBER]): result = await bot.get_chat_administrators(chat_id=chat.id) @@ -435,14 +435,14 @@ async def test_get_chat_administrators(bot: Bot): assert len(result) == 2 -async def test_get_chat_members_count(bot: Bot): +async def test_get_chat_member_count(bot: Bot): """ getChatMembersCount method test """ from .types.dataset import CHAT chat = types.Chat(**CHAT) count = 5 async with FakeTelegram(message_data=count): - result = await bot.get_chat_members_count(chat_id=chat.id) + result = await bot.get_chat_member_count(chat_id=chat.id) assert result == count @@ -450,7 +450,7 @@ async def test_get_chat_member(bot: Bot): """ getChatMember method test """ from .types.dataset import CHAT, CHAT_MEMBER chat = types.Chat(**CHAT) - member = types.ChatMember(**CHAT_MEMBER) + member = types.ChatMember.resolve(**CHAT_MEMBER) async with FakeTelegram(message_data=CHAT_MEMBER): result = await bot.get_chat_member(chat_id=chat.id, user_id=member.user.id) diff --git a/tests/types/test_chat_member.py b/tests/types/test_chat_member.py index 2cea44ce..2fe3e677 100644 --- a/tests/types/test_chat_member.py +++ b/tests/types/test_chat_member.py @@ -1,7 +1,7 @@ from aiogram import types from .dataset import CHAT_MEMBER -chat_member = types.ChatMember(**CHAT_MEMBER) +chat_member = types.ChatMember.resolve(**CHAT_MEMBER) def test_export(): From af031431f5c269b3febd8a003ef64b3b6eab91a6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 5 Jul 2021 02:42:51 +0300 Subject: [PATCH 106/118] Fixed BotCommandScopeType helper mode --- aiogram/types/bot_command_scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/bot_command_scope.py b/aiogram/types/bot_command_scope.py index e3091a7e..cb9bc78e 100644 --- a/aiogram/types/bot_command_scope.py +++ b/aiogram/types/bot_command_scope.py @@ -5,7 +5,7 @@ from ..utils import helper class BotCommandScopeType(helper.Helper): - mode = helper.HelperMode.lowercase + mode = helper.HelperMode.snake_case DEFAULT = helper.Item() # default ALL_PRIVATE_CHATS = helper.Item() # all_private_chats From 64a7a781dacf179fff45900a34963a54dddbd50c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 5 Jul 2021 02:43:32 +0300 Subject: [PATCH 107/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 2d852a7e..e04c6870 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.14' +__version__ = '2.14.1' __api_version__ = '5.3' From 2e207c636cdc59e762c90c0f5a3ee02f8583f4b6 Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Tue, 6 Jul 2021 01:10:51 +0300 Subject: [PATCH 108/118] fix fault on `reset_state` in memory storage (#619) --- aiogram/contrib/fsm_storage/memory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/contrib/fsm_storage/memory.py b/aiogram/contrib/fsm_storage/memory.py index 8950aa8e..a5686a34 100644 --- a/aiogram/contrib/fsm_storage/memory.py +++ b/aiogram/contrib/fsm_storage/memory.py @@ -105,6 +105,7 @@ class MemoryStorage(BaseStorage): self.data[chat][user]['bucket'].update(bucket, **kwargs) def _cleanup(self, chat, user): + chat, user = self.resolve_address(chat=chat, user=user) if self.data[chat][user] == {'state': None, 'data': {}, 'bucket': {}}: del self.data[chat][user] if not self.data[chat]: From 899228904a98c3e8d0b337406387be3f2cedf154 Mon Sep 17 00:00:00 2001 From: alfred richardsn Date: Mon, 5 Jul 2021 22:12:52 +0000 Subject: [PATCH 109/118] Fix unused default locale in I18nMiddleware (#562) (#563) Co-authored-by: Alex Root Junior --- aiogram/contrib/middlewares/i18n.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 651b77de..1c50a603 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -132,11 +132,11 @@ class I18nMiddleware(BaseMiddleware): user: Optional[types.User] = types.User.get_current() locale: Optional[Locale] = user.locale if user else None - if locale: + if locale and locale.language in self.locales: *_, data = args language = data['locale'] = locale.language return language - return None + return self.default async def trigger(self, action, args): """ From 7edf9e77b6866a8076916a12d0b4c5691136b503 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 6 Jul 2021 01:23:51 +0300 Subject: [PATCH 110/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index e04c6870..d1d71744 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.14.1' +__version__ = '2.14.2' __api_version__ = '5.3' From a26c6428a3a1c06665f0fa5e9a8853b4ad8add30 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 6 Jul 2021 13:20:52 +0300 Subject: [PATCH 111/118] fix: chat_member status check (#621) --- aiogram/types/chat_member.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 58e4cb62..71d6d755 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -81,6 +81,15 @@ class ChatMember(base.TelegramObject): return class_(**kwargs) + def is_chat_creator(self) -> bool: + return ChatMemberStatus.is_chat_creator(self.status) + + def is_chat_admin(self) -> bool: + return ChatMemberStatus.is_chat_admin(self.status) + + def is_chat_member(self) -> bool: + return ChatMemberStatus.is_chat_member(self.status) + class ChatMemberOwner(ChatMember): """ From 2b4e3ad5c661956a8d7edaae72164f6b189e2fec Mon Sep 17 00:00:00 2001 From: Oleg A Date: Wed, 7 Jul 2021 02:21:15 +0300 Subject: [PATCH 112/118] fix: optional list field serialize-deserialize (#622) --- aiogram/types/fields.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index 022b9b72..40b0da50 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -129,6 +129,9 @@ class ListField(Field): super(ListField, self).__init__(*args, default=default, **kwargs) def serialize(self, value): + if value is None: + return None + result = [] serialize = super(ListField, self).serialize for item in value: @@ -136,6 +139,9 @@ class ListField(Field): return result def deserialize(self, value, parent=None): + if value is None: + return None + result = [] deserialize = super(ListField, self).deserialize for item in value: From 7a9bab01eb00e8d41d623e12cc7d66e1ed87389f Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 10 Jul 2021 22:43:36 +0300 Subject: [PATCH 113/118] fix: deserialize via to_object method (#624) --- aiogram/types/base.py | 8 ++++++-- aiogram/types/chat_member.py | 11 +++++++++++ aiogram/types/fields.py | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 0ed8579c..4da3e9f4 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -139,14 +139,18 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): return type(self).telegram_types @classmethod - def to_object(cls: typing.Type[T], data: typing.Dict[str, typing.Any]) -> T: + def to_object(cls: typing.Type[T], + data: typing.Dict[str, typing.Any], + conf: typing.Dict[str, typing.Any] = None + ) -> T: """ Deserialize object :param data: + :param conf: :return: """ - return cls(**data) + return cls(conf=conf, **data) @property def bot(self) -> Bot: diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 71d6d755..372b3468 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,4 +1,5 @@ import datetime +import typing from typing import Optional from . import base, fields @@ -6,6 +7,9 @@ from .user import User from ..utils import helper +T = typing.TypeVar('T') + + class ChatMemberStatus(helper.Helper): """ Chat member status @@ -81,6 +85,13 @@ class ChatMember(base.TelegramObject): return class_(**kwargs) + @classmethod + def to_object(cls, + data: typing.Dict[str, typing.Any], + conf: typing.Dict[str, typing.Any] = None + ) -> "ChatMember": + return cls.resolve(**data) + def is_chat_creator(self) -> bool: return ChatMemberStatus.is_chat_creator(self.status) diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index 40b0da50..7994f04e 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -112,7 +112,7 @@ class Field(BaseField): and not hasattr(value, 'to_python'): if not isinstance(parent, weakref.ReferenceType): parent = weakref.ref(parent) - return self.base_object(conf={'parent':parent}, **value) + return self.base_object.to_object(conf={'parent': parent}, data=value) return value From 04a64fbfc5c386dfb005a1a620c192ebbedab727 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 10 Jul 2021 23:06:27 +0300 Subject: [PATCH 114/118] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index d1d71744..ceca7f58 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -43,5 +43,5 @@ __all__ = ( 'utils', ) -__version__ = '2.14.2' +__version__ = '2.14.3' __api_version__ = '5.3' From 70aa80abf160ccad8f317b4f22fd4e1380d80cfa Mon Sep 17 00:00:00 2001 From: Almaz Date: Sun, 18 Jul 2021 14:20:07 +0300 Subject: [PATCH 115/118] Simplify some conditions (#632) --- aiogram/dispatcher/dispatcher.py | 46 ++++++++++++--------------- aiogram/dispatcher/filters/builtin.py | 7 ++-- aiogram/dispatcher/webhook.py | 7 ++-- aiogram/types/inline_keyboard.py | 2 +- aiogram/types/input_file.py | 11 ++----- aiogram/types/message_entity.py | 8 ++--- aiogram/types/reply_keyboard.py | 2 +- aiogram/utils/callback_data.py | 5 ++- aiogram/utils/helper.py | 5 +-- 9 files changed, 38 insertions(+), 55 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 8231c4f7..5e696f19 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -330,9 +330,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): def _loop_create_task(self, coro): if self._main_loop is None: return asyncio.create_task(coro) - else: - _ensure_loop(self._main_loop) - return self._main_loop.create_task(coro) + _ensure_loop(self._main_loop) + return self._main_loop.create_task(coro) async def start_polling(self, timeout=20, @@ -1394,29 +1393,26 @@ class Dispatcher(DataMixin, ContextInstanceMixin): no_error=True) if is_not_throttled: return await func(*args, **kwargs) - else: - kwargs.update( - { - 'rate': rate, - 'key': key, - 'user_id': user_id, - 'chat_id': chat_id - } - ) # update kwargs with parameters which were given to throttled + kwargs.update( + { + 'rate': rate, + 'key': key, + 'user_id': user_id, + 'chat_id': chat_id, + } + ) # update kwargs with parameters which were given to throttled - if on_throttled: - if asyncio.iscoroutinefunction(on_throttled): - await on_throttled(*args, **kwargs) - else: - kwargs.update( - { - 'loop': asyncio.get_running_loop() - } - ) - partial_func = functools.partial(on_throttled, *args, **kwargs) - asyncio.get_running_loop().run_in_executor(None, - partial_func - ) + if on_throttled: + if asyncio.iscoroutinefunction(on_throttled): + await on_throttled(*args, **kwargs) + else: + kwargs.update({'loop': asyncio.get_running_loop()}) + partial_func = functools.partial( + on_throttled, *args, **kwargs + ) + asyncio.get_running_loop().run_in_executor( + None, partial_func + ) return wrapped return decorator diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 457de182..7a21ca3f 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -279,9 +279,10 @@ class Text(Filter): elif check == 0: raise ValueError(f"No one mode is specified!") - equals, contains, endswith, startswith = map(lambda e: [e] if isinstance(e, str) or isinstance(e, LazyProxy) - else e, - (equals, contains, endswith, startswith)) + equals, contains, endswith, startswith = map( + lambda e: [e] if isinstance(e, (str, LazyProxy)) else e, + (equals, contains, endswith, startswith), + ) self.equals = equals self.contains = contains self.endswith = endswith diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index bc21e22c..c76ffae2 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -188,10 +188,9 @@ class WebhookRequestHandler(web.View): if fut.done(): return fut.result() - else: - # context.set_value(WEBHOOK_CONNECTION, False) - fut.remove_done_callback(cb) - fut.add_done_callback(self.respond_via_request) + # context.set_value(WEBHOOK_CONNECTION, False) + fut.remove_done_callback(cb) + fut.add_done_callback(self.respond_via_request) finally: timeout_handle.cancel() diff --git a/aiogram/types/inline_keyboard.py b/aiogram/types/inline_keyboard.py index 97ad35da..49a947be 100644 --- a/aiogram/types/inline_keyboard.py +++ b/aiogram/types/inline_keyboard.py @@ -50,7 +50,7 @@ class InlineKeyboardMarkup(base.TelegramObject): if index % self.row_width == 0: self.inline_keyboard.append(row) row = [] - if len(row) > 0: + if row: self.inline_keyboard.append(row) return self diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index 3a78c499..c974025a 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -41,13 +41,9 @@ class InputFile(base.TelegramObject): self._path = path_or_bytesio if filename is None: filename = os.path.split(path_or_bytesio)[-1] - elif isinstance(path_or_bytesio, io.IOBase): + elif isinstance(path_or_bytesio, (io.IOBase, _WebPipe)): self._path = None self._file = path_or_bytesio - elif isinstance(path_or_bytesio, _WebPipe): - self._path = None - self._file = path_or_bytesio - elif isinstance(path_or_bytesio, Path): self._file = path_or_bytesio.open("rb") self._path = path_or_bytesio.resolve() @@ -174,10 +170,7 @@ class _WebPipe: def name(self): if not self._name: *_, part = self.url.rpartition('/') - if part: - self._name = part - else: - self._name = secrets.token_urlsafe(24) + self._name = part or secrets.token_urlsafe(24) return self._name async def open(self): diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 58705265..9ee98c11 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -50,11 +50,9 @@ class MessageEntity(base.TelegramObject): if sys.maxunicode == 0xFFFF: return text[self.offset : self.offset + self.length] - if not isinstance(text, bytes): - entity_text = text.encode("utf-16-le") - else: - entity_text = text - + entity_text = ( + text.encode("utf-16-le") if not isinstance(text, bytes) else text + ) entity_text = entity_text[self.offset * 2 : (self.offset + self.length) * 2] return entity_text.decode("utf-16-le") diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index e648e036..7bfdb8e6 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -67,7 +67,7 @@ class ReplyKeyboardMarkup(base.TelegramObject): if index % self.row_width == 0: self.keyboard.append(row) row = [] - if len(row) > 0: + if row: self.keyboard.append(row) return self diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py index e24ad7b1..d44fa5b9 100644 --- a/aiogram/utils/callback_data.py +++ b/aiogram/utils/callback_data.py @@ -130,7 +130,6 @@ class CallbackDataFilter(Filter): if isinstance(value, (list, tuple, set, frozenset)): if data.get(key) not in value: return False - else: - if data.get(key) != value: - return False + elif data.get(key) != value: + return False return {'callback_data': data} diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index 735afe5d..2189d1dc 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -103,10 +103,7 @@ class HelperMode(Helper): if symbol == '_' and pos > 0: need_upper = True else: - if need_upper: - result += symbol.upper() - else: - result += symbol.lower() + result += symbol.upper() if need_upper else symbol.lower() need_upper = False if first_upper: result = result[0].upper() + result[1:] From ebd7f79326e272d66b2fe2b3d89cec8795aa8b5b Mon Sep 17 00:00:00 2001 From: Almaz Date: Sun, 18 Jul 2021 14:20:49 +0300 Subject: [PATCH 116/118] Replace yield inside for loop with yield from (#631) --- aiogram/types/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 4da3e9f4..5ef774dd 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -253,8 +253,7 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): :return: """ - for item in self.to_python().items(): - yield item + yield from self.to_python().items() def iter_keys(self) -> typing.Generator[typing.Any, None, None]: """ From 82d10844d75cda590dfad56cda45ba58ce01cbdf Mon Sep 17 00:00:00 2001 From: Almaz Date: Sun, 18 Jul 2021 14:23:18 +0300 Subject: [PATCH 117/118] Replace for loops with comprehensions (#629) --- aiogram/contrib/fsm_storage/mongo.py | 9 +-------- aiogram/dispatcher/dispatcher.py | 4 +--- aiogram/dispatcher/filters/factory.py | 10 +++++++--- aiogram/dispatcher/filters/filters.py | 4 ++-- aiogram/types/fields.py | 20 ++++---------------- aiogram/types/inline_keyboard.py | 4 +--- aiogram/types/reply_keyboard.py | 4 +--- aiogram/utils/helper.py | 12 ++++++++---- 8 files changed, 25 insertions(+), 42 deletions(-) diff --git a/aiogram/contrib/fsm_storage/mongo.py b/aiogram/contrib/fsm_storage/mongo.py index ab7d3176..0055743a 100644 --- a/aiogram/contrib/fsm_storage/mongo.py +++ b/aiogram/contrib/fsm_storage/mongo.py @@ -213,12 +213,5 @@ class MongoStorage(BaseStorage): :return: list of tuples where first element is chat id and second is user id """ db = await self.get_db() - result = [] - items = await db[STATE].find().to_list() - for item in items: - result.append( - (int(item['chat']), int(item['user'])) - ) - - return result + return [(int(item['chat']), int(item['user'])) for item in items] diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 5e696f19..1e36f202 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -232,9 +232,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): :return: """ if fast: - tasks = [] - for update in updates: - tasks.append(self.updates_handler.notify(update)) + tasks = [self.updates_handler.notify(update) for update in updates] return await asyncio.gather(*tasks) results = [] diff --git a/aiogram/dispatcher/filters/factory.py b/aiogram/dispatcher/filters/factory.py index 564e7f89..d53047d5 100644 --- a/aiogram/dispatcher/filters/factory.py +++ b/aiogram/dispatcher/filters/factory.py @@ -48,9 +48,13 @@ class FiltersFactory: :param full_config: :return: """ - filters_set = [] - filters_set.extend(self._resolve_registered(event_handler, - {k: v for k, v in full_config.items() if v is not None})) + filters_set = list( + self._resolve_registered( + event_handler, + {k: v for k, v in full_config.items() if v is not None}, + ) + ) + if custom_filters: filters_set.extend(custom_filters) diff --git a/aiogram/dispatcher/filters/filters.py b/aiogram/dispatcher/filters/filters.py index 220ef96c..47a4f22e 100644 --- a/aiogram/dispatcher/filters/filters.py +++ b/aiogram/dispatcher/filters/filters.py @@ -242,7 +242,7 @@ class NotFilter(_LogicFilter): class AndFilter(_LogicFilter): def __init__(self, *targets): - self.targets = list(wrap_async(target) for target in targets) + self.targets = [wrap_async(target) for target in targets] async def check(self, *args): """ @@ -268,7 +268,7 @@ class AndFilter(_LogicFilter): class OrFilter(_LogicFilter): def __init__(self, *targets): - self.targets = list(wrap_async(target) for target in targets) + self.targets = [wrap_async(target) for target in targets] async def check(self, *args): """ diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index 7994f04e..f898fc62 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -131,22 +131,14 @@ class ListField(Field): def serialize(self, value): if value is None: return None - - result = [] serialize = super(ListField, self).serialize - for item in value: - result.append(serialize(item)) - return result + return [serialize(item) for item in value] def deserialize(self, value, parent=None): if value is None: return None - - result = [] deserialize = super(ListField, self).deserialize - for item in value: - result.append(deserialize(item, parent=parent)) - return result + return [deserialize(item, parent=parent) for item in value] class ListOfLists(Field): @@ -154,9 +146,7 @@ class ListOfLists(Field): result = [] serialize = super(ListOfLists, self).serialize for row in value: - row_result = [] - for item in row: - row_result.append(serialize(item)) + row_result = [serialize(item) for item in row] result.append(row_result) return result @@ -165,9 +155,7 @@ class ListOfLists(Field): deserialize = super(ListOfLists, self).deserialize if hasattr(value, '__iter__'): for row in value: - row_result = [] - for item in row: - row_result.append(deserialize(item, parent=parent)) + row_result = [deserialize(item, parent=parent) for item in row] result.append(row_result) return result diff --git a/aiogram/types/inline_keyboard.py b/aiogram/types/inline_keyboard.py index 49a947be..195c1e67 100644 --- a/aiogram/types/inline_keyboard.py +++ b/aiogram/types/inline_keyboard.py @@ -62,9 +62,7 @@ class InlineKeyboardMarkup(base.TelegramObject): :return: self :rtype: :obj:`types.InlineKeyboardMarkup` """ - btn_array = [] - for button in args: - btn_array.append(button) + btn_array = [button for button in args] self.inline_keyboard.append(btn_array) return self diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index 7bfdb8e6..8455aff6 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -79,9 +79,7 @@ class ReplyKeyboardMarkup(base.TelegramObject): :return: self :rtype: :obj:`types.ReplyKeyboardMarkup` """ - btn_array = [] - for button in args: - btn_array.append(button) + btn_array = [button for button in args] self.keyboard.append(btn_array) return self diff --git a/aiogram/utils/helper.py b/aiogram/utils/helper.py index 2189d1dc..55a134a3 100644 --- a/aiogram/utils/helper.py +++ b/aiogram/utils/helper.py @@ -198,10 +198,14 @@ 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) + props_keys = [ + prop_name + for prop_name in ( + name + for name, prop in namespace.items() + if isinstance(prop, (Item, ListItem)) + ) + ] setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys) From 29d2ffc9ed32ea8955f98866f2f02a67fecd330e Mon Sep 17 00:00:00 2001 From: Oleg A Date: Sat, 24 Jul 2021 23:00:21 +0300 Subject: [PATCH 118/118] fix: add missed ChatMember{x} in types (#636) --- aiogram/types/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index a9e6af8c..1b289698 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -13,7 +13,9 @@ from .callback_query import CallbackQuery from .chat import Chat, ChatActions, ChatType from .chat_invite_link import ChatInviteLink from .chat_location import ChatLocation -from .chat_member import ChatMember, ChatMemberStatus +from .chat_member import ChatMember, ChatMemberAdministrator, ChatMemberBanned, \ + ChatMemberLeft, ChatMemberMember, ChatMemberOwner, ChatMemberRestricted, \ + ChatMemberStatus from .chat_member_updated import ChatMemberUpdated from .chat_permissions import ChatPermissions from .chat_photo import ChatPhoto @@ -104,6 +106,12 @@ __all__ = ( 'ChatMember', 'ChatMemberStatus', 'ChatMemberUpdated', + 'ChatMemberOwner', + 'ChatMemberAdministrator', + 'ChatMemberMember', + 'ChatMemberRestricted', + 'ChatMemberLeft', + 'ChatMemberBanned', 'ChatPermissions', 'ChatPhoto', 'ChatType',