From 79677cb20fdcb77c26ee99433566ad114f547e90 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 22 Feb 2019 18:39:02 +0300 Subject: [PATCH 01/85] Fixed entities parsing for captions --- 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 4f5e0914..710220ad 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -193,7 +193,7 @@ class Message(base.TelegramObject): quote_fn = md.quote_html if as_html else md.escape_md - if not self.entities: + if not (self.entities or self.caption_entities): return quote_fn(text) if not sys.maxunicode == 0xffff: @@ -202,7 +202,7 @@ class Message(base.TelegramObject): result = '' offset = 0 - for entity in sorted(self.entities, key=lambda item: item.offset): + for entity in sorted(self.entities or self.caption_entities, key=lambda item: item.offset): entity_text = entity.parse(text, as_html=as_html) if sys.maxunicode == 0xffff: From 98173f64e1a98189820623dfb8ff27a6a11919c8 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 22 Feb 2019 18:47:09 +0300 Subject: [PATCH 02/85] Simplify code --- aiogram/types/message.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 710220ad..79980523 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -192,8 +192,9 @@ class Message(base.TelegramObject): raise TypeError("This message doesn't have any text.") quote_fn = md.quote_html if as_html else md.escape_md - - if not (self.entities or self.caption_entities): + + entities = self.entities or self.caption_entities + if not entities: return quote_fn(text) if not sys.maxunicode == 0xffff: @@ -202,7 +203,7 @@ class Message(base.TelegramObject): result = '' offset = 0 - for entity in sorted(self.entities or self.caption_entities, key=lambda item: item.offset): + for entity in sorted(entities, key=lambda item: item.offset): entity_text = entity.parse(text, as_html=as_html) if sys.maxunicode == 0xffff: From 0045eaca24825c9632735dd06b5ee97c885ba357 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sun, 24 Feb 2019 21:41:27 +0300 Subject: [PATCH 03/85] Some useful Message edit functions Message.edit_caption Message.edit_media Message.edit_reply_markup --- aiogram/types/message.py | 63 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 79980523..b163f548 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -13,7 +13,7 @@ from .chat import Chat, ChatType from .contact import Contact from .document import Document from .game import Game -from .input_media import MediaGroup +from .input_media import MediaGroup, InputMedia from .invoice import Invoice from .location import Location from .message_entity import MessageEntity @@ -776,6 +776,67 @@ class Message(base.TelegramObject): disable_web_page_preview=disable_web_page_preview, reply_markup=reply_markup) + async def edit_caption(self, caption: base.String, + parse_mode: typing.Union[base.String, None] = None, + reply_markup=None): + """ + 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]` + :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. + :rtype: :obj:`typing.Union[types.Message, base.Boolean]` + """ + return await self.bot.edit_message_caption(chat_id=self.chat.id, message_id=self.message_id, caption=caption, + parse_mode=parse_mode, reply_markup=reply_markup) + + async def edit_media(self, media: InputMedia, reply_markup=None): + """ + Use this method to edit audio, document, photo, or video messages. + If a message is a part of a message album, then it can be edited only to a photo or a video. + Otherwise, message type can be changed arbitrarily. + When inline message is edited, new file can't be uploaded. + Use previously uploaded file via its file_id or specify a URL. + + On success, if the edited message was sent by the bot, + the edited Message is returned, otherwise True is returned. + + Source https://core.telegram.org/bots/api#editmessagemedia + + :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]` + :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]` + """ + return await self.bot.edit_message_media(media=media, chat_id=self.chat.id, message_id=self.message_id, + reply_markup=reply_markup) + + async def edit_reply_markup(self, reply_markup=None): + """ + Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). + + 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]` + :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_reply_markup(chat_id=self.chat.id, message_id=self.message_id, + reply_markup=reply_markup) + async def delete(self): """ Delete this message From 3a38125619fa473eb0568969ccb85a9cb3279791 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Mon, 25 Feb 2019 12:34:26 +0300 Subject: [PATCH 04/85] Check bot instance on Dispatcher __init__ --- aiogram/dispatcher/dispatcher.py | 5 ++++- tests/test_dispatcher.py | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/test_dispatcher.py diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 035ec1f5..d1fc72ba 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -35,6 +35,9 @@ class Dispatcher(DataMixin, ContextInstanceMixin): throttling_rate_limit=DEFAULT_RATE_LIMIT, no_throttle_error=False, filters_factory=None): + 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: @@ -276,7 +279,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): :return: """ - if self._polling: + if hasattr(self, '_polling') and self._polling: log.info('Stop polling...') self._polling = False diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py new file mode 100644 index 00000000..3dec03e7 --- /dev/null +++ b/tests/test_dispatcher.py @@ -0,0 +1,35 @@ +from aiogram import Dispatcher, Bot + +import pytest + +pytestmark = pytest.mark.asyncio + + +@pytest.yield_fixture() +async def bot(event_loop): + """ Bot fixture """ + _bot = Bot(token='123456789:AABBCCDDEEFFaabbccddeeff-1234567890', + loop=event_loop) + yield _bot + await _bot.close() + + +class TestDispatcherInit: + async def test_successful_init(self, bot): + """ + Success __init__ case + + :param bot: bot instance + :type bot: Bot + """ + dp = Dispatcher(bot=bot) + assert isinstance(dp, Dispatcher) + + @pytest.mark.parametrize("bot_instance", [None, Bot, 123, 'abc']) + async def test_wrong_bot_instance(self, bot_instance): + """ + User provides wrong data to 'bot' argument. + :return: TypeError with reason + """ + with pytest.raises(TypeError): + _ = Dispatcher(bot=bot_instance) From 4e4bbdfc7e6bf9888da9cc96241416a6317cf78d Mon Sep 17 00:00:00 2001 From: Oleg A Date: Mon, 25 Feb 2019 13:23:50 +0300 Subject: [PATCH 05/85] Raise Exception if there's no bot instance in context --- aiogram/types/base.py | 8 +++++++- tests/__init__.py | 39 +++++++++++++++++++++++++++++++++++ tests/test_message.py | 47 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/test_message.py diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 8125a37d..4514e956 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -142,7 +142,13 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): @property def bot(self): from ..bot.bot import Bot - return Bot.get_current() + + bot = Bot.get_current() + if bot is None: + raise RuntimeError("Can't get bot instance from context. " + "You can fix it with setting current instance: " + "'Bot.set_current(bot_instance)'") + return bot def to_python(self) -> typing.Dict: """ diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..cb0c6b9d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,39 @@ +import aresponses +from aiogram import Bot + +TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' + + +class FakeTelegram(aresponses.ResponsesMockServer): + def __init__(self, message_dict, bot=None, **kwargs): + super().__init__(**kwargs) + self._body, self._headers = self.parse_data(message_dict) + + if isinstance(bot, Bot): + Bot.set_current(bot) + + async def __aenter__(self): + await super().__aenter__() + _response = self.Response(text=self._body, headers=self._headers, status=200, reason='OK') + self.add(self.ANY, response=_response) + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if hasattr(self, 'monkeypatch'): + self.monkeypatch.undo() + await super().__aexit__(exc_type, exc_val, exc_tb) + + @staticmethod + def parse_data(message_dict): + import json + + _body = '{"ok":true,"result":' + json.dumps(message_dict) + '}' + _headers = {'Server': 'nginx/1.12.2', + 'Date': 'Tue, 03 Apr 2018 16:59:54 GMT', + 'Content-Type': 'application/json', + 'Content-Length': str(len(_body)), + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Expose-Headers': 'Content-Length,Content-Type,Date,Server,Connection', + 'Strict-Transport-Security': 'max-age=31536000; includeSubdomains'} + return _body, _headers diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 00000000..996529f3 --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,47 @@ +from asyncio import BaseEventLoop + +import pytest + +from aiogram import Bot, types +from . import FakeTelegram, TOKEN + +pytestmark = pytest.mark.asyncio + + +@pytest.yield_fixture() +async def bot(event_loop): + """ Bot fixture """ + _bot = Bot(TOKEN, loop=event_loop, parse_mode=types.ParseMode.HTML) + yield _bot + await _bot.close() + + +@pytest.yield_fixture() +async def message(bot, event_loop): + """ + Message fixture + :param bot: Telegram bot fixture + :type bot: Bot + :param event_loop: asyncio event loop + :type event_loop: BaseEventLoop + """ + from .types.dataset import MESSAGE + msg = types.Message(**MESSAGE) + + async with FakeTelegram(message_dict=MESSAGE, loop=event_loop): + _message = await bot.send_message(chat_id=msg.chat.id, text=msg.text) + + yield _message + + +class TestMiscCases: + async def test_calling_bot_not_from_context(self, message): + """ + Calling any helper method without bot instance in context. + + :param message: message fixture + :type message: types.Message + :return: RuntimeError with reason and help + """ + with pytest.raises(RuntimeError): + await message.edit_text('test_calling_bot_not_from_context') From 7c0ada58977d20729e11ed6d8bfa8ee165d4dd84 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Mon, 25 Feb 2019 15:43:18 +0300 Subject: [PATCH 06/85] Replaced 'ssl_context' kwarg with 'ssl' cause: DeprecationWarning: ssl_context is deprecated, use ssl=context instead --- aiogram/bot/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index d612a13b..cab491d1 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -77,8 +77,7 @@ class BaseBot: self.proxy = None self.proxy_auth = None else: - connector = aiohttp.TCPConnector(limit=connections_limit, ssl_context=ssl_context, - loop=self.loop) + connector = aiohttp.TCPConnector(limit=connections_limit, ssl=ssl_context, loop=self.loop) self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, json_serialize=json.dumps) From 3a947407fcf1a27a54ac23d7d9fc0becc09d2796 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 10 Mar 2019 22:04:19 +0500 Subject: [PATCH 07/85] More "await" in RedisStorage and RedisStorage2 --- aiogram/contrib/fsm_storage/redis.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 8c1abd90..2bfe1642 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -173,10 +173,10 @@ class RedisStorage(BaseStorage): conn = await self.redis() if full: - conn.execute('FLUSHDB') + await conn.execute('FLUSHDB') else: keys = await conn.execute('KEYS', 'fsm:*') - conn.execute('DEL', *keys) + await conn.execute('DEL', *keys) def has_bucket(self): return True @@ -350,10 +350,10 @@ class RedisStorage2(BaseStorage): conn = await self.redis() if full: - conn.flushdb() + await conn.flushdb() else: keys = await conn.keys(self.generate_key('*')) - conn.delete(*keys) + await conn.delete(*keys) async def get_states_list(self) -> typing.List[typing.Tuple[int]]: """ From 39c8d859dc8bef464276013a96bbf50d15c485c5 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 16 Mar 2019 01:07:24 +0200 Subject: [PATCH 08/85] Implement logging filter for extending LogRecord by the data from Telegram Update object --- aiogram/contrib/middlewares/logging.py | 282 ++++++++++++++++++++++++- 1 file changed, 281 insertions(+), 1 deletion(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index ca6b628f..5d14e61f 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -1,6 +1,7 @@ -import logging import time +import logging + from aiogram import types from aiogram.dispatcher.middlewares import BaseMiddleware @@ -139,3 +140,282 @@ class LoggingMiddleware(BaseMiddleware): timeout = self.check_timeout(update) if timeout > 0: self.logger.info(f"Process update [ID:{update.update_id}]: [failed] (in {timeout} ms)") + + +class LoggingFilter(logging.Filter): + """ + Extend LogRecord by data from Telegram Update object. + + Can be used in logging config: + .. code-block: python3 + + 'filters': { + 'telegram': { + '()': LoggingFilter, + 'include_content': True, + } + }, + ... + 'handlers': { + 'graypy': { + '()': GELFRabbitHandler, + 'url': 'amqp://localhost:5672/', + 'routing_key': '#', + 'localname': 'testapp' + }, + }, + + """ + + def __init__(self, name='', prefix='tg', include_content=False): + """ + :param name: + :param prefix: prefix for all records + :param include_content: pass into record all data from Update object + """ + super(LoggingFilter, self).__init__(name=name) + + self.prefix = prefix + self.include_content = include_content + + def filter(self, record: logging.LogRecord): + """ + Extend LogRecord by data from Telegram Update object. + + :param record: + :return: + """ + update = types.Update.get_current(True) + if update: + for key, value in self.make_prefix(self.prefix, self.process_update(update)): + setattr(self, key, value) + + return True + + def process_update(self, update: types.Update): + """ + Parse Update object + + :param update: + :return: + """ + yield 'update_id', update.update_id + + if update.message: + yield 'update_type', 'message' + yield from self.process_message(update.message) + if update.edited_message: + yield 'update_type', 'edited_message' + yield from self.process_message(update.edited_message) + if update.channel_post: + yield 'update_type', 'channel_post' + yield from self.process_message(update.channel_post) + if update.edited_channel_post: + yield 'update_type', 'edited_channel_post' + yield from self.process_message(update.edited_channel_post) + if update.inline_query: + yield 'update_type', 'inline_query' + yield from self.process_inline_query(update.inline_query) + if update.chosen_inline_result: + yield 'update_type', 'chosen_inline_result' + yield from self.process_chosen_inline_result(update.chosen_inline_result) + if update.callback_query: + yield 'update_type', 'callback_query' + yield from self.process_callback_query(update.callback_query) + if update.shipping_query: + yield 'update_type', 'shipping_query' + yield from self.process_shipping_query(update.shipping_query) + if update.pre_checkout_query: + yield 'update_type', 'pre_checkout_query' + yield from self.process_pre_checkout_query(update.pre_checkout_query) + + def make_prefix(self, prefix, iterable): + """ + Add prefix to the label + + :param prefix: + :param iterable: + :return: + """ + if not prefix: + yield from iterable + + for key, value in iterable: + yield f"{prefix}_{key}", value + + def process_user(self, user: types.User): + """ + Generate user data + + :param user: + :return: + """ + if not user: + return + + yield 'user_id', user.id + if self.include_content: + yield 'user_full_name', user.full_name + if user.username: + yield 'user_name', f"@{user.username}" + + def process_chat(self, chat: types.Chat): + """ + Generate chat data + + :param chat: + :return: + """ + if not chat: + return + + yield 'chat_id', chat.id + yield 'chat_type', chat.type + if self.include_content: + yield 'chat_title', chat.full_name + if chat.username: + yield 'chat_name', f"@{chat.username}" + + def process_message(self, message: types.Message): + yield 'message_content_type', message.content_type + yield from self.process_user(message.from_user) + yield from self.process_chat(message.chat) + + if not self.include_content: + return + + if message.reply_to_message: + yield from self.make_prefix('reply_to', self.process_message(message.reply_to_message)) + if message.forward_from: + yield from self.make_prefix('forward_from', self.process_user(message.forward_from)) + if message.forward_from_chat: + yield from self.make_prefix('forward_from_chat', self.process_chat(message.forward_from_chat)) + if message.forward_from_message_id: + yield 'message_forward_from_message_id', message.forward_from_message_id + if message.forward_date: + yield 'message_forward_date', message.forward_date + if message.edit_date: + yield 'message_edit_date', message.edit_date + if message.media_group_id: + yield 'message_media_group_id', message.media_group_id + if message.author_signature: + yield 'message_author_signature', message.author_signature + + if message.text: + yield 'text', message.text or message.caption + yield 'html_text', message.html_text + elif message.audio: + yield 'audio', message.audio.file_id + elif message.animation: + yield 'animation', message.animation.file_id + elif message.document: + yield 'document', message.document.file_id + elif message.game: + yield 'game', message.game.title + elif message.photo: + yield 'photo', message.photo[-1].file_id + elif message.sticker: + yield 'sticker', message.sticker.file_id + elif message.video: + yield 'video', message.video.file_id + elif message.video_note: + yield 'video_note', message.video_note.file_id + elif message.voice: + yield 'voice', message.voice.file_id + elif message.contact: + yield 'contact_full_name', message.contact.full_name + yield 'contact_phone_number', message.contact.phone_number + elif message.venue: + yield 'venue_address', message.venue.address + yield 'location_latitude', message.venue.location.latitude + yield 'location_longitude', message.venue.location.longitude + elif message.location: + yield 'location_latitude', message.location.latitude + yield 'location_longitude', message.location.longitude + elif message.new_chat_members: + yield 'new_chat_members', [user.id for user in message.new_chat_members] + elif message.left_chat_member: + yield 'left_chat_member', [user.id for user in message.new_chat_members] + elif message.invoice: + yield 'invoice_title', message.invoice.title + yield 'invoice_description', message.invoice.description + yield 'invoice_start_parameter', message.invoice.start_parameter + yield 'invoice_currency', message.invoice.currency + yield 'invoice_total_amount', message.invoice.total_amount + elif message.successful_payment: + yield 'successful_payment_currency', message.successful_payment.currency + yield 'successful_payment_total_amount', message.successful_payment.total_amount + yield 'successful_payment_invoice_payload', message.successful_payment.invoice_payload + yield 'successful_payment_shipping_option_id', message.successful_payment.shipping_option_id + yield 'successful_payment_telegram_payment_charge_id', message.successful_payment.telegram_payment_charge_id + yield 'successful_payment_provider_payment_charge_id', message.successful_payment.provider_payment_charge_id + elif message.connected_website: + yield 'connected_website', message.connected_website + elif message.migrate_from_chat_id: + yield 'migrate_from_chat_id', message.migrate_from_chat_id + elif message.migrate_to_chat_id: + yield 'migrate_to_chat_id', message.migrate_to_chat_id + elif message.pinned_message: + yield from self.make_prefix('pinned_message', message.pinned_message) + elif message.new_chat_title: + yield 'new_chat_title', message.new_chat_title + elif message.new_chat_photo: + yield 'new_chat_photo', message.new_chat_photo[-1].file_id + # elif message.delete_chat_photo: + # yield 'delete_chat_photo', message.delete_chat_photo + # elif message.group_chat_created: + # yield 'group_chat_created', message.group_chat_created + # elif message.passport_data: + # yield 'passport_data', message.passport_data + + def process_inline_query(self, inline_query: types.InlineQuery): + yield 'inline_query_id', inline_query.id + yield from self.process_user(inline_query.from_user) + + if self.include_content: + yield 'inline_query_text', inline_query.query + if inline_query.location: + yield 'location_latitude', inline_query.location.latitude + yield 'location_longitude', inline_query.location.longitude + if inline_query.offset: + yield 'inline_query_offset', inline_query.offset + + def process_chosen_inline_result(self, chosen_inline_result: types.ChosenInlineResult): + yield 'chosen_inline_result_id', chosen_inline_result.result_id + yield from self.process_user(chosen_inline_result.from_user) + + if self.include_content: + yield 'inline_query_text', chosen_inline_result.query + if chosen_inline_result.location: + yield 'location_latitude', chosen_inline_result.location.latitude + yield 'location_longitude', chosen_inline_result.location.longitude + + def process_callback_query(self, callback_query: types.CallbackQuery): + yield from self.process_user(callback_query.from_user) + yield 'callback_query_data', callback_query.data + + if callback_query.message: + yield from self.process_message(callback_query.message) + if callback_query.inline_message_id: + yield 'callback_query_inline_message_id', callback_query.inline_message_id + if callback_query.chat_instance: + yield 'callback_query_chat_instance', callback_query.chat_instance + if callback_query.game_short_name: + yield 'callback_query_game_short_name', callback_query.game_short_name + + def process_shipping_query(self, shipping_query: types.ShippingQuery): + yield 'shipping_query_id', shipping_query.id + yield from self.process_user(shipping_query.from_user) + + if self.include_content: + yield 'shipping_query_invoice_payload', shipping_query.invoice_payload + + def process_pre_checkout_query(self, pre_checkout_query: types.PreCheckoutQuery): + yield 'pre_checkout_query_id', pre_checkout_query.id + yield from self.process_user(pre_checkout_query.from_user) + + if self.include_content: + yield 'pre_checkout_query_currency', pre_checkout_query.currency + yield 'pre_checkout_query_total_amount', pre_checkout_query.total_amount + yield 'pre_checkout_query_invoice_payload', pre_checkout_query.invoice_payload + yield 'pre_checkout_query_shipping_option_id', pre_checkout_query.shipping_option_id From 81935aead5c21d2a06a0be226e2d0447cb7d029b Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Fri, 22 Mar 2019 21:09:32 +0500 Subject: [PATCH 09/85] Replace "send" in method names with "reply" All reply-related methods used the prefix "reply", except for animation, venue and contact, which could be confusing --- aiogram/types/message.py | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index b163f548..d8ed2daa 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -192,7 +192,7 @@ class Message(base.TelegramObject): raise TypeError("This message doesn't have any text.") quote_fn = md.quote_html if as_html else md.escape_md - + entities = self.entities or self.caption_entities if not entities: return quote_fn(text) @@ -367,17 +367,17 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def send_animation(self, - animation: typing.Union[base.InputFile, base.String], - duration: typing.Union[base.Integer, None] = None, - width: typing.Union[base.Integer, None] = None, - height: typing.Union[base.Integer, None] = None, - thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, - caption: typing.Union[base.String, None] = None, - parse_mode: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + async def reply_animation(self, + animation: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -667,11 +667,11 @@ class Message(base.TelegramObject): return await self.bot.stop_message_live_location(chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) - async def send_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, - foursquare_id: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + async def reply_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, + foursquare_id: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> Message: """ Use this method to send information about a venue. @@ -706,11 +706,11 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def send_contact(self, phone_number: base.String, - first_name: base.String, last_name: typing.Union[base.String, None] = None, - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + async def reply_contact(self, phone_number: base.String, + first_name: base.String, last_name: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> Message: """ Use this method to send phone contacts. From 23d3d988135ba8c32e145ed611505fe8300c82c1 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Fri, 22 Mar 2019 21:50:41 +0500 Subject: [PATCH 10/85] Add warnings --- aiogram/types/message.py | 143 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index d8ed2daa..2dc29077 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -28,6 +28,7 @@ from .video_note import VideoNote from .voice import Voice from ..utils import helper from ..utils import markdown as md +from ..utils.deprecated import warn_deprecated class Message(base.TelegramObject): @@ -367,6 +368,69 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) + async def send_animation(self, + animation: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> Message: + """ + Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). + + On success, the sent Message is returned. + Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. + + Source https://core.telegram.org/bots/api#sendanimation + + :param animation: Animation to send. Pass a file_id as String to send an animation that exists + on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation + from the Internet, or upload a new animation using multipart/form-data + :type animation: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent animation in seconds + :type duration: :obj:`typing.Union[base.Integer, None]` + :param width: Animation width + :type width: :obj:`typing.Union[base.Integer, None]` + :param height: Animation height + :type height: :obj:`typing.Union[base.Integer, None]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 90. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` + :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters + :type caption: :obj:`typing.Union[base.String, None]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, + fixed-width text or inline URLs in the media caption + :type parse_mode: :obj:`typing.Union[base.String, None]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, + types.ReplyKeyboardRemove, types.ForceReply], None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned + :rtype: :obj:`types.Message` + """ + warn_deprecated('"Message.send_animation" method will be removed in 2.2 version.\n' + 'Use "Message.reply_animation" instead.', + stacklevel=8) + + return await self.bot.send_animation(self.chat.id, animation=animation, + duration=duration, + width=width, + height=height, + thumb=thumb, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup + ) + async def reply_animation(self, animation: typing.Union[base.InputFile, base.String], duration: typing.Union[base.Integer, None] = None, @@ -667,6 +731,49 @@ class Message(base.TelegramObject): return await self.bot.stop_message_live_location(chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) + async def send_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, + foursquare_id: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> Message: + """ + Use this method to send information about a venue. + + Source: https://core.telegram.org/bots/api#sendvenue + + :param latitude: Latitude of the venue + :type latitude: :obj:`base.Float` + :param longitude: Longitude of the venue + :type longitude: :obj:`base.Float` + :param title: Name of the venue + :type title: :obj:`base.String` + :param address: Address of the venue + :type address: :obj:`base.String` + :param foursquare_id: Foursquare identifier of the venue + :type foursquare_id: :obj:`typing.Union[base.String, None]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + warn_deprecated('"Message.send_venue" method will be removed in 2.2 version.\n' + 'Use "Message.reply_venue" instead.', + stacklevel=8) + + return await self.bot.send_venue(chat_id=self.chat.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + async def reply_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, foursquare_id: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, @@ -706,6 +813,42 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) + async def send_contact(self, phone_number: base.String, + first_name: base.String, last_name: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=True) -> Message: + """ + Use this method to send phone contacts. + + Source: https://core.telegram.org/bots/api#sendcontact + + :param phone_number: Contact's phone number + :type phone_number: :obj:`base.String` + :param first_name: Contact's first name + :type first_name: :obj:`base.String` + :param last_name: Contact's last name + :type last_name: :obj:`typing.Union[base.String, None]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + warn_deprecated('"Message.send_contact" method will be removed in 2.2 version.\n' + 'Use "Message.reply_contact" instead.', + stacklevel=8) + + return await self.bot.send_contact(chat_id=self.chat.id, + phone_number=phone_number, + first_name=first_name, last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + async def reply_contact(self, phone_number: base.String, first_name: base.String, last_name: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, From 183e664a758ffecd6c06f36d9a0a6abc11337823 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Fri, 22 Mar 2019 22:01:26 +0500 Subject: [PATCH 11/85] Add answer method This is the "reply" method with the name "answer" and the default argument reply=False --- aiogram/types/message.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 2dc29077..b211fd1d 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -276,6 +276,26 @@ class Message(base.TelegramObject): return md.hlink(text, url) return md.link(text, url) + async def answer(self, text, parse_mode=None, disable_web_page_preview=None, + disable_notification=None, reply_markup=None, reply=False) -> Message: + """ + Answer to this message + + :param text: str + :param parse_mode: str + :param disable_web_page_preview: bool + :param disable_notification: bool + :param reply_markup: + :param reply: fill 'reply_to_message_id' + :return: :class:`aiogram.types.Message` + """ + return await self.bot.send_message(chat_id=self.chat.id, text=text, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + async def reply(self, text, parse_mode=None, disable_web_page_preview=None, disable_notification=None, reply_markup=None, reply=True) -> Message: """ From 28dc56cc88480ccda7360554558362637ced085f Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sat, 23 Mar 2019 23:58:49 +0500 Subject: [PATCH 12/85] Add lazy_gettext method --- aiogram/contrib/middlewares/i18n.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 65cb1400..7b247d8a 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -4,6 +4,7 @@ from contextvars import ContextVar from typing import Any, Dict, Tuple from babel import Locale +from babel.support import LazyProxy from ... import types from ...dispatcher.middlewares import BaseMiddleware @@ -106,6 +107,9 @@ class I18nMiddleware(BaseMiddleware): else: return translator.ngettext(singular, plural, n) + def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy: + return LazyProxy(self.gettext, singular, plural, n, locale) + # noinspection PyMethodMayBeStatic,PyUnusedLocal async def get_user_locale(self, action: str, args: Tuple[Any]) -> str: """ From d41d38d49f0e794b038dc44388aaabe9d1795e05 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 24 Mar 2019 00:01:36 +0500 Subject: [PATCH 13/85] Update i18n.py --- aiogram/contrib/middlewares/i18n.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/aiogram/contrib/middlewares/i18n.py b/aiogram/contrib/middlewares/i18n.py index 7b247d8a..264bc653 100644 --- a/aiogram/contrib/middlewares/i18n.py +++ b/aiogram/contrib/middlewares/i18n.py @@ -108,6 +108,15 @@ class I18nMiddleware(BaseMiddleware): return translator.ngettext(singular, plural, n) def lazy_gettext(self, singular, plural=None, n=1, locale=None) -> LazyProxy: + """ + Lazy get text + + :param singular: + :param plural: + :param n: + :param locale: + :return: + """ return LazyProxy(self.gettext, singular, plural, n, locale) # noinspection PyMethodMayBeStatic,PyUnusedLocal From 088116e1c7622f15b0603a753d80f9376003a232 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 24 Mar 2019 00:15:09 +0500 Subject: [PATCH 14/85] Reformat code --- aiogram/dispatcher/webhook.py | 1 - aiogram/types/encrypted_passport_element.py | 3 ++- aiogram/types/passport_data.py | 5 +++-- aiogram/types/venue.py | 1 - aiogram/utils/auth_widget.py | 3 +-- aiogram/utils/exceptions.py | 6 +++--- aiogram/utils/mixins.py | 1 - examples/callback_data_factory.py | 6 +++--- examples/payments.py | 3 +-- examples/webhook_example.py | 3 ++- tests/__init__.py | 1 + tests/test_dispatcher.py | 4 ++-- 12 files changed, 18 insertions(+), 19 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 10a4b3c1..c739a789 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -9,7 +9,6 @@ from typing import Dict, List, Optional, Union from aiohttp import web from aiohttp.web_exceptions import HTTPGone - from .. import types from ..bot import api from ..types import ParseMode diff --git a/aiogram/types/encrypted_passport_element.py b/aiogram/types/encrypted_passport_element.py index bc7b212b..76e02ec1 100644 --- a/aiogram/types/encrypted_passport_element.py +++ b/aiogram/types/encrypted_passport_element.py @@ -1,6 +1,7 @@ +import typing + from . import base from . import fields -import typing from .passport_file import PassportFile diff --git a/aiogram/types/passport_data.py b/aiogram/types/passport_data.py index 06cbad1c..2fed9fae 100644 --- a/aiogram/types/passport_data.py +++ b/aiogram/types/passport_data.py @@ -1,8 +1,9 @@ +import typing + from . import base from . import fields -import typing -from .encrypted_passport_element import EncryptedPassportElement from .encrypted_credentials import EncryptedCredentials +from .encrypted_passport_element import EncryptedPassportElement class PassportData(base.TelegramObject): diff --git a/aiogram/types/venue.py b/aiogram/types/venue.py index 1b420d57..f7b2a277 100644 --- a/aiogram/types/venue.py +++ b/aiogram/types/venue.py @@ -14,4 +14,3 @@ class Venue(base.TelegramObject): address: base.String = fields.Field() foursquare_id: base.String = fields.Field() foursquare_type: base.String = fields.Field() - diff --git a/aiogram/utils/auth_widget.py b/aiogram/utils/auth_widget.py index 8b2eacf7..b9084eb1 100644 --- a/aiogram/utils/auth_widget.py +++ b/aiogram/utils/auth_widget.py @@ -4,11 +4,10 @@ for more information https://core.telegram.org/widgets/login#checking-authorizat Source: https://gist.github.com/JrooTJunior/887791de7273c9df5277d2b1ecadc839 """ +import collections import hashlib import hmac -import collections - def generate_hash(data: dict, token: str) -> str: """ diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 9e3bb6d2..24afe356 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -174,7 +174,7 @@ class MessageTextIsEmpty(MessageError): class MessageCantBeEdited(MessageError): match = 'message can\'t be edited' - + class MessageCantBeDeleted(MessageError): match = 'message can\'t be deleted' @@ -429,5 +429,5 @@ class Throttled(TelegramAPIError): def __str__(self): return f"Rate limit exceeded! (Limit: {self.rate} s, " \ - f"exceeded: {self.exceeded_count}, " \ - f"time delta: {round(self.delta, 3)} s)" + f"exceeded: {self.exceeded_count}, " \ + f"time delta: {round(self.delta, 3)} s)" diff --git a/aiogram/utils/mixins.py b/aiogram/utils/mixins.py index aa248cd9..776479bd 100644 --- a/aiogram/utils/mixins.py +++ b/aiogram/utils/mixins.py @@ -45,4 +45,3 @@ class ContextInstanceMixin: if not isinstance(value, cls): raise TypeError(f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'") cls.__context_instance.set(value) - diff --git a/examples/callback_data_factory.py b/examples/callback_data_factory.py index d87ae1a3..3dd7d35e 100644 --- a/examples/callback_data_factory.py +++ b/examples/callback_data_factory.py @@ -49,9 +49,9 @@ def get_keyboard() -> types.InlineKeyboardMarkup: def format_post(post_id: str, post: dict) -> (str, types.InlineKeyboardMarkup): text = f"{md.hbold(post['title'])}\n" \ - f"{md.quote_html(post['body'])}\n" \ - f"\n" \ - f"Votes: {post['votes']}" + f"{md.quote_html(post['body'])}\n" \ + f"\n" \ + f"Votes: {post['votes']}" markup = types.InlineKeyboardMarkup() markup.row( diff --git a/examples/payments.py b/examples/payments.py index d85e94ab..e8e37011 100644 --- a/examples/payments.py +++ b/examples/payments.py @@ -2,10 +2,9 @@ import asyncio from aiogram import Bot from aiogram import types -from aiogram.utils import executor from aiogram.dispatcher import Dispatcher from aiogram.types.message import ContentTypes - +from aiogram.utils import executor BOT_TOKEN = 'BOT TOKEN HERE' PAYMENTS_PROVIDER_TOKEN = '123456789:TEST:1234567890abcdef1234567890abcdef' diff --git a/examples/webhook_example.py b/examples/webhook_example.py index fb0046ef..86520988 100644 --- a/examples/webhook_example.py +++ b/examples/webhook_example.py @@ -76,7 +76,8 @@ async def unknown(message: types.Message): """ Handler for unknown messages. """ - return SendMessage(message.chat.id, f"I don\'t know what to do with content type `{message.content_type()}`. Sorry :c") + return SendMessage(message.chat.id, + f"I don\'t know what to do with content type `{message.content_type()}`. Sorry :c") async def cmd_id(message: types.Message): diff --git a/tests/__init__.py b/tests/__init__.py index cb0c6b9d..262c9395 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,5 @@ import aresponses + from aiogram import Bot TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 3dec03e7..6ebaf472 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -1,7 +1,7 @@ -from aiogram import Dispatcher, Bot - import pytest +from aiogram import Dispatcher, Bot + pytestmark = pytest.mark.asyncio From a977ae331838d1554205bdcbd878f73e9d5d9245 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Thu, 28 Mar 2019 16:22:03 +0500 Subject: [PATCH 15/85] Attempt to optimize Now introspection takes place during handler registration. I am not sure about this implementation. --- aiogram/dispatcher/handler.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 2caf80d8..31aae135 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -13,11 +13,15 @@ class CancelHandler(Exception): pass -def _check_spec(func: callable, kwargs: dict): +def _get_spec(func: callable): while hasattr(func, '__wrapped__'): # Try to resolve decorated callbacks func = func.__wrapped__ spec = inspect.getfullargspec(func) + return spec, func + + +def _check_spec(spec, kwargs: dict): if spec.varkw: return kwargs @@ -30,6 +34,7 @@ class Handler: self.once = once self.handlers = [] + self.specs = {} self.middleware_key = middleware_key def register(self, handler, filters=None, index=None): @@ -42,6 +47,9 @@ class Handler: :param filters: list of filters :param index: you can reorder handlers """ + spec, handler = _get_spec(handler) + self.specs[handler] = spec + if filters and not isinstance(filters, (list, tuple, set)): filters = [filters] record = (filters, handler) @@ -60,6 +68,7 @@ class Handler: for handler_with_filters in self.handlers: _, registered = handler_with_filters if handler is registered: + self.specs.pop(handler, None) self.handlers.remove(handler_with_filters) return True raise ValueError('This handler is not registered!') @@ -95,7 +104,7 @@ class Handler: try: if self.middleware_key: await self.dispatcher.middleware.trigger(f"process_{self.middleware_key}", args + (data,)) - partial_data = _check_spec(handler, data) + partial_data = _check_spec(self.specs[handler], data) response = await handler(*args, **partial_data) if response is not None: results.append(response) From 585bbfaee794485975b97c67908deeb3ed64b354 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sat, 30 Mar 2019 19:44:22 +0500 Subject: [PATCH 16/85] Update Text filter made the Text filter ready for lazy_gettext --- aiogram/dispatcher/filters/builtin.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 04a4132c..07e702de 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -4,6 +4,8 @@ from contextvars import ContextVar from dataclasses import dataclass, field from typing import Any, Dict, Iterable, Optional, Union +from babel.support import LazyProxy + from aiogram import types from aiogram.dispatcher.filters.filters import BoundFilter, Filter from aiogram.types import CallbackQuery, Message, InlineQuery @@ -116,10 +118,10 @@ class Text(Filter): """ def __init__(self, - equals: Optional[str] = None, - contains: Optional[str] = None, - startswith: Optional[str] = None, - endswith: Optional[str] = None, + equals: Optional[Union[str, LazyProxy]] = None, + contains: Optional[Union[str, LazyProxy]] = None, + startswith: Optional[Union[str, LazyProxy]] = None, + endswith: Optional[Union[str, LazyProxy]] = None, ignore_case=False): """ Check text for one of pattern. Only one mode can be used in one filter. @@ -173,13 +175,13 @@ class Text(Filter): text = text.lower() if self.equals: - return text == self.equals + return text == str(self.equals) elif self.contains: - return self.contains in text + return str(self.contains) in text elif self.startswith: - return text.startswith(self.startswith) + return text.startswith(str(self.startswith)) elif self.endswith: - return text.endswith(self.endswith) + return text.endswith(str(self.endswith)) return False From ccb8245adaeb3aeceed345a8c61b80d90d4594b2 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 30 Mar 2019 22:06:20 +0200 Subject: [PATCH 17/85] Fix logging filter --- aiogram/contrib/middlewares/logging.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index 5d14e61f..1a3566c6 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -161,7 +161,8 @@ class LoggingFilter(logging.Filter): '()': GELFRabbitHandler, 'url': 'amqp://localhost:5672/', 'routing_key': '#', - 'localname': 'testapp' + 'localname': 'testapp', + 'filters': ['telegram'] }, }, @@ -188,7 +189,7 @@ class LoggingFilter(logging.Filter): update = types.Update.get_current(True) if update: for key, value in self.make_prefix(self.prefix, self.process_update(update)): - setattr(self, key, value) + setattr(record, key, value) return True @@ -395,7 +396,7 @@ class LoggingFilter(logging.Filter): yield 'callback_query_data', callback_query.data if callback_query.message: - yield from self.process_message(callback_query.message) + yield from self.make_prefix('callback_query_message', self.process_message(callback_query.message)) if callback_query.inline_message_id: yield 'callback_query_inline_message_id', callback_query.inline_message_id if callback_query.chat_instance: From 4cc67ea03a46efe3f07828614a61a9ab45548475 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 30 Mar 2019 22:42:36 +0200 Subject: [PATCH 18/85] Pass update from webhook to updates handler instead of `Dispatcher.process_update` --- aiogram/dispatcher/webhook.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 10a4b3c1..c8abdef2 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -1,4 +1,6 @@ import asyncio +import itertools + import asyncio.tasks import datetime import functools @@ -165,7 +167,7 @@ class WebhookRequestHandler(web.View): timeout_handle = loop.call_later(RESPONSE_TIMEOUT, asyncio.tasks._release_waiter, waiter) cb = functools.partial(asyncio.tasks._release_waiter, waiter) - fut = asyncio.ensure_future(dispatcher.process_update(update), loop=loop) + fut = asyncio.ensure_future(dispatcher.updates_handler.notify(update), loop=loop) fut.add_done_callback(cb) try: @@ -219,7 +221,7 @@ class WebhookRequestHandler(web.View): """ if results is None: return None - for result in results: + for result in itertools.chain.from_iterable(results): if isinstance(result, BaseResponse): return result From 91ceeed59a019c805b001b08b55f39b8430f0986 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 30 Mar 2019 22:51:29 +0200 Subject: [PATCH 19/85] Allow to use separators longer than 1 symbols. Allow to use not string values. --- aiogram/utils/callback_data.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py index bb7d6862..090b1920 100644 --- a/aiogram/utils/callback_data.py +++ b/aiogram/utils/callback_data.py @@ -31,10 +31,8 @@ class CallbackData: raise TypeError(f"Prefix must be instance of str not {type(prefix).__name__}") elif not prefix: raise ValueError('Prefix can\'t be empty') - elif len(sep) != 1: - raise ValueError(f"Length of sep should be equals to 1") elif sep in prefix: - raise ValueError(f"Symbol '{sep}' can't be used in prefix") + raise ValueError(f"Separator '{sep}' can't be used in prefix") elif not parts: raise TypeError('Parts is not passed!') @@ -63,14 +61,12 @@ class CallbackData: else: raise ValueError(f"Value for '{part}' is not passed!") - if not isinstance(value, str): - raise TypeError(f"Value must be instance of str not {type(value).__name__}") - elif not value: + if not value: raise ValueError(f"Value for part {part} can't be empty!'") elif self.sep in value: raise ValueError(f"Symbol defined as separator can't be used in values of parts") - data.append(value) + data.append(str(value)) if args or kwargs: raise TypeError('Too many arguments is passed!') From f98439024404de3530ebdb397c087dc6fd538d4f Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 30 Mar 2019 23:59:21 +0200 Subject: [PATCH 20/85] Prevent errors in previous changes. --- aiogram/utils/callback_data.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py index 090b1920..ddf3f764 100644 --- a/aiogram/utils/callback_data.py +++ b/aiogram/utils/callback_data.py @@ -61,12 +61,15 @@ class CallbackData: else: raise ValueError(f"Value for '{part}' is not passed!") + if value is not None and not isinstance(value, str): + value = str(value) + if not value: raise ValueError(f"Value for part {part} can't be empty!'") elif self.sep in value: raise ValueError(f"Symbol defined as separator can't be used in values of parts") - data.append(str(value)) + data.append(value) if args or kwargs: raise TypeError('Too many arguments is passed!') From ad4c85eb77c3331fab5314dfb5742e6bea4064a1 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 31 Mar 2019 13:42:21 +0500 Subject: [PATCH 21/85] Update message.py - Add more answer_* methods - Move some methods for beauty --- aiogram/types/message.py | 554 ++++++++++++++++++++++++++++++++++----- 1 file changed, 491 insertions(+), 63 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index b211fd1d..3863de68 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -296,6 +296,434 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) + async def answer_photo(self, photo: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, reply=False) -> Message: + """ + Use this method to send photos. + + Source: https://core.telegram.org/bots/api#sendphoto + + :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-200 characters + :type caption: :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. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_photo(chat_id=self.chat.id, photo=photo, caption=caption, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_audio(self, audio: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + duration: typing.Union[base.Integer, None] = None, + performer: typing.Union[base.String, None] = None, + title: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=False) -> Message: + """ + Use this method to send audio files, if you want Telegram clients to display them in the music player. + Your audio must be in the .mp3 format. + + For sending voice messages, use the sendVoice method instead. + + Source: https://core.telegram.org/bots/api#sendaudio + + :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]` + :param duration: Duration of the audio in seconds + :type duration: :obj:`typing.Union[base.Integer, None]` + :param performer: Performer + :type performer: :obj:`typing.Union[base.String, None]` + :param title: Track name + :type title: :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. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_audio(chat_id=self.chat.id, + audio=audio, + caption=caption, + duration=duration, + performer=performer, + title=title, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_animation(self, + animation: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None, + caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=False) -> Message: + """ + Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). + + On success, the sent Message is returned. + Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. + + Source https://core.telegram.org/bots/api#sendanimation + + :param animation: Animation to send. Pass a file_id as String to send an animation that exists + on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation + from the Internet, or upload a new animation using multipart/form-data + :type animation: :obj:`typing.Union[base.InputFile, base.String]` + :param duration: Duration of sent animation in seconds + :type duration: :obj:`typing.Union[base.Integer, None]` + :param width: Animation width + :type width: :obj:`typing.Union[base.Integer, None]` + :param height: Animation height + :type height: :obj:`typing.Union[base.Integer, None]` + :param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. + A thumbnail‘s width and height should not exceed 90. + :type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]` + :param caption: Animation caption (may also be used when resending animation by file_id), 0-1024 characters + :type caption: :obj:`typing.Union[base.String, None]` + :param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic, + fixed-width text or inline URLs in the media caption + :type parse_mode: :obj:`typing.Union[base.String, None]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, + types.ReplyKeyboardRemove, types.ForceReply], None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned + :rtype: :obj:`types.Message` + """ + return await self.bot.send_animation(self.chat.id, animation=animation, + duration=duration, + width=width, + height=height, + thumb=thumb, + caption=caption, + parse_mode=parse_mode, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup + ) + + async def answer_document(self, document: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=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. + + Source: https://core.telegram.org/bots/api#senddocument + + :param document: File to send. + :type document: :obj:`typing.Union[base.InputFile, base.String]` + :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 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. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_document(chat_id=self.chat.id, + document=document, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_video(self, video: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + width: typing.Union[base.Integer, None] = None, + height: typing.Union[base.Integer, None] = None, + caption: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=False) -> Message: + """ + Use this method to send video files, Telegram clients support mp4 videos + (other formats may be sent as Document). + + Source: https://core.telegram.org/bots/api#sendvideo + + :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]` + :param width: Video width + :type width: :obj:`typing.Union[base.Integer, None]` + :param height: Video height + :type height: :obj:`typing.Union[base.Integer, 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]` + :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. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_video(chat_id=self.chat.id, + video=video, + duration=duration, + width=width, + height=height, + caption=caption, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_voice(self, voice: typing.Union[base.InputFile, base.String], + caption: typing.Union[base.String, None] = None, + duration: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=False) -> Message: + """ + Use this method to send audio files, if you want Telegram clients to display the file + as a playable voice message. + + For this to work, your audio must be in an .ogg file encoded with OPUS + (other formats may be sent as Audio or Document). + + Source: https://core.telegram.org/bots/api#sendvoice + + :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]` + :param duration: Duration of the voice message in seconds + :type duration: :obj:`typing.Union[base.Integer, 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. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_voice(chat_id=self.chat.id, + voice=voice, + caption=caption, + duration=duration, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_video_note(self, video_note: typing.Union[base.InputFile, base.String], + duration: typing.Union[base.Integer, None] = None, + length: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=False) -> Message: + """ + As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. + Use this method to send video messages. + + Source: https://core.telegram.org/bots/api#sendvideonote + + :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]` + :param length: Video width and height + :type length: :obj:`typing.Union[base.Integer, 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. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_video_note(chat_id=self.chat.id, + video_note=video_note, + duration=duration, + length=length, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_media_group(self, media: typing.Union[MediaGroup, typing.List], + disable_notification: typing.Union[base.Boolean, None] = None, + reply=False) -> typing.List[Message]: + """ + Use this method to send a group of photos or videos as an album. + + 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 reply: fill 'reply_to_message_id' + :return: On success, an array of the sent Messages is returned. + :rtype: typing.List[types.Message] + """ + return await self.bot.send_media_group(self.chat.id, + media=media, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None) + + async def answer_location(self, latitude: base.Float, + longitude: base.Float, live_period: typing.Union[base.Integer, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=False) -> Message: + """ + Use this method to send point on the map. + + Source: https://core.telegram.org/bots/api#sendlocation + + :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]` + :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. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_location(chat_id=self.chat.id, + latitude=latitude, + longitude=longitude, + live_period=live_period, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, + foursquare_id: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=False) -> Message: + """ + Use this method to send information about a venue. + + Source: https://core.telegram.org/bots/api#sendvenue + + :param latitude: Latitude of the venue + :type latitude: :obj:`base.Float` + :param longitude: Longitude of the venue + :type longitude: :obj:`base.Float` + :param title: Name of the venue + :type title: :obj:`base.String` + :param address: Address of the venue + :type address: :obj:`base.String` + :param foursquare_id: Foursquare identifier of the venue + :type foursquare_id: :obj:`typing.Union[base.String, None]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_venue(chat_id=self.chat.id, + latitude=latitude, + longitude=longitude, + title=title, + address=address, + foursquare_id=foursquare_id, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_contact(self, phone_number: base.String, + first_name: base.String, last_name: typing.Union[base.String, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, + reply=False) -> Message: + """ + Use this method to send phone contacts. + + Source: https://core.telegram.org/bots/api#sendcontact + + :param phone_number: Contact's phone number + :type phone_number: :obj:`base.String` + :param first_name: Contact's first name + :type first_name: :obj:`base.String` + :param last_name: Contact's last name + :type last_name: :obj:`typing.Union[base.String, None]` + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`typing.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_contact(chat_id=self.chat.id, + phone_number=phone_number, + first_name=first_name, last_name=last_name, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + + async def answer_sticker(self, sticker: typing.Union[base.InputFile, base.String], + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, reply=False) -> Message: + """ + Use this method to send .webp stickers. + + Source: https://core.telegram.org/bots/api#sendsticker + + :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]` + :param reply_markup: Additional interface options. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_sticker(chat_id=self.chat.id, sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + async def reply(self, text, parse_mode=None, disable_web_page_preview=None, disable_notification=None, reply_markup=None, reply=True) -> Message: """ @@ -712,45 +1140,6 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def edit_live_location(self, latitude: base.Float, longitude: base.Float, - reply_markup=None) -> typing.Union[Message, base.Boolean]: - """ - Use this method to edit live location messages sent by the bot or via the bot (for inline bots). - A location can be edited until its live_period expires or editing is explicitly disabled by a call - to stopMessageLiveLocation. - - Source: https://core.telegram.org/bots/api#editmessagelivelocation - - :param latitude: Latitude of new location - :type latitude: :obj:`base.Float` - :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]` - :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]` - """ - return await self.bot.edit_message_live_location(latitude=latitude, longitude=longitude, - chat_id=self.chat.id, message_id=self.message_id, - reply_markup=reply_markup) - - async def stop_live_location(self, reply_markup=None) -> typing.Union[Message, base.Boolean]: - """ - Use this method to stop updating a live location message sent by the bot or via the bot - (for inline bots) before live_period expires. - - 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]` - :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]` - """ - return await self.bot.stop_message_live_location(chat_id=self.chat.id, message_id=self.message_id, - reply_markup=reply_markup) - async def send_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, foursquare_id: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, @@ -901,6 +1290,30 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) + async def reply_sticker(self, sticker: typing.Union[base.InputFile, base.String], + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup=None, reply=True) -> Message: + """ + Use this method to send .webp stickers. + + Source: https://core.telegram.org/bots/api#sendsticker + + :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]` + :param reply_markup: Additional interface options. + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` + :param reply: fill 'reply_to_message_id' + :return: On success, the sent Message is returned. + :rtype: :obj:`types.Message` + """ + return await self.bot.send_sticker(chat_id=self.chat.id, sticker=sticker, + disable_notification=disable_notification, + reply_to_message_id=self.message_id if reply else None, + reply_markup=reply_markup) + async def forward(self, chat_id, disable_notification=None) -> Message: """ Forward this message @@ -1000,6 +1413,45 @@ class Message(base.TelegramObject): return await self.bot.edit_message_reply_markup(chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) + async def edit_live_location(self, latitude: base.Float, longitude: base.Float, + reply_markup=None) -> typing.Union[Message, base.Boolean]: + """ + Use this method to edit live location messages sent by the bot or via the bot (for inline bots). + A location can be edited until its live_period expires or editing is explicitly disabled by a call + to stopMessageLiveLocation. + + Source: https://core.telegram.org/bots/api#editmessagelivelocation + + :param latitude: Latitude of new location + :type latitude: :obj:`base.Float` + :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]` + :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]` + """ + return await self.bot.edit_message_live_location(latitude=latitude, longitude=longitude, + chat_id=self.chat.id, message_id=self.message_id, + reply_markup=reply_markup) + + async def stop_live_location(self, reply_markup=None) -> typing.Union[Message, base.Boolean]: + """ + Use this method to stop updating a live location message sent by the bot or via the bot + (for inline bots) before live_period expires. + + 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]` + :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]` + """ + return await self.bot.stop_message_live_location(chat_id=self.chat.id, message_id=self.message_id, + reply_markup=reply_markup) + async def delete(self): """ Delete this message @@ -1008,30 +1460,6 @@ class Message(base.TelegramObject): """ return await self.bot.delete_message(self.chat.id, self.message_id) - async def reply_sticker(self, sticker: typing.Union[base.InputFile, base.String], - disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, reply=True) -> Message: - """ - Use this method to send .webp stickers. - - Source: https://core.telegram.org/bots/api#sendsticker - - :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]` - :param reply_markup: Additional interface options. - :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, - types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` - :param reply: fill 'reply_to_message_id' - :return: On success, the sent Message is returned. - :rtype: :obj:`types.Message` - """ - return await self.bot.send_sticker(chat_id=self.chat.id, sticker=sticker, - disable_notification=disable_notification, - reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup) - async def pin(self, disable_notification: bool = False): """ Pin message From d6ab1a7afc15b5cbcbd8163cf5acb274fd0893fc Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 31 Mar 2019 13:49:30 +0500 Subject: [PATCH 22/85] Update handler.py Now the func, specs and filters are put in the dataclass --- aiogram/dispatcher/handler.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 31aae135..073747c9 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -1,5 +1,6 @@ import inspect from contextvars import ContextVar +from dataclasses import dataclass ctx_data = ContextVar('ctx_handler_data') current_handler = ContextVar('current_handler') @@ -21,7 +22,7 @@ def _get_spec(func: callable): return spec, func -def _check_spec(spec, kwargs: dict): +def _check_spec(spec: inspect.FullArgSpec, kwargs: dict): if spec.varkw: return kwargs @@ -34,7 +35,6 @@ class Handler: self.once = once self.handlers = [] - self.specs = {} self.middleware_key = middleware_key def register(self, handler, filters=None, index=None): @@ -48,11 +48,10 @@ class Handler: :param index: you can reorder handlers """ spec, handler = _get_spec(handler) - self.specs[handler] = spec if filters and not isinstance(filters, (list, tuple, set)): filters = [filters] - record = (filters, handler) + record = Handler.HandlerObj(handler=handler, spec=spec, filters=filters) if index is None: self.handlers.append(record) else: @@ -65,11 +64,10 @@ class Handler: :param handler: callback :return: """ - for handler_with_filters in self.handlers: - _, registered = handler_with_filters + for handler_obj in self.handlers: + registered = handler_obj.handler if handler is registered: - self.specs.pop(handler, None) - self.handlers.remove(handler_with_filters) + self.handlers.remove(handler_obj) return True raise ValueError('This handler is not registered!') @@ -94,18 +92,18 @@ class Handler: return results try: - for filters, handler in self.handlers: + for handler_obj in self.handlers: try: - data.update(await check_filters(self.dispatcher, filters, args)) + data.update(await check_filters(self.dispatcher, handler_obj.filters, args)) except FilterNotPassed: continue else: - ctx_token = current_handler.set(handler) + ctx_token = current_handler.set(handler_obj.handler) try: if self.middleware_key: await self.dispatcher.middleware.trigger(f"process_{self.middleware_key}", args + (data,)) - partial_data = _check_spec(self.specs[handler], data) - response = await handler(*args, **partial_data) + partial_data = _check_spec(handler_obj.spec, data) + response = await handler_obj.handler(*args, **partial_data) if response is not None: results.append(response) if self.once: @@ -122,3 +120,9 @@ class Handler: args + (results, data,)) return results + + @dataclass + class HandlerObj: + handler: callable + spec: inspect.FullArgSpec + filters: list = None From 8b3f0b887dde97a75d075abcea19a3ea74cb3bc9 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 31 Mar 2019 13:54:15 +0500 Subject: [PATCH 23/85] Update handler.py --- aiogram/dispatcher/handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 073747c9..ea60d0d5 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -1,6 +1,7 @@ import inspect from contextvars import ContextVar from dataclasses import dataclass +from typing import Union ctx_data = ContextVar('ctx_handler_data') current_handler = ContextVar('current_handler') @@ -125,4 +126,4 @@ class Handler: class HandlerObj: handler: callable spec: inspect.FullArgSpec - filters: list = None + filters: Union[list, tuple, set] = None From 8340fd494c97fb74e946d9eab2a143f20c650607 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 31 Mar 2019 13:55:17 +0500 Subject: [PATCH 24/85] Update handler.py --- aiogram/dispatcher/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index ea60d0d5..24771e92 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -126,4 +126,4 @@ class Handler: class HandlerObj: handler: callable spec: inspect.FullArgSpec - filters: Union[list, tuple, set] = None + filters: Union[list, tuple, set, None] = None From 35f0451d81f02044bcbc213fc45fa2b4df81c3b9 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sat, 6 Apr 2019 22:43:46 +0500 Subject: [PATCH 25/85] Fix dev_requirements.txt --- dev_requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index ee1fc5e5..4aa84dd9 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -10,7 +10,6 @@ aresponses>=1.0.0 uvloop>=0.9.1 aioredis>=1.1.0 wheel>=0.31.0 -rethinkdb>=2.3.0 sphinx>=1.7.3 sphinx-rtd-theme>=0.3.0 sphinxcontrib-programoutput>=0.11 From 9ee1a0cbed5e0d6d8bf979eb7554952ee7fa83ee Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sat, 6 Apr 2019 22:47:33 +0500 Subject: [PATCH 26/85] Optimize filter checking --- aiogram/dispatcher/filters/__init__.py | 8 +++-- aiogram/dispatcher/filters/filters.py | 43 +++++++++++++++++--------- aiogram/dispatcher/handler.py | 16 ++++++++-- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 489e7c76..49a0e637 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,7 +1,8 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ ExceptionsFilter, FuncFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text from .factory import FiltersFactory -from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, check_filter, check_filters +from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, FilterObj, execute_filter, \ + check_filters, get_filter_spec, get_filters_spec __all__ = [ 'AbstractFilter', @@ -23,6 +24,9 @@ __all__ = [ 'Regexp', 'StateFilter', 'Text', - 'check_filter', + 'FilterObj', + 'get_filters_spec', + 'get_filters_spec', + 'execute_filter', 'check_filters' ] diff --git a/aiogram/dispatcher/filters/filters.py b/aiogram/dispatcher/filters/filters.py index 7c0203f2..a6d83c62 100644 --- a/aiogram/dispatcher/filters/filters.py +++ b/aiogram/dispatcher/filters/filters.py @@ -2,7 +2,7 @@ import abc import inspect import typing -from ..handler import Handler +from ..handler import Handler, FilterObj class FilterNotPassed(Exception): @@ -20,15 +20,7 @@ def wrap_async(func): return async_wrapper -async def check_filter(dispatcher, filter_, args): - """ - Helper for executing filter - - :param dispatcher: - :param filter_: - :param args: - :return: - """ +def get_filter_spec(dispatcher, filter_: callable): kwargs = {} if not callable(filter_): raise TypeError('Filter must be callable and/or awaitable!') @@ -39,16 +31,37 @@ async def check_filter(dispatcher, filter_, args): if inspect.isawaitable(filter_) \ or inspect.iscoroutinefunction(filter_) \ or isinstance(filter_, AbstractFilter): - return await filter_(*args, **kwargs) + return FilterObj(filter=filter_, kwargs=kwargs, is_async=True) else: - return filter_(*args, **kwargs) + return FilterObj(filter=filter_, kwargs=kwargs, is_async=False) -async def check_filters(dispatcher, filters, args): +def get_filters_spec(dispatcher, filters: typing.Iterable[callable]): + data = [] + if filters is not None: + for i in filters: + data.append(get_filter_spec(dispatcher, i)) + return data + + +async def execute_filter(filter_: FilterObj, args): + """ + Helper for executing filter + + :param filter_: + :param args: + :return: + """ + if filter_.is_async: + return await filter_.filter(*args, **filter_.kwargs) + else: + return filter_.filter(*args, **filter_.kwargs) + + +async def check_filters(filters: typing.Iterable[FilterObj], args): """ Check list of filters - :param dispatcher: :param filters: :param args: :return: @@ -56,7 +69,7 @@ async def check_filters(dispatcher, filters, args): data = {} if filters is not None: for filter_ in filters: - f = await check_filter(dispatcher, filter_, args) + f = await execute_filter(filter_, args) if not f: raise FilterNotPassed() elif isinstance(f, dict): diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 24771e92..17b715d1 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -1,12 +1,19 @@ import inspect from contextvars import ContextVar from dataclasses import dataclass -from typing import Union +from typing import Optional, Iterable ctx_data = ContextVar('ctx_handler_data') current_handler = ContextVar('current_handler') +@dataclass +class FilterObj: + filter: callable + kwargs: dict + is_async: bool + + class SkipHandler(Exception): pass @@ -39,6 +46,7 @@ class Handler: self.middleware_key = middleware_key def register(self, handler, filters=None, index=None): + from .filters import get_filters_spec """ Register callback @@ -52,6 +60,8 @@ class Handler: if filters and not isinstance(filters, (list, tuple, set)): filters = [filters] + filters = get_filters_spec(self.dispatcher, filters) + record = Handler.HandlerObj(handler=handler, spec=spec, filters=filters) if index is None: self.handlers.append(record) @@ -95,7 +105,7 @@ class Handler: try: for handler_obj in self.handlers: try: - data.update(await check_filters(self.dispatcher, handler_obj.filters, args)) + data.update(await check_filters(handler_obj.filters, args)) except FilterNotPassed: continue else: @@ -126,4 +136,4 @@ class Handler: class HandlerObj: handler: callable spec: inspect.FullArgSpec - filters: Union[list, tuple, set, None] = None + filters: Optional[Iterable[FilterObj]] = None From a927be5635b45f48dcdae17579c9ac3f856fb74b Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sat, 6 Apr 2019 22:49:53 +0500 Subject: [PATCH 27/85] Update __init__.py --- aiogram/dispatcher/filters/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index 49a0e637..d81e2ccc 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, FuncFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text from .factory import FiltersFactory -from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, FilterObj, execute_filter, \ +from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec __all__ = [ @@ -24,7 +24,6 @@ __all__ = [ 'Regexp', 'StateFilter', 'Text', - 'FilterObj', 'get_filters_spec', 'get_filters_spec', 'execute_filter', From db18d54b2132c53ab25ec6141b3b69c18bdce522 Mon Sep 17 00:00:00 2001 From: Kostiantyn Kriuchkov <36363097+Latand@users.noreply.github.com> Date: Sat, 6 Apr 2019 20:26:58 +0100 Subject: [PATCH 28/85] Update callback_data.py Can't pass 0 to value, raises error 'ValueError: Value for {part} is not passed!' --- aiogram/utils/callback_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/callback_data.py b/aiogram/utils/callback_data.py index ddf3f764..916d08c4 100644 --- a/aiogram/utils/callback_data.py +++ b/aiogram/utils/callback_data.py @@ -55,7 +55,7 @@ class CallbackData: for part in self._part_names: value = kwargs.pop(part, None) - if not value: + if value is None: if args: value = args.pop(0) else: From 25bdb9cf4d34573f92e5c4b177988bd1c3d9efe5 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 7 Apr 2019 14:13:54 +0300 Subject: [PATCH 29/85] Fix #117: TypeError with LazyProxy object in keyboards --- aiogram/types/base.py | 4 ++++ aiogram/utils/payload.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/aiogram/types/base.py b/aiogram/types/base.py index 4514e956..97f67b16 100644 --- a/aiogram/types/base.py +++ b/aiogram/types/base.py @@ -4,6 +4,8 @@ import io import typing from typing import TypeVar +from babel.support import LazyProxy + from .fields import BaseField from ..utils import json from ..utils.mixins import ContextInstanceMixin @@ -163,6 +165,8 @@ class TelegramObject(ContextInstanceMixin, metaclass=MetaTelegramObject): value = self.props[name].export(self) if isinstance(value, TelegramObject): value = value.to_python() + if isinstance(value, LazyProxy): + value = str(value) result[self.props_aliases.get(name, name)] = value return result diff --git a/aiogram/utils/payload.py b/aiogram/utils/payload.py index bbed1967..45643553 100644 --- a/aiogram/utils/payload.py +++ b/aiogram/utils/payload.py @@ -1,6 +1,8 @@ import datetime import secrets +from babel.support import LazyProxy + from aiogram import types from . import json @@ -57,6 +59,8 @@ def prepare_arg(value): return int((now + value).timestamp()) elif isinstance(value, datetime.datetime): return round(value.timestamp()) + elif isinstance(value, LazyProxy): + return str(value) return value From 9ac2dc4f4f07421d84dd02210ef19e20a4e47011 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 7 Apr 2019 21:34:02 +0500 Subject: [PATCH 30/85] Update __init__.py --- aiogram/dispatcher/filters/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index d81e2ccc..b11b8374 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -24,7 +24,7 @@ __all__ = [ 'Regexp', 'StateFilter', 'Text', - 'get_filters_spec', + 'get_filter_spec', 'get_filters_spec', 'execute_filter', 'check_filters' From 1eefb237a2648a644aacd36f0ec4e6a667a562be Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Mon, 8 Apr 2019 09:43:03 +0500 Subject: [PATCH 31/85] Add delete_reply_markup to Message --- aiogram/types/message.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 3863de68..f2741a9c 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1413,6 +1413,16 @@ class Message(base.TelegramObject): return await self.bot.edit_message_reply_markup(chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) + async def delete_reply_markup(self): + """ + Use this method to delete reply markup of messages sent by the bot or via the bot (for inline bots). + + :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_reply_markup(chat_id=self.chat.id, message_id=self.message_id) + async def edit_live_location(self, latitude: base.Float, longitude: base.Float, reply_markup=None) -> typing.Union[Message, base.Boolean]: """ From e2e2d9c9fe805286d75a1eb05ae514ac95b94a23 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Thu, 11 Apr 2019 00:32:46 +0300 Subject: [PATCH 32/85] Implemented polls [From test API] --- aiogram/__init__.py | 2 +- aiogram/bot/api.py | 4 ++++ aiogram/bot/bot.py | 17 +++++++++++++++++ aiogram/dispatcher/dispatcher.py | 24 +++++++++++++++++++++--- aiogram/dispatcher/filters/builtin.py | 14 +++++++++++--- aiogram/types/__init__.py | 3 +++ aiogram/types/message.py | 6 ++++++ aiogram/types/poll.py | 16 ++++++++++++++++ aiogram/types/update.py | 2 ++ aiogram/utils/exceptions.py | 26 ++++++++++++++++++++++++++ 10 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 aiogram/types/poll.py diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 321a4e34..f7e61e76 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.0.2.dev1' +__version__ = '2.1.dev1' __api_version__ = '4.1' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 25935dac..16dbb9fa 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -229,6 +229,10 @@ class Methods(Helper): SET_GAME_SCORE = Item() # setGameScore GET_GAME_HIGH_SCORES = Item() # getGameHighScores + # Polls + SEND_POLL = Item() + STOP_POLL = Item() + @staticmethod def api_url(token, method): """ diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 1f76823d..42caf989 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -2056,3 +2056,20 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_GAME_HIGH_SCORES, payload) return [types.GameHighScore(**gamehighscore) for gamehighscore in result] + + async def send_poll(self, chat_id: typing.Union[base.Integer, base.String], + question: base.String, + options: typing.List[base.String], + reply_to_message_id: typing.Union[base.Integer, None]): + options = prepare_arg(options) + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.SEND_POLL, payload) + return types.Message(**result) + + async def stop_poll(self, chat_id: typing.Union[base.String, base.Integer], + message_id: base.Integer) -> types.Poll: + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.STOP_POLL, payload) + return types.Poll(**result) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index d1fc72ba..d1096718 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -64,6 +64,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self.callback_query_handlers = Handler(self, middleware_key='callback_query') self.shipping_query_handlers = Handler(self, middleware_key='shipping_query') self.pre_checkout_query_handlers = Handler(self, middleware_key='pre_checkout_query') + self.poll_handlers = Handler(self, middleware_key='poll') self.errors_handlers = Handler(self, once=False, middleware_key='error') self.middleware = MiddlewareManager(self) @@ -80,7 +81,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory = self.filters_factory filters_factory.bind(StateFilter, exclude_event_handlers=[ - self.errors_handlers + self.errors_handlers, + self.poll_handlers ]) filters_factory.bind(ContentTypeFilter, event_handlers=[ self.message_handlers, self.edited_message_handlers, @@ -92,7 +94,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(Text, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers + self.callback_query_handlers, self.poll_handlers ]) filters_factory.bind(HashTag, event_handlers=[ self.message_handlers, self.edited_message_handlers, @@ -101,7 +103,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(Regexp, event_handlers=[ self.message_handlers, self.edited_message_handlers, self.channel_post_handlers, self.edited_channel_post_handlers, - self.callback_query_handlers + self.callback_query_handlers, self.poll_handlers ]) filters_factory.bind(RegexpCommandsFilter, event_handlers=[ self.message_handlers, self.edited_message_handlers @@ -185,6 +187,8 @@ class Dispatcher(DataMixin, ContextInstanceMixin): if update.pre_checkout_query: types.User.set_current(update.pre_checkout_query.from_user) return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query) + if update.poll: + return await self.poll_handlers.notify(update.poll) except Exception as e: err = await self.errors_handlers.notify(update, e) if err: @@ -796,6 +800,20 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return decorator + def register_poll_handler(self, callback, *custom_filters, run_task=None, **kwargs): + filters_set = self.filters_factory.resolve(self.poll_handlers, + *custom_filters, + **kwargs) + self.poll_handlers.register(self._wrap_async_task(callback, run_task), filters_set) + + def poll_handler(self, *custom_filters, run_task=None, **kwargs): + def decorator(callback): + self.register_poll_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 07e702de..de61c560 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -8,7 +8,7 @@ from babel.support import LazyProxy from aiogram import types from aiogram.dispatcher.filters.filters import BoundFilter, Filter -from aiogram.types import CallbackQuery, Message, InlineQuery +from aiogram.types import CallbackQuery, Message, InlineQuery, Poll from aiogram.utils.deprecated import warn_deprecated @@ -164,10 +164,14 @@ class Text(Filter): async def check(self, obj: Union[Message, CallbackQuery, InlineQuery]): if isinstance(obj, Message): text = obj.text or obj.caption or '' + if not text and obj.poll: + text = obj.poll.question elif isinstance(obj, CallbackQuery): text = obj.data elif isinstance(obj, InlineQuery): text = obj.query + elif isinstance(obj, Poll): + text = obj.question else: return False @@ -269,12 +273,16 @@ class Regexp(Filter): async def check(self, obj: Union[Message, CallbackQuery]): if isinstance(obj, Message): - match = self.regexp.search(obj.text or obj.caption or '') + content = obj.text or obj.caption or '' + if not content and obj.poll: + content = obj.poll.question elif isinstance(obj, CallbackQuery) and obj.data: - match = self.regexp.search(obj.data) + content = obj.data else: return False + match = self.regexp.search(content) + if match: return {'regexp': match} return False diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 5416afd9..1dcd2c1f 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -43,6 +43,7 @@ from .passport_element_error import PassportElementError, PassportElementErrorDa PassportElementErrorSelfie from .passport_file import PassportFile from .photo_size import PhotoSize +from .poll import PollOptions, Poll from .pre_checkout_query import PreCheckoutQuery from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove from .response_parameters import ResponseParameters @@ -142,6 +143,8 @@ __all__ = ( 'PassportElementErrorSelfie', 'PassportFile', 'PhotoSize', + 'Poll', + 'PollOptions', 'PreCheckoutQuery', 'ReplyKeyboardMarkup', 'ReplyKeyboardRemove', diff --git a/aiogram/types/message.py b/aiogram/types/message.py index f2741a9c..5dde2c73 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -19,6 +19,7 @@ from .location import Location from .message_entity import MessageEntity from .passport_data import PassportData from .photo_size import PhotoSize +from .poll import Poll from .sticker import Sticker from .successful_payment import SuccessfulPayment from .user import User @@ -81,6 +82,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) + poll: Poll = fields.Field(base=Poll) @property @functools.lru_cache() @@ -137,6 +139,8 @@ class Message(base.TelegramObject): return ContentType.GROUP_CHAT_CREATED elif self.passport_data: return ContentType.PASSPORT_DATA + elif self.poll: + return ContentType.POLL else: return ContentType.UNKNOWN @@ -1539,6 +1543,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 + POLL = helper.Item() UNKNOWN = helper.Item() # unknown ANY = helper.Item() # any @@ -1600,6 +1605,7 @@ 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 diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py new file mode 100644 index 00000000..e3f8caca --- /dev/null +++ b/aiogram/types/poll.py @@ -0,0 +1,16 @@ +import typing + +from . import base +from . import fields + + +class PollOptions(base.TelegramObject): + text: base.String = fields.Field() + voter_count: base.Integer = fields.Field() + + +class Poll(base.TelegramObject): + id: base.String = fields.Field() + question: base.String = fields.Field() + options: typing.List[PollOptions] = fields.ListField(base=PollOptions) + is_closed: base.Boolean = fields.Field() diff --git a/aiogram/types/update.py b/aiogram/types/update.py index 2753ae5f..9f8ce0fb 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -6,6 +6,7 @@ from .callback_query import CallbackQuery from .chosen_inline_result import ChosenInlineResult from .inline_query import InlineQuery from .message import Message +from .poll import Poll from .pre_checkout_query import PreCheckoutQuery from .shipping_query import ShippingQuery from ..utils import helper @@ -28,6 +29,7 @@ class Update(base.TelegramObject): callback_query: CallbackQuery = fields.Field(base=CallbackQuery) shipping_query: ShippingQuery = fields.Field(base=ShippingQuery) pre_checkout_query: PreCheckoutQuery = fields.Field(base=PreCheckoutQuery) + poll: Poll = fields.Field(base=Poll) def __hash__(self): return self.update_id diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 24afe356..b08c681d 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -7,12 +7,16 @@ TelegramAPIError MessageNotModified MessageToForwardNotFound MessageToDeleteNotFound + MessageWithPollNotFound + MessageIsNotAPoll MessageIdentifierNotSpecified MessageTextIsEmpty MessageCantBeEdited MessageCantBeDeleted MessageToEditNotFound ToMuchMessages + PollCantBeStopped + PollHasAlreadyClosed ObjectExpectedAsReplyMarkup InlineKeyboardExpected ChatNotFound @@ -164,6 +168,20 @@ class MessageToDeleteNotFound(MessageError): match = 'message to delete not found' +class MessageWithPollNotFound(MessageError): + """ + Will be raised when you try to stop poll with message without poll + """ + match = 'message with poll to stop not found' + + +class MessageIsNotAPoll(MessageError): + """ + Will be raised when you try to stop poll with message without poll + """ + match = 'message is not a poll' + + class MessageIdentifierNotSpecified(MessageError): match = 'message identifier is not specified' @@ -203,6 +221,14 @@ class InlineKeyboardExpected(BadRequest): match = 'inline keyboard expected' +class PollCantBeStopped(BadRequest): + match = "poll can't be stopped" + + +class PollHasAlreadyBeenClosed(BadRequest): + match = 'poll has already been closed' + + class ChatNotFound(BadRequest): match = 'chat not found' From 411202de45d7bc03c7e73604f097b22b76e9371b Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 13 Apr 2019 04:41:35 +0300 Subject: [PATCH 33/85] More poll errors --- aiogram/utils/exceptions.py | 85 ++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index b08c681d..50f8de4c 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -7,16 +7,25 @@ TelegramAPIError MessageNotModified MessageToForwardNotFound MessageToDeleteNotFound - MessageWithPollNotFound - MessageIsNotAPoll MessageIdentifierNotSpecified MessageTextIsEmpty MessageCantBeEdited MessageCantBeDeleted MessageToEditNotFound ToMuchMessages - PollCantBeStopped - PollHasAlreadyClosed + PollError + PollCantBeStopped + PollHasAlreadyClosed + PollsCantBeSentToPrivateChats + PollSizeError + PollMustHaveMoreOptions + PollCantHaveMoreOptions + PollsOptionsLengthTooLong + PollOptionsMustBeNonEmpty + PollQuestionMustBeNonEmpty + + MessageWithPollNotFound (with MessageError) + MessageIsNotAPoll (with MessageError) ObjectExpectedAsReplyMarkup InlineKeyboardExpected ChatNotFound @@ -168,20 +177,6 @@ class MessageToDeleteNotFound(MessageError): match = 'message to delete not found' -class MessageWithPollNotFound(MessageError): - """ - Will be raised when you try to stop poll with message without poll - """ - match = 'message with poll to stop not found' - - -class MessageIsNotAPoll(MessageError): - """ - Will be raised when you try to stop poll with message without poll - """ - match = 'message is not a poll' - - class MessageIdentifierNotSpecified(MessageError): match = 'message identifier is not specified' @@ -221,14 +216,64 @@ class InlineKeyboardExpected(BadRequest): match = 'inline keyboard expected' -class PollCantBeStopped(BadRequest): +class PollError(BadRequest): + __group = True + + +class PollCantBeStopped(PollError): match = "poll can't be stopped" -class PollHasAlreadyBeenClosed(BadRequest): +class PollHasAlreadyBeenClosed(PollError): match = 'poll has already been closed' +class PollsCantBeSentToPrivateChats(PollError): + match = "polls can't be sent to private chats" + + +class PollSizeError(PollError): + __group = True + + +class PollMustHaveMoreOptions(PollSizeError): + match = "poll must have at least 2 option" + + +class PollCantHaveMoreOptions(PollSizeError): + match = "poll can't have more than 10 options" + + +class PollOptionsMustBeNonEmpty(PollSizeError): + match = "poll options must be non-empty" + + +class PollQuestionMustBeNonEmpty(PollSizeError): + match = "poll question must be non-empty" + + +class PollOptionsLengthTooLong(PollSizeError): + match = "poll options length must not exceed 100" + + +class PollQuestionLengthTooLong(PollSizeError): + match = "poll question length must not exceed 255" + + +class MessageWithPollNotFound(PollError, MessageError): + """ + Will be raised when you try to stop poll with message without poll + """ + match = 'message with poll to stop not found' + + +class MessageIsNotAPoll(PollError, MessageError): + """ + Will be raised when you try to stop poll with message without poll + """ + match = 'message is not a poll' + + class ChatNotFound(BadRequest): match = 'chat not found' From 96a4db637653350309e65cca223cbfcd9e855cd2 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 13 Apr 2019 04:44:49 +0300 Subject: [PATCH 34/85] Fix error prefix cleaner --- aiogram/utils/exceptions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 50f8de4c..b475a6d0 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -23,7 +23,6 @@ TelegramAPIError PollsOptionsLengthTooLong PollOptionsMustBeNonEmpty PollQuestionMustBeNonEmpty - MessageWithPollNotFound (with MessageError) MessageIsNotAPoll (with MessageError) ObjectExpectedAsReplyMarkup @@ -84,7 +83,7 @@ import time # TODO: Use exceptions detector from `aiograph`. -_PREFIXES = ['Error: ', '[Error]: ', 'Bad Request: ', 'Conflict: ', 'Not Found: '] +_PREFIXES = ['error: ', '[error]: ', 'bad request: ', 'conflict: ', 'not found: '] def _clean_message(text): From 43022a7810507715417040f2e85939bc67b28b89 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 13 Apr 2019 04:57:24 +0300 Subject: [PATCH 35/85] More errors. --- aiogram/utils/exceptions.py | 39 ++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index b475a6d0..b817e4cc 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -28,7 +28,6 @@ TelegramAPIError ObjectExpectedAsReplyMarkup InlineKeyboardExpected ChatNotFound - ChatIdIsEmpty ChatDescriptionIsNotModified InvalidQueryID InvalidPeerID @@ -43,14 +42,15 @@ TelegramAPIError WebhookRequireHTTPS BadWebhookPort BadWebhookAddrInfo - CantParseUrl + BadWebhookNoAddressAssociatedWithHostname NotFound MethodNotKnown PhotoAsInputFileRequired InvalidStickersSet NoStickerInRequest ChatAdminRequired - NotEnoughRightsToPinMessage + NeedAdministratorRightsInTheChannel + MethodNotAvailableInPrivateChats CantDemoteChatCreator CantRestrictSelf NotEnoughRightsToRestrict @@ -61,7 +61,9 @@ TelegramAPIError PaymentProviderInvalid CurrencyTotalAmountInvalid CantParseUrl + UnsupportedUrlProtocol CantParseEntities + ResultIdDuplicate ConflictError TerminatedByOtherGetUpdates CantGetUpdates @@ -76,6 +78,10 @@ TelegramAPIError MigrateToChat RestartingTelegram + +TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0 +TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout + AIOGramWarning TimeoutWarning """ @@ -281,6 +287,11 @@ class ChatIdIsEmpty(BadRequest): match = 'chat_id is empty' +class InvalidUserId(BadRequest): + match = 'user_id_invalid' + text = 'Invalid user id' + + class ChatDescriptionIsNotModified(BadRequest): match = 'chat description is not modified' @@ -347,10 +358,19 @@ class ChatAdminRequired(BadRequest): text = 'Admin permissions is required!' +class NeedAdministratorRightsInTheChannel(BadRequest): + match = 'need administrator rights in the channel chat' + text = 'Admin permissions is required!' + + class NotEnoughRightsToPinMessage(BadRequest): match = 'not enough rights to pin a message' +class MethodNotAvailableInPrivateChats(BadRequest): + match = 'method is available only for supergroups and channel' + + class CantDemoteChatCreator(BadRequest): match = 'can\'t demote chat creator' @@ -410,14 +430,27 @@ class BadWebhookAddrInfo(BadWebhook): text = 'bad webhook: ' + match +class BadWebhookNoAddressAssociatedWithHostname(BadWebhook): + match = 'failed to resolve host: no address associated with hostname' + + class CantParseUrl(BadRequest): match = 'can\'t parse URL' +class UnsupportedUrlProtocol(BadRequest): + match = 'unsupported URL protocol' + + class CantParseEntities(BadRequest): match = 'can\'t parse entities' +class ResultIdDuplicate(BadRequest): + match = 'result_id_duplicate' + text = 'Result ID duplicate' + + class NotFound(TelegramAPIError, _MatchErrorMixin): __group = True From 599e87ec131cd3cc9d0131d3d264e158f8e74f80 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Mon, 15 Apr 2019 23:38:17 +0300 Subject: [PATCH 36/85] Bump polls to latest API version (Naming and location) --- aiogram/__init__.py | 4 +-- aiogram/bot/base.py | 17 +++++---- aiogram/bot/bot.py | 75 ++++++++++++++++++++++++++++++--------- aiogram/types/__init__.py | 4 +-- aiogram/types/poll.py | 4 +-- 5 files changed, 75 insertions(+), 29 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index f7e61e76..77ceb357 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.1.dev1' -__api_version__ = '4.1' +__version__ = '2.1.dev2' +__api_version__ = '4.2' diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index cab491d1..19031fd0 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -1,6 +1,7 @@ import asyncio import io import ssl +import typing from typing import Dict, List, Optional, Union import aiohttp @@ -17,12 +18,16 @@ class BaseBot: Base class for bot. It's raw bot. """ - def __init__(self, token: base.String, - loop: Optional[Union[asyncio.BaseEventLoop, asyncio.AbstractEventLoop]] = None, - connections_limit: Optional[base.Integer] = None, - proxy: Optional[base.String] = None, proxy_auth: Optional[aiohttp.BasicAuth] = None, - validate_token: Optional[base.Boolean] = True, - parse_mode=None): + def __init__( + self, + token: base.String, + loop: Optional[Union[asyncio.BaseEventLoop, asyncio.AbstractEventLoop]] = None, + connections_limit: Optional[base.Integer] = None, + proxy: Optional[base.String] = None, + proxy_auth: Optional[aiohttp.BasicAuth] = None, + validate_token: Optional[base.Boolean] = True, + parse_mode: typing.Optional[base.String] = None, + ): """ Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 42caf989..0e13d62b 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -847,6 +847,43 @@ 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], + question: base.String, + options: typing.List[base.String], + disable_notification: typing.Optional[base.Boolean], + reply_to_message_id: typing.Union[base.Integer, None], + reply_markup: typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, + types.ReplyKeyboardRemove, + 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. + + :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. + :type chat_id: :obj:`typing.Union[base.Integer, base.String]` + :param question: Poll question, 1-255 characters + :type question: :obj:`base.String` + :param options: List of answer options, 2-10 strings 1-100 characters each + :param options: :obj:`typing.List[base.String]` + :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 + :type reply_to_message_id: :obj:`typing.Optional[Integer]` + :param reply_markup: Additional interface options + :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` + """ + options = prepare_arg(options) + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.SEND_POLL, payload) + return types.Message(**result) + async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String], action: base.String) -> base.Boolean: """ @@ -1524,6 +1561,27 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): return result return types.Message(**result) + 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: + """ + Use this method to stop a poll which was sent by the bot. + On success, the stopped Poll with the final results is returned. + + :param chat_id: Unique identifier for the target chat or username of the target channel + :type chat_id: :obj:`typing.Union[base.String, base.Integer]` + :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]` + :return: On success, the stopped Poll with the final results is returned. + :rtype: :obj:`types.Poll` + """ + payload = generate_payload(**locals()) + + result = await self.request(api.Methods.STOP_POLL, payload) + return types.Poll(**result) + async def delete_message(self, chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer) -> base.Boolean: """ @@ -2056,20 +2114,3 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.GET_GAME_HIGH_SCORES, payload) return [types.GameHighScore(**gamehighscore) for gamehighscore in result] - - async def send_poll(self, chat_id: typing.Union[base.Integer, base.String], - question: base.String, - options: typing.List[base.String], - reply_to_message_id: typing.Union[base.Integer, None]): - options = prepare_arg(options) - payload = generate_payload(**locals()) - - result = await self.request(api.Methods.SEND_POLL, payload) - return types.Message(**result) - - async def stop_poll(self, chat_id: typing.Union[base.String, base.Integer], - message_id: base.Integer) -> types.Poll: - payload = generate_payload(**locals()) - - result = await self.request(api.Methods.STOP_POLL, payload) - return types.Poll(**result) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 1dcd2c1f..e509ee74 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -43,7 +43,7 @@ from .passport_element_error import PassportElementError, PassportElementErrorDa PassportElementErrorSelfie from .passport_file import PassportFile from .photo_size import PhotoSize -from .poll import PollOptions, Poll +from .poll import PollOption, Poll from .pre_checkout_query import PreCheckoutQuery from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove from .response_parameters import ResponseParameters @@ -144,7 +144,7 @@ __all__ = ( 'PassportFile', 'PhotoSize', 'Poll', - 'PollOptions', + 'PollOption', 'PreCheckoutQuery', 'ReplyKeyboardMarkup', 'ReplyKeyboardRemove', diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index e3f8caca..316bca2d 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -4,7 +4,7 @@ from . import base from . import fields -class PollOptions(base.TelegramObject): +class PollOption(base.TelegramObject): text: base.String = fields.Field() voter_count: base.Integer = fields.Field() @@ -12,5 +12,5 @@ class PollOptions(base.TelegramObject): class Poll(base.TelegramObject): id: base.String = fields.Field() question: base.String = fields.Field() - options: typing.List[PollOptions] = fields.ListField(base=PollOptions) + options: typing.List[PollOption] = fields.ListField(base=PollOption) is_closed: base.Boolean = fields.Field() From b7e13daeaecd48cbd41866e4d11e6fdb7557d38c Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Tue, 16 Apr 2019 00:03:02 +0300 Subject: [PATCH 37/85] Update chat member --- aiogram/types/chat_member.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 321d77db..6679c5e0 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -1,4 +1,5 @@ import datetime +import warnings from . import base from . import fields @@ -24,15 +25,22 @@ class ChatMember(base.TelegramObject): can_restrict_members: base.Boolean = fields.Field() can_pin_messages: base.Boolean = fields.Field() can_promote_members: 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_other_messages: base.Boolean = fields.Field() can_add_web_page_previews: base.Boolean = fields.Field() def is_admin(self): + warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' + 'This method renamed to `is_chat_admin` and will be available until aiogram 2.2', + DeprecationWarning, stacklevel=2) + return self.is_chat_admin() + + def is_chat_admin(self): return ChatMemberStatus.is_admin(self.status) - def is_member(self): + def is_chat_member(self): return ChatMemberStatus.is_member(self.status) def __int__(self): @@ -54,8 +62,22 @@ class ChatMemberStatus(helper.Helper): @classmethod def is_admin(cls, role): - return role in [cls.ADMINISTRATOR, cls.CREATOR] + warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' + 'This method renamed to `is_chat_admin` and will be available until aiogram 2.2', + DeprecationWarning, stacklevel=2) + return cls.is_chat_admin(role) @classmethod def is_member(cls, role): + warnings.warn('`is_member` method deprecated due to updates in Bot API 4.2. ' + 'This method renamed to `is_chat_member` and will be available until aiogram 2.2', + DeprecationWarning, stacklevel=2) + return cls.is_chat_member(role) + + @classmethod + def is_chat_admin(cls, role): + return role in [cls.ADMINISTRATOR, cls.CREATOR] + + @classmethod + def is_chat_member(cls, role): return role in [cls.MEMBER, cls.ADMINISTRATOR, cls.CREATOR] From 129787db5ce2a75317edd81872c7abfb523b8bff Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Tue, 16 Apr 2019 00:04:05 +0300 Subject: [PATCH 38/85] Update message --- aiogram/types/message.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 5dde2c73..92cebe3b 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -51,6 +51,7 @@ class Message(base.TelegramObject): edit_date: datetime.datetime = fields.DateTimeField() media_group_id: base.String = fields.Field() author_signature: base.String = fields.Field() + forward_sender_name: base.String = fields.Field() text: base.String = fields.Field() entities: typing.List[MessageEntity] = fields.ListField(base=MessageEntity) caption_entities: typing.List[MessageEntity] = fields.ListField(base=MessageEntity) From fe7735b96dc47f0fa8ea815c5672e843606f205b Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Tue, 16 Apr 2019 00:20:44 +0300 Subject: [PATCH 39/85] Update Telegram IP addresses --- aiogram/dispatcher/webhook.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 8a5662bf..4c06c2af 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -1,10 +1,9 @@ import asyncio -import itertools - import asyncio.tasks import datetime import functools import ipaddress +import itertools import typing from typing import Dict, List, Optional, Union @@ -31,8 +30,8 @@ WEBHOOK = 'webhook' WEBHOOK_CONNECTION = 'WEBHOOK_CONNECTION' WEBHOOK_REQUEST = 'WEBHOOK_REQUEST' -TELEGRAM_IP_LOWER = ipaddress.IPv4Address('149.154.167.197') -TELEGRAM_IP_UPPER = ipaddress.IPv4Address('149.154.167.233') +TELEGRAM_SUBNET_1 = ipaddress.IPv4Network('149.154.160.0/20') +TELEGRAM_SUBNET_2 = ipaddress.IPv4Network('91.108.4.0/22') allowed_ips = set() @@ -48,18 +47,26 @@ def _check_ip(ip: str) -> bool: return address in allowed_ips -def allow_ip(*ips: str): +def allow_ip(*ips: typing.Union[str, ipaddress.IPv4Network, ipaddress.IPv4Address]): """ Allow ip address. :param ips: :return: """ - allowed_ips.update(ipaddress.IPv4Address(ip) for ip in ips) + for ip in ips: + if isinstance(ip, ipaddress.IPv4Address): + allowed_ips.add(ip) + elif isinstance(ip, str): + allowed_ips.add(ipaddress.IPv4Address(ip)) + elif isinstance(ip, ipaddress.IPv4Network): + allowed_ips.update(ip.hosts()) + else: + raise ValueError(f"Bad type of ipaddress: {type(ip)} ('{ip}')") # Allow access from Telegram servers -allow_ip(*(ip for ip in range(int(TELEGRAM_IP_LOWER), int(TELEGRAM_IP_UPPER) + 1))) +allow_ip(TELEGRAM_SUBNET_1, TELEGRAM_SUBNET_2) class WebhookRequestHandler(web.View): From 71c49fd08b7413d91c8e927ac57bb04261030e7f Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Tue, 16 Apr 2019 00:22:22 +0300 Subject: [PATCH 40/85] Small changes --- aiogram/bot/api.py | 8 +++----- aiogram/bot/bot.py | 7 +++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 16dbb9fa..051ae37f 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -147,7 +147,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 4.1 + List is updated to Bot API 4.2 """ mode = HelperMode.lowerCamelCase @@ -174,6 +174,7 @@ class Methods(Helper): STOP_MESSAGE_LIVE_LOCATION = Item() # stopMessageLiveLocation SEND_VENUE = Item() # sendVenue SEND_CONTACT = Item() # sendContact + SEND_POLL = Item() # sendPoll SEND_CHAT_ACTION = Item() # sendChatAction GET_USER_PROFILE_PHOTOS = Item() # getUserProfilePhotos GET_FILE = Item() # getFile @@ -202,6 +203,7 @@ class Methods(Helper): EDIT_MESSAGE_CAPTION = Item() # editMessageCaption EDIT_MESSAGE_MEDIA = Item() # editMessageMedia EDIT_MESSAGE_REPLY_MARKUP = Item() # editMessageReplyMarkup + STOP_POLL = Item() # stopPoll DELETE_MESSAGE = Item() # deleteMessage # Stickers @@ -229,10 +231,6 @@ class Methods(Helper): SET_GAME_SCORE = Item() # setGameScore GET_GAME_HIGH_SCORES = Item() # getGameHighScores - # Polls - SEND_POLL = Item() - STOP_POLL = Item() - @staticmethod def api_url(token, method): """ diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 0e13d62b..c47c9232 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -1585,15 +1585,14 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): async def delete_message(self, chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer) -> base.Boolean: """ - Use this method to delete a message, including service messages, with the following limitations + Use this method to delete a message, including service messages, with the following limitations: - A message can only be deleted if it was sent less than 48 hours ago. - - Bots can delete outgoing messages in groups and supergroups. + - Bots can delete outgoing messages in private chats, groups, and supergroups. + - Bots can delete incoming messages in private chats. - Bots granted can_post_messages permissions can delete outgoing messages in channels. - If the bot is an administrator of a group, it can delete any message there. - If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there. - The following methods and objects allow your bot to handle stickers and sticker sets. - Source: https://core.telegram.org/bots/api#deletemessage :param chat_id: Unique identifier for the target chat or username of the target channel From beca19b5e23d215aff86f6f241eefe32ceee5c4b Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Wed, 17 Apr 2019 23:24:23 +0300 Subject: [PATCH 41/85] Implements opportunity to change request timeouts --- aiogram/bot/base.py | 51 ++++++++++++++++++++++++++++++-- aiogram/dispatcher/dispatcher.py | 24 ++++++++++++--- aiogram/utils/executor.py | 4 +-- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 19031fd0..4f55dbd7 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -1,11 +1,14 @@ import asyncio +import contextlib import io import ssl import typing +from contextvars import ContextVar from typing import Dict, List, Optional, Union import aiohttp import certifi +from aiohttp.helpers import sentinel from . import api from ..types import ParseMode, base @@ -17,6 +20,7 @@ class BaseBot: """ Base class for bot. It's raw bot. """ + _ctx_timeout = ContextVar('TelegramRequestTimeout') def __init__( self, @@ -27,6 +31,7 @@ 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]] = None ): """ Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot @@ -45,6 +50,8 @@ class BaseBot: :type validate_token: :obj:`bool` :param parse_mode: You can set default parse mode :type parse_mode: :obj:`str` + :param timeout: Request timeout + :type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float]]` :raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError` """ # Authentication @@ -83,11 +90,51 @@ class BaseBot: self.proxy_auth = None else: connector = aiohttp.TCPConnector(limit=connections_limit, ssl=ssl_context, loop=self.loop) + self._timeout = None + self.timeout = timeout self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, json_serialize=json.dumps) self.parse_mode = parse_mode + @staticmethod + def _prepare_timeout( + value: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] + ) -> typing.Optional[aiohttp.ClientTimeout]: + if value is None or isinstance(value, aiohttp.ClientTimeout): + return value + return aiohttp.ClientTimeout(total=value) + + @property + def timeout(self): + timeout = self._ctx_timeout.get(self._timeout) + if timeout is None: + return sentinel + return timeout + + @timeout.setter + def timeout(self, value): + self._timeout = self._prepare_timeout(value) + + @timeout.deleter + def timeout(self): + self.timeout = None + + @contextlib.contextmanager + def request_timeout(self, timeout): + """ + Context manager implements opportunity to change request timeout in current context + + :param timeout: + :return: + """ + timeout = self._prepare_timeout(timeout) + token = self._ctx_timeout.set(timeout) + try: + yield + finally: + self._ctx_timeout.reset(token) + async def close(self): """ Close all client sessions @@ -113,11 +160,11 @@ class BaseBot: :raise: :obj:`aiogram.exceptions.TelegramApiError` """ return await api.make_request(self.session, self.__token, method, data, files, - proxy=self.proxy, proxy_auth=self.proxy_auth, **kwargs) + proxy=self.proxy, proxy_auth=self.proxy_auth, timeout=self.timeout, **kwargs) async def download_file(self, file_path: base.String, destination: Optional[base.InputFile] = None, - timeout: Optional[base.Integer] = 30, + timeout: Optional[base.Integer] = sentinel, chunk_size: Optional[base.Integer] = 65536, seek: Optional[base.Boolean] = True) -> Union[io.BytesIO, io.FileIO]: """ diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index d1096718..0223c85f 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -5,6 +5,9 @@ import logging import time import typing +import aiohttp +from aiohttp.helpers import sentinel + from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, FuncFilter, HashTag, Regexp, \ RegexpCommandsFilter, StateFilter, Text from .handler import Handler @@ -209,8 +212,13 @@ class Dispatcher(DataMixin, ContextInstanceMixin): return await self.bot.delete_webhook() - async def start_polling(self, timeout=20, relax=0.1, limit=None, reset_webhook=None, - fast: typing.Optional[bool] = True): + async def start_polling(self, + timeout=20, + relax=0.1, + limit=None, + reset_webhook=None, + fast: typing.Optional[bool] = True, + error_sleep: int = 5): """ Start long-polling @@ -238,12 +246,19 @@ class Dispatcher(DataMixin, ContextInstanceMixin): self._polling = True offset = None try: + current_request_timeout = self.bot.timeout + if current_request_timeout is not sentinel and timeout is not None: + request_timeout = aiohttp.ClientTimeout(total=current_request_timeout.total + timeout or 1) + else: + request_timeout = None + while self._polling: try: - updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout) + with self.bot.request_timeout(request_timeout): + updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout) except: log.exception('Cause exception while getting updates.') - await asyncio.sleep(15) + await asyncio.sleep(error_sleep) continue if updates: @@ -254,6 +269,7 @@ class Dispatcher(DataMixin, ContextInstanceMixin): if relax: await asyncio.sleep(relax) + finally: self._close_waiter._set_result(None) log.warning('Polling is stopped.') diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 34acf6e9..65594371 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -23,7 +23,7 @@ def _setup_callbacks(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=None, fast=True): + on_startup=None, on_shutdown=None, timeout=20, fast=True): """ Start bot in long-polling mode @@ -291,7 +291,7 @@ class Executor: self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name) self.run_app(**kwargs) - def start_polling(self, reset_webhook=None, timeout=None, fast=True): + def start_polling(self, reset_webhook=None, timeout=20, fast=True): """ Start bot in long-polling mode From 23f68628d5cc304157f7ba5d7c7087d6595fa779 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Thu, 18 Apr 2019 00:33:39 +0300 Subject: [PATCH 42/85] Deep-linking filter based on commands handler --- aiogram/dispatcher/filters/builtin.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index de61c560..86408260 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -93,8 +93,23 @@ class Command(Filter): class CommandStart(Command): - def __init__(self): + def __init__(self, deep_link=None): super(CommandStart, self).__init__(['start']) + self.deep_link = deep_link + + async def check(self, message: types.Message): + check = await super(CommandStart, self).check(message) + + if check and self.deep_link is not None: + if not isinstance(self.deep_link, re.Pattern): + return message.get_args() == self.deep_link + + match = self.deep_link.match(message.get_args()) + if match: + return {'deep_link': match} + return False + + return check class CommandHelp(Command): From ade5b8c2c960fa605a8c8b103a07b80f8bbd3999 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Thu, 18 Apr 2019 00:52:52 +0300 Subject: [PATCH 43/85] Type hint for CommandStart --- aiogram/dispatcher/filters/builtin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 86408260..08c3cbb6 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -1,5 +1,6 @@ import inspect import re +import typing from contextvars import ContextVar from dataclasses import dataclass, field from typing import Any, Dict, Iterable, Optional, Union @@ -93,7 +94,8 @@ class Command(Filter): class CommandStart(Command): - def __init__(self, deep_link=None): + def __init__(self, deep_link: typing.Optional[str, re.Pattern] = None): + super(CommandStart, self).__init__(['start']) self.deep_link = deep_link From 28b8ccd0bbe59241e4f7e7faa503054d4c70c8c0 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Thu, 18 Apr 2019 21:01:03 +0300 Subject: [PATCH 44/85] Update requirements --- dev_requirements.txt | 29 ++++++++++++++--------------- requirements.txt | 4 ++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 4aa84dd9..08363189 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,18 +1,17 @@ -r requirements.txt ujson>=1.35 -python-rapidjson>=0.6.3 -emoji>=0.5.0 -pytest>=3.5.0 -pytest-asyncio>=0.8.0 -tox>=3.0.0 -aresponses>=1.0.0 -uvloop>=0.9.1 -aioredis>=1.1.0 -wheel>=0.31.0 -sphinx>=1.7.3 -sphinx-rtd-theme>=0.3.0 -sphinxcontrib-programoutput>=0.11 -aresponses>=1.0.0 -aiohttp-socks>=0.1.5 -rethinkdb>=2.4.1 \ No newline at end of file +python-rapidjson>=0.7.0 +emoji>=0.5.2 +pytest>=4.4.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 +sphinx-rtd-theme>=0.4.3 +sphinxcontrib-programoutput>=0.14 +aiohttp-socks>=0.2.2 +rethinkdb>=2.4.1 diff --git a/requirements.txt b/requirements.txt index 4f038772..37d328b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -aiohttp>=3.4.4 +aiohttp>=3.5.4 Babel>=2.6.0 -certifi>=2018.8.24 +certifi>=2019.3.9 From 7d794fc5232de7ac702937a6f6eb945beb323050 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Thu, 18 Apr 2019 21:18:17 +0300 Subject: [PATCH 45/85] Try use new .readthedocs.yml config. --- .readthedocs.yml | 19 +++++++++++++++++++ readthedocs.yml | 5 ----- 2 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 .readthedocs.yml delete mode 100644 readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..c022e7f7 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,19 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +formats: all + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.7 + install: + - requirements: dev_requirements.txt diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index 87136e21..00000000 --- a/readthedocs.yml +++ /dev/null @@ -1,5 +0,0 @@ -conda: - file: environment.yml -python: - version: 3 - pip_install: true \ No newline at end of file From 65a165934569ff5971f6c94147ba725d4852126e Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Thu, 18 Apr 2019 21:20:03 +0300 Subject: [PATCH 46/85] Fix typing --- aiogram/dispatcher/filters/builtin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 08c3cbb6..5dea00f8 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -94,8 +94,7 @@ class Command(Filter): class CommandStart(Command): - def __init__(self, deep_link: typing.Optional[str, re.Pattern] = None): - + def __init__(self, deep_link: typing.Optional[typing.Union[str, re.Pattern]] = None): super(CommandStart, self).__init__(['start']) self.deep_link = deep_link From 7236541723fd42bc46db1e592af9ecd7b2ffd8dc Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Thu, 18 Apr 2019 21:30:02 +0300 Subject: [PATCH 47/85] Remove func filter --- aiogram/dispatcher/dispatcher.py | 5 +---- aiogram/dispatcher/filters/__init__.py | 3 +-- aiogram/dispatcher/filters/builtin.py | 16 ---------------- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 0223c85f..e11ff536 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -8,7 +8,7 @@ import typing import aiohttp from aiohttp.helpers import sentinel -from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, FuncFilter, HashTag, Regexp, \ +from .filters import Command, ContentTypeFilter, ExceptionsFilter, FiltersFactory, HashTag, Regexp, \ RegexpCommandsFilter, StateFilter, Text from .handler import Handler from .middlewares import MiddlewareManager @@ -114,9 +114,6 @@ class Dispatcher(DataMixin, ContextInstanceMixin): filters_factory.bind(ExceptionsFilter, event_handlers=[ self.errors_handlers ]) - filters_factory.bind(FuncFilter, exclude_event_handlers=[ - self.errors_handlers - ]) def __del__(self): self.stop_polling() diff --git a/aiogram/dispatcher/filters/__init__.py b/aiogram/dispatcher/filters/__init__.py index b11b8374..2ae959cf 100644 --- a/aiogram/dispatcher/filters/__init__.py +++ b/aiogram/dispatcher/filters/__init__.py @@ -1,5 +1,5 @@ from .builtin import Command, CommandHelp, CommandPrivacy, CommandSettings, CommandStart, ContentTypeFilter, \ - ExceptionsFilter, FuncFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text + ExceptionsFilter, HashTag, Regexp, RegexpCommandsFilter, StateFilter, Text from .factory import FiltersFactory from .filters import AbstractFilter, BoundFilter, Filter, FilterNotPassed, FilterRecord, execute_filter, \ check_filters, get_filter_spec, get_filters_spec @@ -15,7 +15,6 @@ __all__ = [ 'ContentTypeFilter', 'ExceptionsFilter', 'HashTag', - 'FuncFilter', 'Filter', 'FilterNotPassed', 'FilterRecord', diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 5dea00f8..538330c3 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -398,22 +398,6 @@ class StateFilter(BoundFilter): return False -class FuncFilter(BoundFilter): - key = 'func' - - def __init__(self, dispatcher, func): - self.dispatcher = dispatcher - self.func = func - - warn_deprecated('"func" filter will be removed in 2.1 version.\n' - 'Read mode: https://aiogram.readthedocs.io/en/dev-2.x/migration_1_to_2.html#custom-filters', - stacklevel=8) - - async def check(self, obj) -> bool: - from .filters import check_filter - return await check_filter(self.dispatcher, self.func, (obj,)) - - class ExceptionsFilter(BoundFilter): """ Filter for exceptions From 0f53e5fbd78a622673eeba9b725324e54cd03005 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Thu, 18 Apr 2019 21:30:13 +0300 Subject: [PATCH 48/85] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 77ceb357..6d30f863 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.1.dev2' +__version__ = '2.1' __api_version__ = '4.2' From 2af930149ce2482547721e2c8755c10307295e48 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 21 Apr 2019 01:16:42 +0300 Subject: [PATCH 49/85] Docs for filters (Not fully) --- aiogram/__init__.py | 2 +- aiogram/dispatcher/filters/builtin.py | 90 ++++++++++++++-- aiogram/dispatcher/filters/factory.py | 2 +- aiogram/dispatcher/filters/filters.py | 41 +++++-- docs/source/dispatcher/filters.rst | 147 +++++++++++++++++++++++++- docs/source/quick_start.rst | 6 +- 6 files changed, 263 insertions(+), 25 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 6d30f863..b5e647c9 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.1' +__version__ = '2.1.1.dev1' __api_version__ = '4.2' diff --git a/aiogram/dispatcher/filters/builtin.py b/aiogram/dispatcher/filters/builtin.py index 538330c3..011b9b67 100644 --- a/aiogram/dispatcher/filters/builtin.py +++ b/aiogram/dispatcher/filters/builtin.py @@ -10,12 +10,15 @@ from babel.support import LazyProxy from aiogram import types from aiogram.dispatcher.filters.filters import BoundFilter, Filter from aiogram.types import CallbackQuery, Message, InlineQuery, Poll -from aiogram.utils.deprecated import warn_deprecated class Command(Filter): """ - You can handle commands by using this filter + You can handle commands by using this filter. + + If filter is successful processed the :obj:`Command.CommandObj` will be passed to the handler arguments. + + By default this filter is registered for messages and edited messages handlers. """ def __init__(self, commands: Union[Iterable, str], @@ -23,12 +26,22 @@ class Command(Filter): ignore_case: bool = True, ignore_mention: bool = False): """ - Filter can be initialized from filters factory or by simply creating instance of this class + Filter can be initialized from filters factory or by simply creating instance of this class. - :param commands: command or list of commands - :param prefixes: - :param ignore_case: - :param ignore_mention: + Examples: + + .. code-block:: python + + @dp.message_handler(commands=['myCommand']) + @dp.message_handler(Command(['myCommand'])) + @dp.message_handler(commands=['myCommand'], commands_prefix='!/') + + :param commands: Command or list of commands always without leading slashes (prefix) + :param prefixes: Allowed commands prefix. By default is slash. + If you change the default behavior pass the list of prefixes to this argument. + :param ignore_case: Ignore case of the command + :param ignore_mention: Ignore mention in command + (By default this filter pass only the commands addressed to current bot) """ if isinstance(commands, str): commands = (commands,) @@ -43,15 +56,21 @@ class Command(Filter): """ Validator for filters factory + From filters factory this filter can be registered with arguments: + + - ``command`` + - ``commands_prefix`` (will be passed as ``prefixes``) + - ``commands_ignore_mention`` (will be passed as ``ignore_mention`` + :param full_config: :return: config or empty dict """ config = {} if 'commands' in full_config: config['commands'] = full_config.pop('commands') - if 'commands_prefix' in full_config: + if config and 'commands_prefix' in full_config: config['prefixes'] = full_config.pop('commands_prefix') - if 'commands_ignore_mention' in full_config: + if config and 'commands_ignore_mention' in full_config: config['ignore_mention'] = full_config.pop('commands_ignore_mention') return config @@ -74,17 +93,37 @@ class Command(Filter): @dataclass class CommandObj: + """ + Instance of this object is always has command and it prefix. + + Can be passed as keyword argument ``command`` to the handler + """ + + """Command prefix""" prefix: str = '/' + """Command without prefix and mention""" command: str = '' + """Mention (if available)""" mention: str = None + """Command argument""" args: str = field(repr=False, default=None) @property def mentioned(self) -> bool: + """ + This command has mention? + + :return: + """ return bool(self.mention) @property def text(self) -> str: + """ + Generate original text from object + + :return: + """ line = self.prefix + self.command if self.mentioned: line += '@' + self.mention @@ -94,11 +133,32 @@ class Command(Filter): class CommandStart(Command): + """ + This filter based on :obj:`Command` filter but can handle only ``/start`` command. + """ + def __init__(self, deep_link: typing.Optional[typing.Union[str, re.Pattern]] = None): + """ + Also this filter can handle `deep-linking `_ arguments. + + Example: + + .. code-block:: python + + @dp.message_handler(CommandStart(re.compile(r'ref-([\\d]+)'))) + + :param deep_link: string or compiled regular expression (by ``re.compile(...)``). + """ super(CommandStart, self).__init__(['start']) self.deep_link = deep_link async def check(self, message: types.Message): + """ + If deep-linking is passed to the filter result of the matching will be passed as ``deep_link`` to the handler + + :param message: + :return: + """ check = await super(CommandStart, self).check(message) if check and self.deep_link is not None: @@ -114,16 +174,28 @@ class CommandStart(Command): class CommandHelp(Command): + """ + This filter based on :obj:`Command` filter but can handle only ``/help`` command. + """ + def __init__(self): super(CommandHelp, self).__init__(['help']) class CommandSettings(Command): + """ + This filter based on :obj:`Command` filter but can handle only ``/settings`` command. + """ + def __init__(self): super(CommandSettings, self).__init__(['settings']) class CommandPrivacy(Command): + """ + This filter based on :obj:`Command` filter but can handle only ``/privacy`` command. + """ + def __init__(self): super(CommandPrivacy, self).__init__(['privacy']) diff --git a/aiogram/dispatcher/filters/factory.py b/aiogram/dispatcher/filters/factory.py index 099a9b60..89e3e792 100644 --- a/aiogram/dispatcher/filters/factory.py +++ b/aiogram/dispatcher/filters/factory.py @@ -6,7 +6,7 @@ from ..handler import Handler class FiltersFactory: """ - Default filters factory + Filters factory """ def __init__(self, dispatcher): diff --git a/aiogram/dispatcher/filters/filters.py b/aiogram/dispatcher/filters/filters.py index a6d83c62..46e44fc9 100644 --- a/aiogram/dispatcher/filters/filters.py +++ b/aiogram/dispatcher/filters/filters.py @@ -128,24 +128,29 @@ class FilterRecord: class AbstractFilter(abc.ABC): """ - Abstract class for custom filters + Abstract class for custom filters. """ @classmethod @abc.abstractmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: """ - Validate and parse config + Validate and parse config. - :param full_config: - :return: config + This method will be called by the filters factory when you bind this filter. + Must be overridden. + + :param full_config: dict with arguments passed to handler registrar + :return: Current filter config """ pass @abc.abstractmethod async def check(self, *args) -> bool: """ - Check object + Will be called when filters checks. + + This method must be overridden. :param args: :return: @@ -173,24 +178,46 @@ class AbstractFilter(abc.ABC): class Filter(AbstractFilter): """ - You can make subclasses of that class for custom filters + You can make subclasses of that class for custom filters. + + Method ``check`` must be overridden """ @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: + """ + Here method ``validate`` is optional. + If you need to use filter from filters factory you need to override this method. + + :param full_config: dict with arguments passed to handler registrar + :return: Current filter config + """ pass class BoundFilter(Filter): """ - Base class for filters with default validator + To easily create your own filters with one parameter, you can inherit from this filter. + + You need to implement ``__init__`` method with single argument related with key attribute + and ``check`` method where you need to implement filter logic. """ + + """Unique name of the filter argument. You need to override this attribute.""" key = None + """If :obj:`True` this filter will be added to the all of the registered handlers""" required = False + """Default value for configure required filters""" default = None @classmethod def validate(cls, full_config: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + """ + If ``cls.key`` is not :obj:`None` and that is in config returns config with that argument. + + :param full_config: + :return: + """ if cls.key is not None: if cls.key in full_config: return {cls.key: full_config[cls.key]} diff --git a/docs/source/dispatcher/filters.rst b/docs/source/dispatcher/filters.rst index 2615a0f7..d103ac36 100644 --- a/docs/source/dispatcher/filters.rst +++ b/docs/source/dispatcher/filters.rst @@ -4,16 +4,155 @@ Filters Basics ====== -Coming soon... + +Filter factory greatly simplifies the reuse of filters when registering handlers. Filters factory =============== -Coming soon... + +.. autoclass:: aiogram.dispatcher.filters.factory.FiltersFactory + :members: + :show-inheritance: Builtin filters =============== -Coming soon... +``aiogram`` has some builtin filters. Here you can see all of them: + +Command +------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.Command + :members: + :show-inheritance: + +CommandStart +------------ + +.. autoclass:: aiogram.dispatcher.filters.builtin.CommandStart + :members: + :show-inheritance: + +CommandHelp +----------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.CommandHelp + :members: + :show-inheritance: + +CommandSettings +--------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.CommandSettings + :members: + :show-inheritance: + + +CommandPrivacy +-------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.CommandPrivacy + :members: + :show-inheritance: + + +Text +---- + +.. autoclass:: aiogram.dispatcher.filters.builtin.Text + :members: + :show-inheritance: + + +HashTag +------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.HashTag + :members: + :show-inheritance: + + +Regexp +------ + +.. autoclass:: aiogram.dispatcher.filters.builtin.Regexp + :members: + :show-inheritance: + + +RegexpCommandsFilter +-------------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.RegexpCommandsFilter + :members: + :show-inheritance: + + +ContentTypeFilter +----------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.ContentTypeFilter + :members: + :show-inheritance: + + +StateFilter +----------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.StateFilter + :members: + :show-inheritance: + + +ExceptionsFilter +---------------- + +.. autoclass:: aiogram.dispatcher.filters.builtin.ExceptionsFilter + :members: + :show-inheritance: + Making own filters (Custom filters) =================================== -Coming soon... + +Own filter can be: + +- any callable object +- any async function +- any anonymous function (Example: ``lambda msg: msg.text == 'spam'``) +- Subclass of :obj:`AbstractFilter`, :obj:`Filter` or :obj:`BoundFilter` + + +AbstractFilter +-------------- +.. autoclass:: aiogram.dispatcher.filters.filters.AbstractFilter + :members: + :show-inheritance: + +Filter +------ +.. autoclass:: aiogram.dispatcher.filters.filters.Filter + :members: + :show-inheritance: + +BoundFilter +----------- +.. autoclass:: aiogram.dispatcher.filters.filters.BoundFilter + :members: + :show-inheritance: + + +.. code-block:: python + + class ChatIdFilter(BoundFilter): + key = 'chat_id' + + def __init__(self, chat_id: typing.Union[typing.Iterable, int]): + if isinstance(chat_id, int): + chat_id = [chat_id] + self.chat_id = chat_id + + def check(self, message: types.Message) -> bool: + return message.chat.id in self.chat_id + + + dp.filters_factory.bind(ChatIdFilter, event_handlers=[dp.message_handlers]) diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index 07224d19..b0724a78 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -22,19 +22,19 @@ Next step: interaction with bots starts with one command. Register your first co .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 21-25 + :lines: 20-25 If you want to handle all messages in the chat simply add handler without filters: .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 28-30 + :lines: 35-37 Last step: run long polling. .. literalinclude:: ../../examples/echo_bot.py :language: python - :lines: 33-34 + :lines: 40-41 Summary ------- From 8a55b677154ca04c560cf991dfeda3e4f9ca5406 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 28 Apr 2019 23:33:34 +0500 Subject: [PATCH 50/85] Type hint and fix parse_mode parameter Fix: t.me/aiogram_ru/54632 --- aiogram/bot/bot.py | 36 ++- aiogram/types/message.py | 499 ++++++++++++++++++++++++++++----------- 2 files changed, 386 insertions(+), 149 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index c47c9232..b0fc3725 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -201,7 +201,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -267,7 +268,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -327,7 +329,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -377,7 +380,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -438,7 +442,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -557,7 +562,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -605,7 +611,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -679,7 +686,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -794,7 +802,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -835,7 +844,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 @@ -872,7 +882,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :type disable_notification: :obj:`typing.Optional[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[Integer]` - :param reply_markup: Additional interface options + :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 @@ -1631,7 +1642,8 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :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 + :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 diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 92cebe3b..8a7e7e9c 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -12,7 +12,9 @@ from .audio import Audio from .chat import Chat, ChatType from .contact import Contact from .document import Document +from .force_reply import ForceReply from .game import Game +from .inline_keyboard import InlineKeyboardMarkup from .input_media import MediaGroup, InputMedia from .invoice import Invoice from .location import Location @@ -20,6 +22,7 @@ from .message_entity import MessageEntity from .passport_data import PassportData from .photo_size import PhotoSize from .poll import Poll +from .reply_keyboard import ReplyKeyboardRemove, ReplyKeyboardMarkup from .sticker import Sticker from .successful_payment import SuccessfulPayment from .user import User @@ -281,20 +284,38 @@ class Message(base.TelegramObject): return md.hlink(text, url) return md.link(text, url) - async def answer(self, text, parse_mode=None, disable_web_page_preview=None, - disable_notification=None, reply_markup=None, reply=False) -> Message: + async def answer(self, text: base.String, + parse_mode: typing.Union[base.String, None] = None, + disable_web_page_preview: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Answer to this message - :param text: str - :param parse_mode: str - :param disable_web_page_preview: bool - :param disable_notification: bool - :param reply_markup: + :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]` + :param disable_web_page_preview: Disables link previews for links in this message + :type disable_web_page_preview: :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.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` :param reply: fill 'reply_to_message_id' - :return: :class:`aiogram.types.Message` + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned + :rtype: :obj:`types.Message` """ - return await self.bot.send_message(chat_id=self.chat.id, text=text, + return await self.bot.send_message(chat_id=self.chat.id, + text=text, parse_mode=parse_mode, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, @@ -303,39 +324,56 @@ class Message(base.TelegramObject): async def answer_photo(self, photo: typing.Union[base.InputFile, base.String], caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send photos. Source: https://core.telegram.org/bots/api#sendphoto - :param photo: Photo to send. + :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-200 characters + :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]` - :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :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_markup: Additional interface options. + :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. + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_photo(chat_id=self.chat.id, photo=photo, caption=caption, + return await self.bot.send_photo(chat_id=self.chat.id, + photo=photo, + caption=caption, + parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) async def answer_audio(self, audio: typing.Union[base.InputFile, base.String], caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, duration: typing.Union[base.Integer, None] = None, performer: typing.Union[base.String, None] = None, title: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .mp3 format. @@ -348,6 +386,9 @@ class Message(base.TelegramObject): :type audio: :obj:`typing.Union[base.InputFile, base.String]` :param caption: Audio caption, 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 your bot's message. + :type parse_mode: :obj:`typing.Union[base.String, None]` :param duration: Duration of the audio in seconds :type duration: :obj:`typing.Union[base.Integer, None]` :param performer: Performer @@ -356,7 +397,8 @@ class Message(base.TelegramObject): :type title: :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. + :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' @@ -366,6 +408,7 @@ class Message(base.TelegramObject): return await self.bot.send_audio(chat_id=self.chat.id, audio=audio, caption=caption, + parse_mode=parse_mode, duration=duration, performer=performer, title=title, @@ -373,8 +416,7 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def answer_animation(self, - animation: typing.Union[base.InputFile, base.String], + 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, @@ -382,8 +424,11 @@ class Message(base.TelegramObject): caption: typing.Union[base.String, None] = None, parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -420,7 +465,8 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_animation(self.chat.id, animation=animation, + return await self.bot.send_animation(self.chat.id, + animation=animation, duration=duration, width=width, height=height, @@ -429,14 +475,17 @@ class Message(base.TelegramObject): parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup - ) + reply_markup=reply_markup) async def answer_document(self, document: typing.Union[base.InputFile, base.String], caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send general files. @@ -448,9 +497,13 @@ class Message(base.TelegramObject): :type document: :obj:`typing.Union[base.InputFile, base.String]` :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. + :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' @@ -460,6 +513,7 @@ class Message(base.TelegramObject): return await self.bot.send_document(chat_id=self.chat.id, document=document, caption=caption, + parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) @@ -469,9 +523,13 @@ class Message(base.TelegramObject): width: typing.Union[base.Integer, None] = None, height: typing.Union[base.Integer, None] = None, caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). @@ -488,9 +546,13 @@ class Message(base.TelegramObject): :type height: :obj:`typing.Union[base.Integer, 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]` + :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. + :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' @@ -503,16 +565,21 @@ class Message(base.TelegramObject): width=width, height=height, caption=caption, + parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) async def answer_voice(self, voice: typing.Union[base.InputFile, base.String], caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, duration: typing.Union[base.Integer, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. @@ -526,11 +593,15 @@ class Message(base.TelegramObject): :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]` + :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 duration: Duration of the voice message in seconds :type duration: :obj:`typing.Union[base.Integer, 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. + :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' @@ -540,6 +611,7 @@ class Message(base.TelegramObject): return await self.bot.send_voice(chat_id=self.chat.id, voice=voice, caption=caption, + parse_mode=parse_mode, duration=duration, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, @@ -549,8 +621,11 @@ class Message(base.TelegramObject): duration: typing.Union[base.Integer, None] = None, length: typing.Union[base.Integer, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send video messages. @@ -565,7 +640,8 @@ class Message(base.TelegramObject): :type length: :obj:`typing.Union[base.Integer, 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. + :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' @@ -582,7 +658,7 @@ class Message(base.TelegramObject): async def answer_media_group(self, media: typing.Union[MediaGroup, typing.List], disable_notification: typing.Union[base.Boolean, None] = None, - reply=False) -> typing.List[Message]: + reply: base.Boolean = False) -> typing.List[Message]: """ Use this method to send a group of photos or videos as an album. @@ -601,11 +677,15 @@ class Message(base.TelegramObject): disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None) - async def answer_location(self, latitude: base.Float, - longitude: base.Float, live_period: typing.Union[base.Integer, None] = None, + async def answer_location(self, + latitude: base.Float, longitude: base.Float, + live_period: typing.Union[base.Integer, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send point on the map. @@ -619,7 +699,8 @@ class Message(base.TelegramObject): :type live_period: :obj:`typing.Union[base.Integer, 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. + :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' @@ -634,11 +715,16 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def answer_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, + async def answer_venue(self, + latitude: base.Float, longitude: base.Float, + title: base.String, address: base.String, foursquare_id: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send information about a venue. @@ -656,7 +742,8 @@ class Message(base.TelegramObject): :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. + :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' @@ -676,8 +763,11 @@ class Message(base.TelegramObject): async def answer_contact(self, phone_number: base.String, first_name: base.String, last_name: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send phone contacts. @@ -691,7 +781,8 @@ class Message(base.TelegramObject): :type last_name: :obj:`typing.Union[base.String, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. + :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' @@ -707,7 +798,11 @@ class Message(base.TelegramObject): async def answer_sticker(self, sticker: typing.Union[base.InputFile, base.String], disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, reply=False) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = False) -> Message: """ Use this method to send .webp stickers. @@ -717,32 +812,52 @@ class Message(base.TelegramObject): :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]` - :param reply_markup: Additional interface options. + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` :param reply: fill 'reply_to_message_id' :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_sticker(chat_id=self.chat.id, sticker=sticker, + return await self.bot.send_sticker(chat_id=self.chat.id, + sticker=sticker, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def reply(self, text, parse_mode=None, disable_web_page_preview=None, - disable_notification=None, reply_markup=None, reply=True) -> Message: + async def reply(self, text: base.String, + parse_mode: typing.Union[base.String, None] = None, + disable_web_page_preview: typing.Union[base.Boolean, None] = None, + disable_notification: typing.Union[base.Boolean, None] = None, + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Reply to this message - :param text: str - :param parse_mode: str - :param disable_web_page_preview: bool - :param disable_notification: bool - :param reply_markup: + :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]` + :param disable_web_page_preview: Disables link previews for links in this message + :type disable_web_page_preview: :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.Union[base.Boolean, None]` + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user + :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, + types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` :param reply: fill 'reply_to_message_id' - :return: :class:`aiogram.types.Message` + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned + :rtype: :obj:`types.Message` """ - return await self.bot.send_message(chat_id=self.chat.id, text=text, + return await self.bot.send_message(chat_id=self.chat.id, + text=text, parse_mode=parse_mode, disable_web_page_preview=disable_web_page_preview, disable_notification=disable_notification, @@ -751,8 +866,13 @@ class Message(base.TelegramObject): async def reply_photo(self, photo: typing.Union[base.InputFile, base.String], caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send photos. @@ -762,28 +882,39 @@ class Message(base.TelegramObject): :type photo: :obj:`typing.Union[base.InputFile, base.String]` :param caption: Photo caption (may also be used when resending photos 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 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_markup: Additional interface options. + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` :param reply: fill 'reply_to_message_id' :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_photo(chat_id=self.chat.id, photo=photo, caption=caption, + return await self.bot.send_photo(chat_id=self.chat.id, + photo=photo, + caption=caption, + parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) async def reply_audio(self, audio: typing.Union[base.InputFile, base.String], caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, duration: typing.Union[base.Integer, None] = None, performer: typing.Union[base.String, None] = None, title: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .mp3 format. @@ -796,6 +927,9 @@ class Message(base.TelegramObject): :type audio: :obj:`typing.Union[base.InputFile, base.String]` :param caption: Audio caption, 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 your bot's message. + :type parse_mode: :obj:`typing.Union[base.String, None]` :param duration: Duration of the audio in seconds :type duration: :obj:`typing.Union[base.Integer, None]` :param performer: Performer @@ -804,7 +938,8 @@ class Message(base.TelegramObject): :type title: :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. + :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' @@ -814,6 +949,7 @@ class Message(base.TelegramObject): return await self.bot.send_audio(chat_id=self.chat.id, audio=audio, caption=caption, + parse_mode=parse_mode, duration=duration, performer=performer, title=title, @@ -821,8 +957,7 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def send_animation(self, - animation: typing.Union[base.InputFile, base.String], + async def send_animation(self, animation: typing.Union[base.InputFile, base.String], duration: typing.Union[base.Integer, None] = None, width: typing.Union[base.Integer, None] = None, height: typing.Union[base.Integer, None] = None, @@ -830,8 +965,11 @@ class Message(base.TelegramObject): caption: typing.Union[base.String, None] = None, parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -872,7 +1010,8 @@ class Message(base.TelegramObject): 'Use "Message.reply_animation" instead.', stacklevel=8) - return await self.bot.send_animation(self.chat.id, animation=animation, + return await self.bot.send_animation(self.chat.id, + animation=animation, duration=duration, width=width, height=height, @@ -881,11 +1020,9 @@ class Message(base.TelegramObject): parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup - ) + reply_markup=reply_markup) - async def reply_animation(self, - animation: typing.Union[base.InputFile, base.String], + 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, @@ -893,8 +1030,11 @@ class Message(base.TelegramObject): caption: typing.Union[base.String, None] = None, parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). @@ -931,7 +1071,8 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - return await self.bot.send_animation(self.chat.id, animation=animation, + return await self.bot.send_animation(self.chat.id, + animation=animation, duration=duration, width=width, height=height, @@ -940,14 +1081,17 @@ class Message(base.TelegramObject): parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, - reply_markup=reply_markup - ) + reply_markup=reply_markup) async def reply_document(self, document: typing.Union[base.InputFile, base.String], caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send general files. @@ -959,9 +1103,13 @@ class Message(base.TelegramObject): :type document: :obj:`typing.Union[base.InputFile, base.String]` :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. + :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' @@ -971,6 +1119,7 @@ class Message(base.TelegramObject): return await self.bot.send_document(chat_id=self.chat.id, document=document, caption=caption, + parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) @@ -980,9 +1129,13 @@ class Message(base.TelegramObject): width: typing.Union[base.Integer, None] = None, height: typing.Union[base.Integer, None] = None, caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). @@ -999,9 +1152,13 @@ class Message(base.TelegramObject): :type height: :obj:`typing.Union[base.Integer, 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]` + :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. + :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' @@ -1014,16 +1171,21 @@ class Message(base.TelegramObject): width=width, height=height, caption=caption, + parse_mode=parse_mode, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) async def reply_voice(self, voice: typing.Union[base.InputFile, base.String], caption: typing.Union[base.String, None] = None, + parse_mode: typing.Union[base.String, None] = None, duration: typing.Union[base.Integer, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. @@ -1037,11 +1199,15 @@ class Message(base.TelegramObject): :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]` + :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 duration: Duration of the voice message in seconds :type duration: :obj:`typing.Union[base.Integer, 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. + :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' @@ -1051,6 +1217,7 @@ class Message(base.TelegramObject): return await self.bot.send_voice(chat_id=self.chat.id, voice=voice, caption=caption, + parse_mode=parse_mode, duration=duration, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, @@ -1060,8 +1227,11 @@ class Message(base.TelegramObject): duration: typing.Union[base.Integer, None] = None, length: typing.Union[base.Integer, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long. Use this method to send video messages. @@ -1076,7 +1246,8 @@ class Message(base.TelegramObject): :type length: :obj:`typing.Union[base.Integer, 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. + :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' @@ -1093,7 +1264,7 @@ class Message(base.TelegramObject): async def reply_media_group(self, media: typing.Union[MediaGroup, typing.List], disable_notification: typing.Union[base.Boolean, None] = None, - reply=True) -> typing.List[Message]: + reply: base.Boolean = True) -> typing.List[Message]: """ Use this method to send a group of photos or videos as an album. @@ -1112,11 +1283,15 @@ class Message(base.TelegramObject): disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None) - async def reply_location(self, latitude: base.Float, - longitude: base.Float, live_period: typing.Union[base.Integer, None] = None, + async def reply_location(self, + latitude: base.Float, longitude: base.Float, + live_period: typing.Union[base.Integer, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send point on the map. @@ -1130,7 +1305,8 @@ class Message(base.TelegramObject): :type live_period: :obj:`typing.Union[base.Integer, 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. + :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' @@ -1145,11 +1321,16 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def send_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, + async def send_venue(self, + latitude: base.Float, longitude: base.Float, + title: base.String, address: base.String, foursquare_id: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send information about a venue. @@ -1167,7 +1348,8 @@ class Message(base.TelegramObject): :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. + :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' @@ -1188,11 +1370,16 @@ class Message(base.TelegramObject): reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def reply_venue(self, latitude: base.Float, longitude: base.Float, title: base.String, address: base.String, + async def reply_venue(self, + latitude: base.Float, longitude: base.Float, + title: base.String, address: base.String, foursquare_id: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send information about a venue. @@ -1210,7 +1397,8 @@ class Message(base.TelegramObject): :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. + :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' @@ -1230,8 +1418,11 @@ class Message(base.TelegramObject): async def send_contact(self, phone_number: base.String, first_name: base.String, last_name: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send phone contacts. @@ -1245,7 +1436,8 @@ class Message(base.TelegramObject): :type last_name: :obj:`typing.Union[base.String, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. + :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' @@ -1266,8 +1458,11 @@ class Message(base.TelegramObject): async def reply_contact(self, phone_number: base.String, first_name: base.String, last_name: typing.Union[base.String, None] = None, disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, - reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send phone contacts. @@ -1281,7 +1476,8 @@ class Message(base.TelegramObject): :type last_name: :obj:`typing.Union[base.String, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Union[base.Boolean, None]` - :param reply_markup: Additional interface options. + :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' @@ -1297,7 +1493,11 @@ class Message(base.TelegramObject): async def reply_sticker(self, sticker: typing.Union[base.InputFile, base.String], disable_notification: typing.Union[base.Boolean, None] = None, - reply_markup=None, reply=True) -> Message: + reply_markup: typing.Union[InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, + ForceReply, None] = None, + reply: base.Boolean = True) -> Message: """ Use this method to send .webp stickers. @@ -1307,32 +1507,40 @@ class Message(base.TelegramObject): :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]` - :param reply_markup: Additional interface options. + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, + custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` :param reply: fill 'reply_to_message_id' :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - return await self.bot.send_sticker(chat_id=self.chat.id, sticker=sticker, + return await self.bot.send_sticker(chat_id=self.chat.id, + sticker=sticker, disable_notification=disable_notification, reply_to_message_id=self.message_id if reply else None, reply_markup=reply_markup) - async def forward(self, chat_id, disable_notification=None) -> Message: + async def forward(self, chat_id: typing.Union[base.Integer, base.String], + disable_notification: typing.Union[base.Boolean, None] = None) -> Message: """ Forward this message - :param chat_id: - :param disable_notification: - :return: + Source: https://core.telegram.org/bots/api#forwardmessage + + :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]` + :return: On success, the sent Message is returned + :rtype: :obj:`types.Message` """ return await self.bot.forward_message(chat_id, self.chat.id, self.message_id, disable_notification) 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=None): + reply_markup: typing.Union[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). @@ -1359,7 +1567,7 @@ class Message(base.TelegramObject): async def edit_caption(self, caption: base.String, parse_mode: typing.Union[base.String, None] = None, - reply_markup=None): + reply_markup: typing.Union[InlineKeyboardMarkup] = None): """ Use this method to edit captions of messages sent by the bot or via the bot (for inline bots). @@ -1379,7 +1587,8 @@ class Message(base.TelegramObject): return await self.bot.edit_message_caption(chat_id=self.chat.id, message_id=self.message_id, caption=caption, parse_mode=parse_mode, reply_markup=reply_markup) - async def edit_media(self, media: InputMedia, reply_markup=None): + async def edit_media(self, media: InputMedia, + reply_markup: typing.Union[InlineKeyboardMarkup] = None): """ Use this method to edit audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. @@ -1403,7 +1612,7 @@ class Message(base.TelegramObject): return await self.bot.edit_message_media(media=media, chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) - async def edit_reply_markup(self, reply_markup=None): + async def edit_reply_markup(self, reply_markup: typing.Union[InlineKeyboardMarkup] = None): """ Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). @@ -1418,7 +1627,7 @@ class Message(base.TelegramObject): return await self.bot.edit_message_reply_markup(chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) - async def delete_reply_markup(self): + async def delete_reply_markup(self) -> typing.Union[Message, base.Boolean]: """ Use this method to delete reply markup of messages sent by the bot or via the bot (for inline bots). @@ -1428,8 +1637,9 @@ class Message(base.TelegramObject): """ return await self.bot.edit_message_reply_markup(chat_id=self.chat.id, message_id=self.message_id) - async def edit_live_location(self, latitude: base.Float, longitude: base.Float, - reply_markup=None) -> typing.Union[Message, base.Boolean]: + async def edit_live_location(self, latitude: base.Float, + longitude: base.Float, + reply_markup: typing.Union[InlineKeyboardMarkup] = None): """ 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 @@ -1451,7 +1661,7 @@ class Message(base.TelegramObject): chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) - async def stop_live_location(self, reply_markup=None) -> typing.Union[Message, base.Boolean]: + async def stop_live_location(self, reply_markup: typing.Union[InlineKeyboardMarkup] = None): """ Use this method to stop updating a live location message sent by the bot or via the bot (for inline bots) before live_period expires. @@ -1467,20 +1677,35 @@ class Message(base.TelegramObject): return await self.bot.stop_message_live_location(chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) - async def delete(self): + async def delete(self) -> base.Boolean: """ - Delete this message + Use this method to delete a message, including service messages, with the following limitations: + - A message can only be deleted if it was sent less than 48 hours ago. + - Bots can delete outgoing messages in private chats, groups, and supergroups. + - Bots can delete incoming messages in private chats. + - Bots granted can_post_messages permissions can delete outgoing messages in channels. + - If the bot is an administrator of a group, it can delete any message there. + - If the bot has can_delete_messages permission in a supergroup or a channel, it can delete any message there. - :return: bool + Source: https://core.telegram.org/bots/api#deletemessage + + :return: Returns True on success + :rtype: :obj:`base.Boolean` """ return await self.bot.delete_message(self.chat.id, self.message_id) - async def pin(self, disable_notification: bool = False): + async def pin(self, disable_notification: typing.Union[base.Boolean, None] = None): """ - Pin message + 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. - :param disable_notification: - :return: + 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]` + :return: Returns True on success + :rtype: :obj:`base.Boolean` """ return await self.chat.pin_message(self.message_id, disable_notification) From e3f9d83b5f32bb5d226464f67f26423877340faa Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 28 Apr 2019 23:35:02 +0500 Subject: [PATCH 51/85] Update message.py --- aiogram/types/message.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 8a7e7e9c..049aa1a5 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -878,21 +878,22 @@ class Message(base.TelegramObject): Source: https://core.telegram.org/bots/api#sendphoto - :param photo: Photo to send. + :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-200 characters + :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]` :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. + :param disable_notification: Sends the message silently. Users will receive a notification with no sound :type disable_notification: :obj:`typing.Union[base.Boolean, None]` :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user :type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply, None]` :param reply: fill 'reply_to_message_id' - :return: On success, the sent Message is returned. + :type reply: :obj:`base.Boolean` + :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ return await self.bot.send_photo(chat_id=self.chat.id, From d1fab6b91022822c32337c8491de3f2f8b07b384 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Fri, 3 May 2019 13:10:15 +0500 Subject: [PATCH 52/85] Update message.py --- aiogram/types/message.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 049aa1a5..cc392749 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1541,7 +1541,8 @@ 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) -> typing.Union[Message, base.Boolean]: + reply_markup: typing.Union[InlineKeyboardMarkup, + None] = None) -> typing.Union[Message, base.Boolean]: """ Use this method to edit text and game messages sent by the bot or via the bot (for inline bots). @@ -1568,7 +1569,8 @@ 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): + reply_markup: typing.Union[InlineKeyboardMarkup, + None] = None) -> typing.Union[Message, base.Boolean]: """ Use this method to edit captions of messages sent by the bot or via the bot (for inline bots). @@ -1589,7 +1591,8 @@ class Message(base.TelegramObject): parse_mode=parse_mode, reply_markup=reply_markup) async def edit_media(self, media: InputMedia, - reply_markup: typing.Union[InlineKeyboardMarkup] = None): + reply_markup: typing.Union[InlineKeyboardMarkup, + None] = None) -> typing.Union[Message, base.Boolean]: """ Use this method to edit audio, document, photo, or video messages. If a message is a part of a message album, then it can be edited only to a photo or a video. @@ -1613,7 +1616,9 @@ class Message(base.TelegramObject): return await self.bot.edit_message_media(media=media, chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) - async def edit_reply_markup(self, reply_markup: typing.Union[InlineKeyboardMarkup] = None): + async def edit_reply_markup(self, + reply_markup: typing.Union[InlineKeyboardMarkup, + None] = None) -> typing.Union[Message, base.Boolean]: """ Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). @@ -1640,7 +1645,8 @@ class Message(base.TelegramObject): async def edit_live_location(self, latitude: base.Float, longitude: base.Float, - reply_markup: typing.Union[InlineKeyboardMarkup] = None): + reply_markup: typing.Union[InlineKeyboardMarkup, + None] = None) -> typing.Union[Message, base.Boolean]: """ Use this method to edit live location messages sent by the bot or via the bot (for inline bots). A location can be edited until its live_period expires or editing is explicitly disabled by a call @@ -1662,7 +1668,9 @@ class Message(base.TelegramObject): chat_id=self.chat.id, message_id=self.message_id, reply_markup=reply_markup) - async def stop_live_location(self, reply_markup: typing.Union[InlineKeyboardMarkup] = None): + async def stop_live_location(self, + reply_markup: typing.Union[InlineKeyboardMarkup, + None] = None) -> typing.Union[Message, base.Boolean]: """ Use this method to stop updating a live location message sent by the bot or via the bot (for inline bots) before live_period expires. @@ -1695,7 +1703,7 @@ 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): + async def pin(self, disable_notification: typing.Union[base.Boolean, None] = None) -> base.Boolean: """ Use this method to pin a message in a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. From 3025d981be08a089526de80768e463246007efa0 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Fri, 3 May 2019 15:45:08 +0500 Subject: [PATCH 53/85] Implements opportunity to create temporary instance of bot with different tokens --- aiogram/bot/base.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 4f55dbd7..644390fe 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -21,6 +21,7 @@ class BaseBot: Base class for bot. It's raw bot. """ _ctx_timeout = ContextVar('TelegramRequestTimeout') + _ctx_token = ContextVar('BotDifferentToken') def __init__( self, @@ -57,6 +58,7 @@ class BaseBot: # Authentication if validate_token: api.check_token(token) + self._token = None self.__token = token self.proxy = proxy @@ -135,6 +137,24 @@ class BaseBot: finally: self._ctx_timeout.reset(token) + @property + def __token(self): + return self._ctx_token.get(self._token) + + @__token.setter + def __token(self, value): + self._token = value + + @contextlib.contextmanager + def with_token(self, bot_token: base.String, validate_token: Optional[base.Boolean] = True): + if validate_token is True: + api.check_token(bot_token) + token = self._ctx_token.set(bot_token) + try: + yield + finally: + self._ctx_token.reset(token) + async def close(self): """ Close all client sessions From 5580cdaa06b99b6213485e36909711c22095f7f4 Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Fri, 3 May 2019 16:08:23 +0500 Subject: [PATCH 54/85] Update base.py --- aiogram/bot/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 4f55dbd7..4f3b7d79 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -31,7 +31,7 @@ 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]] = None + timeout: typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]] = None ): """ Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot @@ -51,7 +51,7 @@ class BaseBot: :param parse_mode: You can set default parse mode :type parse_mode: :obj:`str` :param timeout: Request timeout - :type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float]]` + :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` """ # Authentication @@ -121,7 +121,7 @@ class BaseBot: self.timeout = None @contextlib.contextmanager - def request_timeout(self, timeout): + def request_timeout(self, timeout: typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]): """ Context manager implements opportunity to change request timeout in current context From 49157af59b1ef2982e6892301bd00d691151256c Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Fri, 3 May 2019 16:09:55 +0500 Subject: [PATCH 55/85] Update base.py --- aiogram/bot/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 4f3b7d79..d1d36f36 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -125,7 +125,8 @@ class BaseBot: """ Context manager implements opportunity to change request timeout in current context - :param timeout: + :param timeout: Request timeout + :type timeout: :obj:`typing.Optional[typing.Union[base.Integer, base.Float, aiohttp.ClientTimeout]]` :return: """ timeout = self._prepare_timeout(timeout) From 785aaf3264342ab82f7bff2911a96ecc88c805ee Mon Sep 17 00:00:00 2001 From: Slavik Voronov Date: Sun, 5 May 2019 17:45:29 +0300 Subject: [PATCH 56/85] Fix FileStorage data reading --- aiogram/contrib/fsm_storage/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/fsm_storage/files.py b/aiogram/contrib/fsm_storage/files.py index 88960375..f67a6f69 100644 --- a/aiogram/contrib/fsm_storage/files.py +++ b/aiogram/contrib/fsm_storage/files.py @@ -15,7 +15,7 @@ class _FileStorage(MemoryStorage): path = self.path = pathlib.Path(path) try: - self._data = self.read(path) + self.data = self.read(path) except FileNotFoundError: pass From 9ef3a630d15a8ee0cd2969406b1cc8365be2b01c Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 12 May 2019 11:35:45 +0500 Subject: [PATCH 57/85] Update base.py --- aiogram/bot/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 644390fe..961f213d 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -147,7 +147,7 @@ class BaseBot: @contextlib.contextmanager def with_token(self, bot_token: base.String, validate_token: Optional[base.Boolean] = True): - if validate_token is True: + if validate_token: api.check_token(bot_token) token = self._ctx_token.set(bot_token) try: From 1e1becd88e2d5954590a02f8a1aeb1464af4cdad Mon Sep 17 00:00:00 2001 From: Nikita <43146729+gabbhack@users.noreply.github.com> Date: Sun, 12 May 2019 11:40:44 +0500 Subject: [PATCH 58/85] Make check_result func sync --- aiogram/bot/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 051ae37f..60dbc36a 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -34,7 +34,7 @@ def check_token(token: str) -> bool: return True -async def check_result(method_name: str, content_type: str, status_code: int, body: str): +def check_result(method_name: str, content_type: str, status_code: int, body: str): """ Checks whether `result` is a valid API response. A result is considered invalid if: @@ -95,7 +95,7 @@ async def make_request(session, token, method, data=None, files=None, **kwargs): req = compose_data(data, files) try: async with session.post(url, data=req, **kwargs) as response: - return await check_result(method, response.content_type, response.status, await response.text()) + return check_result(method, response.content_type, response.status, await response.text()) except aiohttp.ClientError as e: raise exceptions.NetworkError(f"aiohttp client throws an error: {e.__class__.__name__}: {e}") From 9c43c7fdde7526ba7f3a0759fd129bd254f51634 Mon Sep 17 00:00:00 2001 From: Ramzan Bekbulatov Date: Sun, 19 May 2019 02:04:35 +0300 Subject: [PATCH 59/85] fix: typo: data -> bucket in update_bucket --- aiogram/contrib/fsm_storage/redis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 2bfe1642..c3a91f00 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -336,9 +336,9 @@ class RedisStorage2(BaseStorage): bucket: typing.Dict = None, **kwargs): if bucket is None: bucket = {} - temp_bucket = await self.get_data(chat=chat, user=user) + temp_bucket = await self.get_bucket(chat=chat, user=user) temp_bucket.update(bucket, **kwargs) - await self.set_data(chat=chat, user=user, data=temp_bucket) + await self.set_bucket(chat=chat, user=user, data=temp_bucket) async def reset_all(self, full=True): """ From 13e64d63947f2bd8723992c303fdd2415b0e69b9 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 24 May 2019 08:46:31 +0200 Subject: [PATCH 60/85] Create FUNDING.yml --- .github/FUNDING.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ab12d702 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: [JRootJunior]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: aiogram # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL From 71466c49603b3b9de9b48b7553d93b18ea6ec76f Mon Sep 17 00:00:00 2001 From: 0623forbidden <0623forbidden@gmail.com> Date: Fri, 24 May 2019 17:54:48 +0700 Subject: [PATCH 61/85] Add get_url method for Downloadable --- aiogram/bot/base.py | 5 ++++- aiogram/types/mixins.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index d1d36f36..b8425f9b 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -185,7 +185,7 @@ class BaseBot: if destination is None: destination = io.BytesIO() - url = api.Methods.file_url(token=self.__token, path=file_path) + url = self.get_file_url(file_path) dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb') async with self.session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response: @@ -199,6 +199,9 @@ class BaseBot: dest.seek(0) return dest + def get_file_url(self, file_path): + return api.Methods.file_url(token=self.__token, path=file_path) + async def send_file(self, file_type, method, file, payload) -> Union[Dict, base.Boolean]: """ Send file diff --git a/aiogram/types/mixins.py b/aiogram/types/mixins.py index 396633ad..f11a1760 100644 --- a/aiogram/types/mixins.py +++ b/aiogram/types/mixins.py @@ -45,5 +45,18 @@ class Downloadable: else: return await self.bot.get_file(self.file_id) + async def get_url(self): + """ + Get file url. + + Attention!! + This method has security vulnerabilities for the reason that result + contains bot's *access token* in open form. Use at your own risk! + + :return: url + """ + file = await self.get_file() + return self.bot.get_file_url(file.file_path) + def __hash__(self): return hash(self.file_id) From b70778fcdb4b6cc1b77dc7f399611a27b6a10141 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sat, 1 Jun 2019 15:16:28 +0300 Subject: [PATCH 62/85] Update to 4.3 Bot API. LoginUrl. --- README.md | 1 + README.rst | 4 ++++ aiogram/__init__.py | 4 ++-- aiogram/bot/api.py | 2 +- aiogram/types/__init__.py | 2 ++ aiogram/types/inline_keyboard.py | 31 ++++++++++++++++++++++++------- aiogram/types/login_url.py | 30 ++++++++++++++++++++++++++++++ aiogram/types/message.py | 3 ++- aiogram/utils/exceptions.py | 5 +++++ docs/source/index.rst | 14 +++++++++++--- 10 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 aiogram/types/login_url.py diff --git a/README.md b/README.md index 639a6ff9..9f977023 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![PyPi status](https://img.shields.io/pypi/status/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Downloads](https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) [![Supported python versions](https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square)](https://pypi.python.org/pypi/aiogram) +[![Telegram Bot API](https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram)](https://core.telegram.org/bots/api) [![Documentation Status](https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square)](http://aiogram.readthedocs.io/en/latest/?badge=latest) [![Github issues](https://img.shields.io/github/issues/aiogram/aiogram.svg?style=flat-square)](https://github.com/aiogram/aiogram/issues) [![MIT License](https://img.shields.io/pypi/l/aiogram.svg?style=flat-square)](https://opensource.org/licenses/MIT) diff --git a/README.rst b/README.rst index fde5bc7f..0377aad9 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,10 @@ AIOGramBot :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram + :target: https://core.telegram.org/bots/api + :alt: Telegram Bot API + .. image:: https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square :target: http://aiogram.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status diff --git a/aiogram/__init__.py b/aiogram/__init__.py index b5e647c9..6fe7d1c6 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.1.1.dev1' -__api_version__ = '4.2' +__version__ = '2.2.dev1' +__api_version__ = '4.3' diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 60dbc36a..6c51b295 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -147,7 +147,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 4.2 + List is updated to Bot API 4.3 """ mode = HelperMode.lowerCamelCase diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index e509ee74..5395e486 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -33,6 +33,7 @@ from .input_message_content import InputContactMessageContent, InputLocationMess from .invoice import Invoice from .labeled_price import LabeledPrice from .location import Location +from .login_url import LoginUrl from .mask_position import MaskPosition from .message import ContentType, ContentTypes, Message, ParseMode from .message_entity import MessageEntity, MessageEntityType @@ -126,6 +127,7 @@ __all__ = ( 'KeyboardButton', 'LabeledPrice', 'Location', + 'LoginUrl', 'MaskPosition', 'MediaGroup', 'Message', diff --git a/aiogram/types/inline_keyboard.py b/aiogram/types/inline_keyboard.py index 69049345..97ad35da 100644 --- a/aiogram/types/inline_keyboard.py +++ b/aiogram/types/inline_keyboard.py @@ -3,6 +3,7 @@ import typing from . import base from . import fields from .callback_game import CallbackGame +from .login_url import LoginUrl class InlineKeyboardMarkup(base.TelegramObject): @@ -16,10 +17,16 @@ class InlineKeyboardMarkup(base.TelegramObject): """ inline_keyboard: 'typing.List[typing.List[InlineKeyboardButton]]' = fields.ListOfLists(base='InlineKeyboardButton') - def __init__(self, row_width=3, inline_keyboard=None): + def __init__(self, row_width=3, inline_keyboard=None, **kwargs): if inline_keyboard is None: inline_keyboard = [] - super(InlineKeyboardMarkup, self).__init__(conf={'row_width': row_width}, inline_keyboard=inline_keyboard) + + conf = kwargs.pop('conf', {}) or {} + conf['row_width'] = row_width + + super(InlineKeyboardMarkup, self).__init__(**kwargs, + conf=conf, + inline_keyboard=inline_keyboard) @property def row_width(self): @@ -84,16 +91,26 @@ class InlineKeyboardButton(base.TelegramObject): """ text: base.String = fields.Field() url: base.String = fields.Field() + login_url: LoginUrl = fields.Field(base=LoginUrl) callback_data: base.String = fields.Field() switch_inline_query: base.String = fields.Field() switch_inline_query_current_chat: base.String = fields.Field() callback_game: CallbackGame = fields.Field(base=CallbackGame) pay: base.Boolean = fields.Field() - def __init__(self, text: base.String, url: base.String = None, callback_data: base.String = None, - switch_inline_query: base.String = None, switch_inline_query_current_chat: base.String = None, - callback_game: CallbackGame = None, pay: base.Boolean = None): - super(InlineKeyboardButton, self).__init__(text=text, url=url, callback_data=callback_data, + def __init__(self, text: base.String, + url: base.String = None, + login_url: LoginUrl = None, + callback_data: base.String = None, + switch_inline_query: base.String = None, + switch_inline_query_current_chat: base.String = None, + callback_game: CallbackGame = None, + pay: base.Boolean = None, **kwargs): + super(InlineKeyboardButton, self).__init__(text=text, + url=url, + login_url=login_url, + callback_data=callback_data, switch_inline_query=switch_inline_query, switch_inline_query_current_chat=switch_inline_query_current_chat, - callback_game=callback_game, pay=pay) + callback_game=callback_game, + pay=pay, **kwargs) diff --git a/aiogram/types/login_url.py b/aiogram/types/login_url.py new file mode 100644 index 00000000..c0dd6133 --- /dev/null +++ b/aiogram/types/login_url.py @@ -0,0 +1,30 @@ +from . import base +from . import fields + + +class LoginUrl(base.TelegramObject): + """ + This object represents a parameter of the inline keyboard button used to automatically authorize a user. + Serves as a great replacement for the Telegram Login Widget when the user is coming from Telegram. + All the user needs to do is tap/click a button and confirm that they want to log in. + + https://core.telegram.org/bots/api#loginurl + """ + url: base.String = fields.Field() + forward_text: base.String = fields.Field() + bot_username: base.String = fields.Field() + request_write_access: base.Boolean = fields.Field() + + def __init__(self, + url: base.String, + forward_text: base.String = None, + bot_username: base.String = None, + request_write_access: base.Boolean = None, + **kwargs): + super(LoginUrl, self).__init__( + url=url, + forward_text=forward_text, + bot_username=bot_username, + request_write_access=request_write_access, + **kwargs + ) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index cc392749..2c1f31c8 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -14,7 +14,7 @@ from .contact import Contact from .document import Document from .force_reply import ForceReply from .game import Game -from .inline_keyboard import InlineKeyboardMarkup +from .inline_keyboard import InlineKeyboardMarkup, InlineKeyboardButton from .input_media import MediaGroup, InputMedia from .invoice import Invoice from .location import Location @@ -87,6 +87,7 @@ class Message(base.TelegramObject): connected_website: base.String = fields.Field() passport_data: PassportData = fields.Field(base=PassportData) poll: Poll = fields.Field(base=Poll) + reply_markup: typing.List[typing.List[InlineKeyboardButton]] = fields.ListOfLists(base=InlineKeyboardButton) @property @functools.lru_cache() diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index b817e4cc..45277206 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -451,6 +451,11 @@ class ResultIdDuplicate(BadRequest): text = 'Result ID duplicate' +class BotDomainInvalid(BadRequest): + match = 'bot_domain_invalid' + text = 'Invalid bot domain' + + class NotFound(TelegramAPIError, _MatchErrorMixin): __group = True diff --git a/docs/source/index.rst b/docs/source/index.rst index d9ad1ca4..89cdbf79 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -2,6 +2,10 @@ Welcome to aiogram's documentation! =================================== + .. image:: https://img.shields.io/badge/telegram-aiogram-blue.svg?style=flat-square + :target: https://t.me/aiogram_live + :alt: [Telegram] aiogram live + .. image:: https://img.shields.io/pypi/v/aiogram.svg?style=flat-square :target: https://pypi.python.org/pypi/aiogram :alt: PyPi Package Version @@ -10,13 +14,17 @@ Welcome to aiogram's documentation! :target: https://pypi.python.org/pypi/aiogram :alt: PyPi status + .. image:: https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square + :target: https://pypi.python.org/pypi/aiogram + :alt: PyPi downloads + .. image:: https://img.shields.io/pypi/pyversions/aiogram.svg?style=flat-square :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions - .. image:: https://img.shields.io/pypi/dm/aiogram.svg?style=flat-square - :target: https://pypi.python.org/pypi/aiogram - :alt: PyPi downloads + .. image:: https://img.shields.io/badge/Telegram%20Bot%20API-4.3-blue.svg?style=flat-square&logo=telegram + :target: https://core.telegram.org/bots/api + :alt: Telegram Bot API .. image:: https://img.shields.io/readthedocs/pip/stable.svg?style=flat-square :target: http://aiogram.readthedocs.io/en/latest/?badge=latest From 542198248289500e7d2b1eaf2ba6cafc28a4852f Mon Sep 17 00:00:00 2001 From: Ilya Samartsev Date: Sun, 2 Jun 2019 14:39:17 +0300 Subject: [PATCH 63/85] update exception matcher and fix tests --- aiogram/utils/exceptions.py | 11 +++++++++-- dev_requirements.txt | 2 +- tests/test_bot.py | 28 +++++++++++++++++++++++++--- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 45277206..afd623cc 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -12,6 +12,7 @@ TelegramAPIError MessageCantBeEdited MessageCantBeDeleted MessageToEditNotFound + MessageToReplyNotFound ToMuchMessages PollError PollCantBeStopped @@ -182,6 +183,13 @@ class MessageToDeleteNotFound(MessageError): match = 'message to delete not found' +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' + + class MessageIdentifierNotSpecified(MessageError): match = 'message identifier is not specified' @@ -297,8 +305,7 @@ class ChatDescriptionIsNotModified(BadRequest): class InvalidQueryID(BadRequest): - match = 'QUERY_ID_INVALID' - text = 'Invalid query ID' + match = 'query is too old and response timeout expired or query id is invalid' class InvalidPeerID(BadRequest): diff --git a/dev_requirements.txt b/dev_requirements.txt index 08363189..79adc949 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -3,7 +3,7 @@ ujson>=1.35 python-rapidjson>=0.7.0 emoji>=0.5.2 -pytest>=4.4.1 +pytest>=4.4.1,<4.6 pytest-asyncio>=0.10.0 tox>=3.9.0 aresponses>=1.1.1 diff --git a/tests/test_bot.py b/tests/test_bot.py index c3a29687..448f8dda 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -185,7 +185,7 @@ async def test_send_location(bot: Bot, event_loop): @pytest.mark.asyncio -async def test_edit_message_live_location(bot: Bot, event_loop): +async def test_edit_message_live_location_by_bot(bot: Bot, event_loop): """ editMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) @@ -197,6 +197,14 @@ async def test_edit_message_live_location(bot: Bot, event_loop): latitude=location.latitude, longitude=location.longitude) assert result == msg + +@pytest.mark.asyncio +async def test_edit_message_live_location_by_user(bot: Bot, event_loop): + """ editMessageLiveLocation method test """ + from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION + msg = types.Message(**MESSAGE_WITH_LOCATION) + location = types.Location(**LOCATION) + # editing user's message async with FakeTelegram(message_dict=True, loop=event_loop): result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id, @@ -205,7 +213,7 @@ async def test_edit_message_live_location(bot: Bot, event_loop): @pytest.mark.asyncio -async def test_stop_message_live_location(bot: Bot, event_loop): +async def test_stop_message_live_location_by_bot(bot: Bot, event_loop): """ stopMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) @@ -215,6 +223,13 @@ async def test_stop_message_live_location(bot: Bot, event_loop): result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id) assert result == msg + +@pytest.mark.asyncio +async def test_stop_message_live_location_by_user(bot: Bot, event_loop): + """ stopMessageLiveLocation method test """ + from .types.dataset import MESSAGE_WITH_LOCATION + msg = types.Message(**MESSAGE_WITH_LOCATION) + # stopping user's message async with FakeTelegram(message_dict=True, loop=event_loop): result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id) @@ -509,7 +524,7 @@ async def test_answer_callback_query(bot: Bot, event_loop): @pytest.mark.asyncio -async def test_edit_message_text(bot: Bot, event_loop): +async def test_edit_message_text_by_bot(bot: Bot, event_loop): """ editMessageText method test """ from .types.dataset import EDITED_MESSAGE msg = types.Message(**EDITED_MESSAGE) @@ -519,6 +534,13 @@ async def test_edit_message_text(bot: Bot, event_loop): result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id) assert result == msg + +@pytest.mark.asyncio +async def test_edit_message_text_by_user(bot: Bot, event_loop): + """ editMessageText method test """ + from .types.dataset import EDITED_MESSAGE + msg = types.Message(**EDITED_MESSAGE) + # message by user async with FakeTelegram(message_dict=True, loop=event_loop): result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id) From 7aa6dc4efe5f1fb83e7a3efbe0a98bd3eab95213 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 9 Jun 2019 21:49:56 +0300 Subject: [PATCH 64/85] Fix Message.reply_markup --- 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 2c1f31c8..a8e65a32 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -87,7 +87,7 @@ class Message(base.TelegramObject): connected_website: base.String = fields.Field() passport_data: PassportData = fields.Field(base=PassportData) poll: Poll = fields.Field(base=Poll) - reply_markup: typing.List[typing.List[InlineKeyboardButton]] = fields.ListOfLists(base=InlineKeyboardButton) + reply_markup: InlineKeyboardMarkup = fields.Field(base=InlineKeyboardMarkup) @property @functools.lru_cache() From 35b0e150c2bff884e83e5d0138caf51fa43825ab Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 9 Jun 2019 21:54:53 +0300 Subject: [PATCH 65/85] Change `deprecated until` versions --- aiogram/types/chat_member.py | 6 +++--- aiogram/types/message.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index 6679c5e0..12789462 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -33,7 +33,7 @@ class ChatMember(base.TelegramObject): def is_admin(self): warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.2', + 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', DeprecationWarning, stacklevel=2) return self.is_chat_admin() @@ -63,14 +63,14 @@ class ChatMemberStatus(helper.Helper): @classmethod def is_admin(cls, role): warnings.warn('`is_admin` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_admin` and will be available until aiogram 2.2', + 'This method renamed to `is_chat_admin` and will be available until aiogram 2.3', DeprecationWarning, stacklevel=2) return cls.is_chat_admin(role) @classmethod def is_member(cls, role): warnings.warn('`is_member` method deprecated due to updates in Bot API 4.2. ' - 'This method renamed to `is_chat_member` and will be available until aiogram 2.2', + 'This method renamed to `is_chat_member` and will be available until aiogram 2.3', DeprecationWarning, stacklevel=2) return cls.is_chat_member(role) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index a8e65a32..cafb8116 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -1008,7 +1008,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned :rtype: :obj:`types.Message` """ - warn_deprecated('"Message.send_animation" method will be removed in 2.2 version.\n' + warn_deprecated('"Message.send_animation" method will be removed in 2.3 version.\n' 'Use "Message.reply_animation" instead.', stacklevel=8) @@ -1358,7 +1358,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - warn_deprecated('"Message.send_venue" method will be removed in 2.2 version.\n' + warn_deprecated('"Message.send_venue" method will be removed in 2.3 version.\n' 'Use "Message.reply_venue" instead.', stacklevel=8) @@ -1446,7 +1446,7 @@ class Message(base.TelegramObject): :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ - warn_deprecated('"Message.send_contact" method will be removed in 2.2 version.\n' + warn_deprecated('"Message.send_contact" method will be removed in 2.3 version.\n' 'Use "Message.reply_contact" instead.', stacklevel=8) From 2083bfb965770fb47f041c4d8b93fba802e4ff45 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 9 Jun 2019 21:55:06 +0300 Subject: [PATCH 66/85] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 6fe7d1c6..c2dd8f24 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.2.dev1' +__version__ = '2.2' __api_version__ = '4.3' From 9b9c1b086da0d6d033247261f55df7d934d96c5d Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Sun, 9 Jun 2019 21:55:41 +0300 Subject: [PATCH 67/85] Remove unused import --- aiogram/types/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index cafb8116..7637cf42 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -14,7 +14,7 @@ from .contact import Contact from .document import Document from .force_reply import ForceReply from .game import Game -from .inline_keyboard import InlineKeyboardMarkup, InlineKeyboardButton +from .inline_keyboard import InlineKeyboardMarkup from .input_media import MediaGroup, InputMedia from .invoice import Invoice from .location import Location From 5b9d82f1ca5e0214bc43a75442bf4da4d358ffc0 Mon Sep 17 00:00:00 2001 From: Alex RootJunior Date: Mon, 10 Jun 2019 20:58:02 +0300 Subject: [PATCH 68/85] Bump dev version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index c2dd8f24..a1c2736b 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -38,5 +38,5 @@ __all__ = [ 'utils' ] -__version__ = '2.2' +__version__ = '2.2.1.dev1' __api_version__ = '4.3' From 550c41e1752aa08c493d7cb4ec5fec402d8e849c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 11 Jun 2019 15:09:39 +0300 Subject: [PATCH 69/85] Update FUNDING.yml Cleanup --- .github/FUNDING.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ab12d702..82ea7257 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,8 +1,2 @@ -# These are supported funding model platforms - -github: [JRootJunior]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: aiogram # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -custom: # Replace with a single custom sponsorship URL +github: [JRootJunior] +open_collective: aiogram From 61d5c3a41a696673ddd940a0c0eb9f46fcb60cad Mon Sep 17 00:00:00 2001 From: "bohdan.lushchyk" Date: Fri, 21 Jun 2019 15:06:36 +0300 Subject: [PATCH 70/85] data to filter from middlwares --- aiogram/dispatcher/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 17b715d1..859cb47e 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -105,7 +105,7 @@ class Handler: try: for handler_obj in self.handlers: try: - data.update(await check_filters(handler_obj.filters, args)) + data.update(await check_filters(handler_obj.filters, args + (data,))) except FilterNotPassed: continue else: From 0b125014980a2c0da1d767c212581d6416f271ba Mon Sep 17 00:00:00 2001 From: Kylmakalle Date: Wed, 26 Jun 2019 00:04:30 +0300 Subject: [PATCH 71/85] Update i18n example --- examples/i18n_example.py | 28 ++++++++++++++++++++++++ examples/locales/mybot.pot | 4 ++++ examples/locales/ru/LC_MESSAGES/mybot.po | 5 +++++ 3 files changed, 37 insertions(+) diff --git a/examples/i18n_example.py b/examples/i18n_example.py index 6469ed5b..bf23c8d1 100644 --- a/examples/i18n_example.py +++ b/examples/i18n_example.py @@ -3,6 +3,19 @@ Internalize your bot Step 1: extract texts # pybabel extract i18n_example.py -o locales/mybot.pot + + Some useful options: + - Extract texts with pluralization support + # -k __:1,2 + - Add comments for translators, you can use another tag if you want (TR) + # --add-comments=NOTE + - Disable comments with string location in code + # --no-location + - Set project name + # --project=MySuperBot + - Set version + # --version=2.2 + Step 2: create *.po files. For e.g. create en, ru, uk locales. # echo {en,ru,uk} | xargs -n1 pybabel init -i locales/mybot.pot -d locales -D mybot -l Step 3: translate texts @@ -51,6 +64,21 @@ async def cmd_start(message: types.Message): async def cmd_lang(message: types.Message, locale): await message.reply(_('Your current language: {language}').format(language=locale)) +# If you care about pluralization, here's small handler +# And also, there's and example of comments for translators. Most translation tools support them. + +# Alias for gettext method, parser will understand double underscore as plural (aka ngettext) +__ = i18n.gettext + +# Some pseudo numeric value +TOTAL_LIKES = 0 + +@dp.message_handler(commands=['like']) +async def cmd_like(message: types.Message, locale): + TOTAL_LIKES += 1 + + # NOTE: This is comment for a translator + await message.reply(__('Aiogram has {number} like!', 'Aiogram has {number} likes!', TOTAL_LIKES).format(number=TOTAL_LIKES)) if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) diff --git a/examples/locales/mybot.pot b/examples/locales/mybot.pot index 988ed463..b0736569 100644 --- a/examples/locales/mybot.pot +++ b/examples/locales/mybot.pot @@ -25,3 +25,7 @@ msgstr "" msgid "Your current language: {language}" msgstr "" +msgid "Aiogram has {number} like!" +msgid_plural "Aiogram has {number} likes!" +msgstr[0] "" +msgstr[1] "" diff --git a/examples/locales/ru/LC_MESSAGES/mybot.po b/examples/locales/ru/LC_MESSAGES/mybot.po index 73876f30..8180af42 100644 --- a/examples/locales/ru/LC_MESSAGES/mybot.po +++ b/examples/locales/ru/LC_MESSAGES/mybot.po @@ -27,3 +27,8 @@ msgstr "Привет, {user}!" msgid "Your current language: {language}" msgstr "Твой язык: {language}" +msgid "Aiogram has {number} like!" +msgid_plural "Aiogram has {number} likes!" +msgstr[0] "Aiogram имеет {number} лайк!" +msgstr[1] "Aiogram имеет {number} лайка!" +msgstr[2] "Aiogram имеет {number} лайков!" \ No newline at end of file From cf95a9080c886352548c55b62f552f66ade0075a Mon Sep 17 00:00:00 2001 From: Ilya Makarov Date: Wed, 26 Jun 2019 22:28:39 +0300 Subject: [PATCH 72/85] Fix #143: Imposible to download file into directory --- aiogram/types/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/mixins.py b/aiogram/types/mixins.py index f11a1760..13f8412f 100644 --- a/aiogram/types/mixins.py +++ b/aiogram/types/mixins.py @@ -24,7 +24,7 @@ class Downloadable: if destination is None: destination = file.file_path elif isinstance(destination, (str, pathlib.Path)) and os.path.isdir(destination): - os.path.join(destination, file.file_path) + destination = os.path.join(destination, file.file_path) else: is_path = False From c6accd1a53d43e4cd63c105a370a4830bd168b4a Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 19:34:38 +0500 Subject: [PATCH 73/85] Add "expire" argument Add "expire" argument to all set_ and update_ methods in RedisStorage2 --- aiogram/contrib/fsm_storage/redis.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index c3a91f00..6b96869b 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -287,29 +287,29 @@ class RedisStorage2(BaseStorage): return default or {} async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - state: typing.Optional[typing.AnyStr] = None): + state: typing.Optional[typing.AnyStr] = None, expire: int = 0): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) redis = await self.redis() if state is None: await redis.delete(key) else: - await redis.set(key, state) + await redis.set(key, state, expire=expire) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None): + data: typing.Dict = None, expire: int = 0): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_DATA_KEY) redis = await self.redis() - await redis.set(key, json.dumps(data)) + await redis.set(key, json.dumps(data), expire=expire) async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None, **kwargs): + data: typing.Dict = None, expire: int = 0, **kwargs): if data is None: data = {} temp_data = await self.get_data(chat=chat, user=user, default={}) temp_data.update(data, **kwargs) - await self.set_data(chat=chat, user=user, data=temp_data) + await self.set_data(chat=chat, user=user, data=temp_data, expire=expire) def has_bucket(self): return True @@ -325,20 +325,20 @@ class RedisStorage2(BaseStorage): return default or {} async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None): + bucket: typing.Dict = None, expire: int = 0): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_BUCKET_KEY) redis = await self.redis() - await redis.set(key, json.dumps(bucket)) + await redis.set(key, json.dumps(bucket), expire=expire) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, **kwargs): + bucket: typing.Dict = None, expire: int = 0, **kwargs): if bucket is None: bucket = {} temp_bucket = await self.get_bucket(chat=chat, user=user) temp_bucket.update(bucket, **kwargs) - await self.set_bucket(chat=chat, user=user, data=temp_bucket) + await self.set_bucket(chat=chat, user=user, data=temp_bucket, expire=expire) async def reset_all(self, full=True): """ From e2842944fa1e051648341b07cb920489f4f55e29 Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 20:39:26 +0500 Subject: [PATCH 74/85] Update redis.py Update docstrings --- aiogram/contrib/fsm_storage/redis.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 6b96869b..e75e7f40 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -62,8 +62,6 @@ class RedisStorage(BaseStorage): async def redis(self) -> aioredis.RedisConnection: """ Get Redis connection - - This property is awaitable. """ # Use thread-safe asyncio Lock because this method without that is not safe async with self._connection_lock: @@ -241,8 +239,6 @@ class RedisStorage2(BaseStorage): async def redis(self) -> aioredis.Redis: """ Get Redis connection - - This property is awaitable. """ # Use thread-safe asyncio Lock because this method without that is not safe async with self._connection_lock: From 8a819eb7ed0758f7342e234c1b10f6c9282e72f1 Mon Sep 17 00:00:00 2001 From: Gabben Date: Thu, 27 Jun 2019 21:00:25 +0500 Subject: [PATCH 75/85] Fix update_bucket --- aiogram/contrib/fsm_storage/redis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index e75e7f40..0e89eaea 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -334,7 +334,7 @@ class RedisStorage2(BaseStorage): bucket = {} temp_bucket = await self.get_bucket(chat=chat, user=user) temp_bucket.update(bucket, **kwargs) - await self.set_bucket(chat=chat, user=user, data=temp_bucket, expire=expire) + await self.set_bucket(chat=chat, user=user, bucket=temp_bucket, expire=expire) async def reset_all(self, full=True): """ From 607e3ea13514942bc10192a81604e00404f209c8 Mon Sep 17 00:00:00 2001 From: Gabben Date: Fri, 28 Jun 2019 15:26:07 +0500 Subject: [PATCH 76/85] Update redis.py --- aiogram/contrib/fsm_storage/redis.py | 34 ++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index 0e89eaea..106a7b97 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -35,7 +35,6 @@ class RedisStorage(BaseStorage): await dp.storage.wait_closed() """ - def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, loop=None, **kwargs): self._host = host self._port = port @@ -220,9 +219,12 @@ class RedisStorage2(BaseStorage): await dp.storage.wait_closed() """ - - def __init__(self, host='localhost', port=6379, db=None, password=None, ssl=None, - pool_size=10, loop=None, prefix='fsm', **kwargs): + def __init__(self, host: str = 'localhost', port=6379, db=None, password=None, + ssl=None, pool_size=10, loop=None, prefix='fsm', + state_ttl: int = 0, + data_ttl: int = 0, + bucket_ttl: int = 0, + **kwargs): self._host = host self._port = port self._db = db @@ -233,6 +235,10 @@ class RedisStorage2(BaseStorage): self._kwargs = kwargs self._prefix = (prefix,) + self._state_ttl = state_ttl + self._data_ttl = data_ttl + self._bucket_ttl = bucket_ttl + self._redis: aioredis.RedisConnection = None self._connection_lock = asyncio.Lock(loop=self._loop) @@ -283,29 +289,29 @@ class RedisStorage2(BaseStorage): return default or {} async def set_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - state: typing.Optional[typing.AnyStr] = None, expire: int = 0): + state: typing.Optional[typing.AnyStr] = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_KEY) redis = await self.redis() if state is None: await redis.delete(key) else: - await redis.set(key, state, expire=expire) + await redis.set(key, state, expire=self._state_ttl) async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None, expire: int = 0): + data: typing.Dict = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_DATA_KEY) redis = await self.redis() - await redis.set(key, json.dumps(data), expire=expire) + await redis.set(key, json.dumps(data), expire=self._data_ttl) async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - data: typing.Dict = None, expire: int = 0, **kwargs): + data: typing.Dict = None, **kwargs): if data is None: data = {} temp_data = await self.get_data(chat=chat, user=user, default={}) temp_data.update(data, **kwargs) - await self.set_data(chat=chat, user=user, data=temp_data, expire=expire) + await self.set_data(chat=chat, user=user, data=temp_data) def has_bucket(self): return True @@ -321,20 +327,20 @@ class RedisStorage2(BaseStorage): return default or {} async def set_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, expire: int = 0): + bucket: typing.Dict = None): chat, user = self.check_address(chat=chat, user=user) key = self.generate_key(chat, user, STATE_BUCKET_KEY) redis = await self.redis() - await redis.set(key, json.dumps(bucket), expire=expire) + await redis.set(key, json.dumps(bucket), expire=self._bucket_ttl) async def update_bucket(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, - bucket: typing.Dict = None, expire: int = 0, **kwargs): + bucket: typing.Dict = None, **kwargs): if bucket is None: bucket = {} temp_bucket = await self.get_bucket(chat=chat, user=user) temp_bucket.update(bucket, **kwargs) - await self.set_bucket(chat=chat, user=user, bucket=temp_bucket, expire=expire) + await self.set_bucket(chat=chat, user=user, bucket=temp_bucket) async def reset_all(self, full=True): """ From eae7dc5a2e09b1d12224e0d7112abf1c47c7306b Mon Sep 17 00:00:00 2001 From: SetazeR Date: Mon, 1 Jul 2019 09:19:31 +0700 Subject: [PATCH 77/85] unify finishing state machine interaction --- examples/finite_state_machine_example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/finite_state_machine_example.py b/examples/finite_state_machine_example.py index 58b8053c..90ab8aba 100644 --- a/examples/finite_state_machine_example.py +++ b/examples/finite_state_machine_example.py @@ -112,8 +112,8 @@ async def process_gender(message: types.Message, state: FSMContext): md.text('Gender:', data['gender']), sep='\n'), reply_markup=markup, parse_mode=ParseMode.MARKDOWN) - # Finish conversation - data.state = None + # Finish conversation + await state.finish() if __name__ == '__main__': From a339b7c5712c6dcab332430ef21dd7c137966101 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 2 Jul 2019 10:05:10 +0300 Subject: [PATCH 78/85] Revert "Send data from middlewares to filters " --- aiogram/dispatcher/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 859cb47e..17b715d1 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -105,7 +105,7 @@ class Handler: try: for handler_obj in self.handlers: try: - data.update(await check_filters(handler_obj.filters, args + (data,))) + data.update(await check_filters(handler_obj.filters, args)) except FilterNotPassed: continue else: From a1531d4e2064c6dd94f5f1d1325f828b07e67e9b Mon Sep 17 00:00:00 2001 From: Oleg A Date: Mon, 8 Jul 2019 23:39:24 +0300 Subject: [PATCH 79/85] logged callback sender id instead of message sender id --- aiogram/contrib/middlewares/logging.py | 33 ++++++++++---------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index 1a3566c6..d0f257d6 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -89,34 +89,27 @@ class LoggingMiddleware(BaseMiddleware): async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict): if callback_query.message: - if callback_query.message.from_user: - self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] " - f"from user [ID:{callback_query.message.from_user.id}]") - else: - self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + self.logger.info(f"Received callback query [ID:{callback_query.id}] " + f"from user [ID:{callback_query.from_user.id}] " + f"for message [ID:{callback_query.message.message_id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") else: self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"from inline message [ID:{callback_query.inline_message_id}] " - f"from user [ID:{callback_query.from_user.id}]") + f"from user [ID:{callback_query.from_user.id}] " + f"for inline message [ID:{callback_query.inline_message_id}] ") async def on_post_process_callback_query(self, callback_query, results, data: dict): if callback_query.message: - if callback_query.message.from_user: - self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " - f"callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}] " - f"from user [ID:{callback_query.message.from_user.id}]") - else: - self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " - f"callback query [ID:{callback_query.id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " + f"callback query [ID:{callback_query.id}] " + f"from user [ID:{callback_query.from_user.id}] " + f"for message [ID:{callback_query.message.message_id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") else: self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " f"callback query [ID:{callback_query.id}] " - f"from inline message [ID:{callback_query.inline_message_id}] " - f"from user [ID:{callback_query.from_user.id}]") + f"from user [ID:{callback_query.from_user.id}]" + f"from inline message [ID:{callback_query.inline_message_id}]") async def on_pre_process_shipping_query(self, shipping_query: types.ShippingQuery, data: dict): self.logger.info(f"Received shipping query [ID:{shipping_query.id}] " From 49e15545fa9035c3e69de2e6fd98a1cf04a6defc Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 9 Jul 2019 12:29:53 +0300 Subject: [PATCH 80/85] added `originally posted by` to callback --- aiogram/contrib/middlewares/logging.py | 30 ++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index d0f257d6..c5d61aac 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -89,10 +89,16 @@ class LoggingMiddleware(BaseMiddleware): async def on_pre_process_callback_query(self, callback_query: types.CallbackQuery, data: dict): if callback_query.message: - self.logger.info(f"Received callback query [ID:{callback_query.id}] " - f"from user [ID:{callback_query.from_user.id}] " - f"for message [ID:{callback_query.message.message_id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + text = (f"Received callback query [ID:{callback_query.id}] " + f"from user [ID:{callback_query.from_user.id}] " + f"for message [ID:{callback_query.message.message_id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + + if callback_query.message.from_user: + text += f" originally posted by [{callback_query.message.from_user.id}]" + + self.logger.info(text) + else: self.logger.info(f"Received callback query [ID:{callback_query.id}] " f"from user [ID:{callback_query.from_user.id}] " @@ -100,11 +106,17 @@ class LoggingMiddleware(BaseMiddleware): async def on_post_process_callback_query(self, callback_query, results, data: dict): if callback_query.message: - self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " - f"callback query [ID:{callback_query.id}] " - f"from user [ID:{callback_query.from_user.id}] " - f"for message [ID:{callback_query.message.message_id}] " - f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + text = (f"{HANDLED_STR[bool(len(results))]} " + f"callback query [ID:{callback_query.id}] " + f"from user [ID:{callback_query.from_user.id}] " + f"for message [ID:{callback_query.message.message_id}] " + f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") + + if callback_query.message.from_user: + text += f" originally posted by [{callback_query.message.from_user.id}]" + + self.logger.info(text) + else: self.logger.debug(f"{HANDLED_STR[bool(len(results))]} " f"callback query [ID:{callback_query.id}] " From 1dd462fcf3572ff56af3c2d694f8cbfe576f6c76 Mon Sep 17 00:00:00 2001 From: Oleg A Date: Tue, 9 Jul 2019 12:38:54 +0300 Subject: [PATCH 81/85] added `user` --- aiogram/contrib/middlewares/logging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/contrib/middlewares/logging.py b/aiogram/contrib/middlewares/logging.py index c5d61aac..9f389b60 100644 --- a/aiogram/contrib/middlewares/logging.py +++ b/aiogram/contrib/middlewares/logging.py @@ -95,7 +95,7 @@ class LoggingMiddleware(BaseMiddleware): f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") if callback_query.message.from_user: - text += f" originally posted by [{callback_query.message.from_user.id}]" + text += f" originally posted by user [ID:{callback_query.message.from_user.id}]" self.logger.info(text) @@ -113,7 +113,7 @@ class LoggingMiddleware(BaseMiddleware): f"in chat [{callback_query.message.chat.type}:{callback_query.message.chat.id}]") if callback_query.message.from_user: - text += f" originally posted by [{callback_query.message.from_user.id}]" + text += f" originally posted by user [ID:{callback_query.message.from_user.id}]" self.logger.info(text) From af889ad6b20a939c2380b76f6059628ff3449975 Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 10 Jul 2019 12:04:03 +0300 Subject: [PATCH 82/85] Add regular keyboard usage example --- examples/regular_keyboard_example.py | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 examples/regular_keyboard_example.py diff --git a/examples/regular_keyboard_example.py b/examples/regular_keyboard_example.py new file mode 100644 index 00000000..350e007e --- /dev/null +++ b/examples/regular_keyboard_example.py @@ -0,0 +1,61 @@ +""" +This bot is created for the demonstration of a usage of regular keyboards. +""" + +import logging + +from aiogram import Bot, Dispatcher, executor, types + +API_TOKEN = 'BOT_TOKEN_HERE' + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Initialize bot and dispatcher +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + + +@dp.message_handler(commands=['start']) +async def start_cmd_handler(message: types.Message): + keyboard_markup = types.ReplyKeyboardMarkup(row_width=3) + # default row_width is 3, so here we can omit it actually + # kept for clearness + + keyboard_markup.row(types.KeyboardButton("Yes!"), + types.KeyboardButton("No!")) + # adds buttons as a new row to the existing keyboard + # the behaviour doesn't depend on row_width attribute + + keyboard_markup.add(types.KeyboardButton("I don't know"), + types.KeyboardButton("Who am i?"), + types.KeyboardButton("Where am i?"), + types.KeyboardButton("Who is there?")) + # adds buttons. New rows is formed according to row_width parameter + + await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) + + +@dp.message_handler() +async def all_msg_handler(message: types.Message): + # pressing of a KeyboardButton is the same as sending the regular message with the same text + # so, to handle the responses from the keyboard, we need to use a message_handler + # in real bot, it's better to define message_handler(text="...") for each button + # but here for the simplicity only one handler is defined + + text_of_button = message.text + logger.debug(text_of_button) # print the text we got + + if text_of_button == 'Yes!': + await message.reply("That's great", reply_markup=types.ReplyKeyboardRemove()) + elif text_of_button == 'No!': + await message.reply("Oh no! Why?", reply_markup=types.ReplyKeyboardRemove()) + else: + await message.reply("Keep calm...Everything is fine", reply_markup=types.ReplyKeyboardRemove()) + # with message, we send types.ReplyKeyboardRemove() to hide the keyboard + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) From 31b92e1b95fb1aa2461f04f087dcddded3d3d7ea Mon Sep 17 00:00:00 2001 From: birdi Date: Wed, 10 Jul 2019 12:16:00 +0300 Subject: [PATCH 83/85] Add inline keyboard usage example --- examples/inline_keyboard_example.py | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 examples/inline_keyboard_example.py diff --git a/examples/inline_keyboard_example.py b/examples/inline_keyboard_example.py new file mode 100644 index 00000000..2478b9e0 --- /dev/null +++ b/examples/inline_keyboard_example.py @@ -0,0 +1,56 @@ +""" +This bot is created for the demonstration of a usage of inline keyboards. +""" + +import logging + +from aiogram import Bot, Dispatcher, executor, types + +API_TOKEN = 'BOT_TOKEN_HERE' + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +# Initialize bot and dispatcher +bot = Bot(token=API_TOKEN) +dp = Dispatcher(bot) + + +@dp.message_handler(commands=['start']) +async def start_cmd_handler(message: types.Message): + keyboard_markup = types.InlineKeyboardMarkup(row_width=3) + # default row_width is 3, so here we can omit it actually + # kept for clearness + + keyboard_markup.row(types.InlineKeyboardButton("Yes!", callback_data='yes'), + # in real life for the callback_data the callback data factory should be used + # here the raw string is used for the simplicity + types.InlineKeyboardButton("No!", callback_data='no')) + + keyboard_markup.add(types.InlineKeyboardButton("aiogram link", + url='https://github.com/aiogram/aiogram')) + # url buttons has no callback data + + await message.reply("Hi!\nDo you love aiogram?", reply_markup=keyboard_markup) + + +@dp.callback_query_handler(lambda cb: cb.data in ['yes', 'no']) # if cb.data is either 'yes' or 'no' +# @dp.callback_query_handler(text='yes') # if cb.data == 'yes' +async def inline_kb_answer_callback_handler(query: types.CallbackQuery): + await query.answer() # send answer to close the rounding circle + + answer_data = query.data + logger.debug(f"answer_data={answer_data}") + # here we can work with query.data + if answer_data == 'yes': + await bot.send_message(query.from_user.id, "That's great!") + elif answer_data == 'no': + await bot.send_message(query.from_user.id, "Oh no...Why so?") + else: + await bot.send_message(query.from_user.id, "Invalid callback data!") + + +if __name__ == '__main__': + executor.start_polling(dp, skip_updates=True) From 4a4eb5cff90f7ae4c44f5e09c186c90ae0f13798 Mon Sep 17 00:00:00 2001 From: AmirHossein Falahati Date: Sat, 13 Jul 2019 02:33:02 +0430 Subject: [PATCH 84/85] fix BotKicked --- aiogram/utils/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index afd623cc..a6612547 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -490,7 +490,7 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin): class BotKicked(Unauthorized): - match = 'Bot was kicked from a chat' + match = 'bot was kicked from a chat' class BotBlocked(Unauthorized): From 9d44ed1a460e5fdfc297a4b558a2917146fe3afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCller?= Date: Sat, 13 Jul 2019 10:43:53 +0200 Subject: [PATCH 85/85] Typo fix --- aiogram/dispatcher/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 4c06c2af..8e483255 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -77,7 +77,7 @@ class WebhookRequestHandler(web.View): .. code-block:: python3 - app.router.add_route('*', '/your/webhook/path', WebhookRequestHadler, name='webhook_handler') + app.router.add_route('*', '/your/webhook/path', WebhookRequestHandler, name='webhook_handler') But first you need to configure application for getting Dispatcher instance from request handler! It must always be with key 'BOT_DISPATCHER'