From 7ffeb8ff57a2e621cd04c6945bd8cba03b54d010 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 24 Aug 2017 01:11:48 +0300 Subject: [PATCH 01/20] Fix filename in InputFile and provide to change filename in send_document. --- aiogram/__init__.py | 2 +- aiogram/bot/base.py | 10 ++++++---- aiogram/bot/bot.py | 7 +++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index be669f7c..e2ad4091 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,6 +1,6 @@ from .bot import Bot from .utils.versions import Version, Stage -VERSION = Version(0, 4, 1, stage=Stage.DEV, build=0) +VERSION = Version(0, 4, 2, stage=Stage.DEV, build=0) __version__ = VERSION.version diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index a37e7243..3a300ed9 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -164,8 +164,6 @@ class BaseBot: # You can use file ID or URL in the most of requests payload[file_type] = file files = None - elif isinstance(file, (io.IOBase, io.FileIO)): - files = {file_type: file.read()} else: files = {file_type: file} @@ -435,7 +433,8 @@ class BaseBot: disable_notification: Optional[Boolean] = None, reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ - types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None) -> Dict: + types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None, + filename: Optional[str]=None) -> Dict: """ Use this method to send general files. On success, the sent Message is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. @@ -456,10 +455,13 @@ class BaseBot: :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + :param filename: Set file name :return: On success, the sent Message is returned. """ reply_markup = prepare_arg(reply_markup) - payload = generate_payload(**locals(), exclude=['document']) + if filename: + document = (filename, document) + payload = generate_payload(**locals(), exclude=['document', 'filename']) return await self.send_file('document', api.Methods.SEND_DOCUMENT, document, payload) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index b5fa8d12..3e4606c5 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -272,7 +272,8 @@ class Bot(BaseBot): reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, - types.ReplyKeyboardMarkup, Dict, String]] = None) -> types.Message: + types.ReplyKeyboardMarkup, Dict, String]] = None, + filename: Optional[str] = None) -> types.Message: """ Use this method to send general files. On success, the sent Message is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. @@ -293,12 +294,14 @@ class Bot(BaseBot): :param reply_markup: Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String] (Optional) - Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + :param filename: Set file name :return: On success, the sent Message is returned. (serialized) """ raw = super(Bot, self).send_document(chat_id=chat_id, document=document, caption=caption, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, - reply_markup=reply_markup) + reply_markup=reply_markup, + filename=filename) return self.prepare_object(types.Message.deserialize(await raw)) async def send_video(self, chat_id: Union[Integer, String], From 5eeace04f4b29d51665ffc6cc30d25adb70c5d37 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 26 Aug 2017 12:12:35 +0300 Subject: [PATCH 02/20] Update annotations. --- aiogram/bot/api.py | 15 ++++++++--- aiogram/bot/base.py | 48 ++++++++++++++++++++++-------------- docs/source/bot/base.rst | 2 ++ docs/source/bot/extended.rst | 4 ++- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 9cd8fdf4..96c2d17e 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -6,8 +6,8 @@ from http import HTTPStatus import aiohttp from ..utils import json -from ..utils.exceptions import ValidationError, TelegramAPIError, BadRequest, Unauthorized, NetworkError, RetryAfter, \ - MigrateToChat, ConflictError +from ..utils.exceptions import BadRequest, ConflictError, MigrateToChat, NetworkError, RetryAfter, TelegramAPIError, \ + Unauthorized, ValidationError from ..utils.helper import Helper, HelperMode, Item # Main aiogram logger @@ -129,13 +129,20 @@ async def request(session, token, method, data=None, files=None, continue_retry= https://core.telegram.org/bots/api#making-requests - :param session: :class:`aiohttp.ClientSession` + :param session: HTTP Client session + :type session: :obj:`aiohttp.ClientSession` :param token: BOT token + :type token: :obj:`str` :param method: API method + :type method: :obj:`str` :param data: request payload + :type data: :obj:`dict` :param files: files + :type files: :obj:`dict` :param continue_retry: - :return: bool or dict + :type continue_retry: :obj:`dict` + :return: result + :rtype :obj:`bool` or :obj:`dict` """ log.debug("Make request: '{0}' with data: {1} and files {2}".format( method, data or {}, files or {})) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 3a300ed9..c9f383ef 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -1,7 +1,7 @@ import asyncio import datetime import io -from typing import Union, TypeVar, List, Dict, Optional +from typing import Dict, List, Optional, TypeVar, Union import aiohttp @@ -31,13 +31,19 @@ class BaseBot: Instructions how to get Bot token is found here: https://core.telegram.org/bots#3-how-do-i-create-a-bot :param token: token from @BotFather + :type token: :obj:`str` :param loop: event loop + :type loop: Optional Union :obj:`asyncio.BaseEventLoop`, :obj:`asyncio.AbstractEventLoop` :param connections_limit: connections limit for aiohttp.ClientSession + :type connections_limit: :obj:`int` :param proxy: HTTP proxy URL - :param proxy_auth: :obj:`aiohttp.BasicAuth` + :type proxy: :obj:`str` + :param proxy_auth: Authentication information + :type proxy_auth: Optional :obj:`aiohttp.BasicAuth` :param continue_retry: automatic retry sent request when flood control exceeded + :type continue_retry: :obj:`bool` + :raise: when token is invalid throw an :obj:`aiogram.utils.exceptions.ValidationError` """ - self.__token = token self.proxy = proxy self.proxy_auth = proxy_auth @@ -56,8 +62,6 @@ class BaseBot: def __del__(self): """ When bot object is deleting - need close all sessions - - :return: """ for session in self._temp_sessions: if not session.closed: @@ -65,15 +69,18 @@ class BaseBot: if self.session and not self.session.closed: self.session.close() - def create_temp_session(self) -> aiohttp.ClientSession: + def create_temp_session(self, limit: int = 1) -> aiohttp.ClientSession: """ Create temporary session - :return: + :param limit: Limit of connections + :type limit: :obj:`int` + :return: New session + :rtype: :obj:`aiohttp.TCPConnector` """ session = aiohttp.ClientSession( - connector=aiohttp.TCPConnector(limit=1, force_close=True), - loop=self.loop) + connector=aiohttp.TCPConnector(limit=limit, force_close=True), + loop=self.loop, json_serialize=json.dumps) self._temp_sessions.append(session) return session @@ -81,8 +88,8 @@ class BaseBot: """ Destroy temporary session - :param session: - :return: + :param session: target session + :type session: :obj:`aiohttp.ClientSession` """ if not session.closed: session.close() @@ -98,10 +105,14 @@ class BaseBot: https://core.telegram.org/bots/api#making-requests :param method: API method + :type method: :obj:`str` :param data: request parameters + :type data: :obj:`dict` :param files: files - :return: Union[List, Dict] - :raise: :class:`aiogram.exceptions.TelegramApiError` + :type files: :obj:`dict` + :return: result + :rtype: Union[List, Dict] + :raise: :obj:`aiogram.exceptions.TelegramApiError` """ return await api.request(self.session, self.__token, method, data, files, proxy=self.proxy, proxy_auth=self.proxy_auth, @@ -118,7 +129,8 @@ class BaseBot: if You want to automatically create destination (:class:`io.BytesIO`) use default value of destination and handle result of this method. - :param file_path: String + :param file_path: file path on telegram server (You can get it from :obj:`aiogram.types.File`) + :type file_path: :obj:`str` :param destination: filename or instance of :class:`io.IOBase`. For e. g. :class:`io.BytesIO` :param timeout: Integer :param chunk_size: Integer @@ -129,7 +141,7 @@ class BaseBot: destination = io.BytesIO() session = self.create_temp_session() - url = api.FILE_URL.format(token=self.__token, path=file_path) + url = api.Methods.file_url(token=self.__token, path=file_path) dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb') try: @@ -153,10 +165,10 @@ class BaseBot: https://core.telegram.org/bots/api#inputfile :param file_type: field name - :param method: API metod + :param method: API method :param file: String or io.IOBase :param payload: request payload - :return: resonse + :return: response """ if file is None: files = {} @@ -434,7 +446,7 @@ class BaseBot: reply_to_message_id: Optional[Integer] = None, reply_markup: Optional[Union[ types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, Dict, String]] = None, - filename: Optional[str]=None) -> Dict: + filename: Optional[str] = None) -> Dict: """ Use this method to send general files. On success, the sent Message is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. diff --git a/docs/source/bot/base.rst b/docs/source/bot/base.rst index 00231e6c..b4e4b663 100644 --- a/docs/source/bot/base.rst +++ b/docs/source/bot/base.rst @@ -4,3 +4,5 @@ BaseBot This class is base of bot. In BaseBot implemented all available methods of Telegram Bot API. .. autoclass:: aiogram.bot.base.BaseBot + :members: + :show-inheritance: diff --git a/docs/source/bot/extended.rst b/docs/source/bot/extended.rst index 6c32bb12..71ed12ea 100644 --- a/docs/source/bot/extended.rst +++ b/docs/source/bot/extended.rst @@ -4,4 +4,6 @@ Bot object That is extended (and recommended for usage) bot class based on BaseBot class. You can use instance of that bot in :obj:`aiogram.dispatcher.Dispatcher` -.. autoclass:: aiogram.bot.bot.Bot \ No newline at end of file +.. autoclass:: aiogram.bot.bot.Bot + :members: + :show-inheritance: From 0b97fb790d13e725bf1f65781ba6737fd35595cf Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 26 Aug 2017 12:12:48 +0300 Subject: [PATCH 03/20] Update conda env. --- environment.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/environment.yml b/environment.yml index 3fabb664..b71c91bd 100644 --- a/environment.yml +++ b/environment.yml @@ -35,3 +35,5 @@ dependencies: - sqlite - xz - aiohttp + - aioredis + From d07ba77ac6fddadb5a35d761f52ab69f6d2cb78b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 26 Aug 2017 12:16:49 +0300 Subject: [PATCH 04/20] Stop using default conda channel. --- environment.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/environment.yml b/environment.yml index b71c91bd..c6a9bf4f 100644 --- a/environment.yml +++ b/environment.yml @@ -1,7 +1,6 @@ name: py36 channels: - conda-forge - - default dependencies: - python=3.6 - sphinx=1.5.3 From be8aaadd07d5c1025c587198fc4fb76c844514cd Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 26 Aug 2017 12:22:57 +0300 Subject: [PATCH 05/20] I hate RTD. --- environment.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/environment.yml b/environment.yml index c6a9bf4f..b6d1c93e 100644 --- a/environment.yml +++ b/environment.yml @@ -34,5 +34,3 @@ dependencies: - sqlite - xz - aiohttp - - aioredis - From 0fcb75e997a29ba680b3fd8f2516e34b8d78db22 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 26 Aug 2017 18:01:30 +0300 Subject: [PATCH 06/20] Implement tasks context manager. --- aiogram/utils/context.py | 105 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 aiogram/utils/context.py diff --git a/aiogram/utils/context.py b/aiogram/utils/context.py new file mode 100644 index 00000000..7e38ec62 --- /dev/null +++ b/aiogram/utils/context.py @@ -0,0 +1,105 @@ +import asyncio +import typing + +CONFIGURED = '@CONFIGURED_TASK_FACTORY' + + +def task_factory(loop: asyncio.BaseEventLoop, coro: typing.Coroutine): + """ + Task factory for implementing context processor + + :param loop: + :param coro: + :return: new task + :rtype: :obj:`asyncio.Task` + """ + # Is not allowed when loop is closed. + loop._check_closed() + + task = asyncio.Task(coro, loop=loop) + + # Hide factory + if task._source_traceback: + del task._source_traceback[-1] + + try: + task.context = asyncio.Task.current_task().context + except AttributeError: + task.context = {CONFIGURED: True} + + return task + + +def get_current_state() -> typing.Dict: + """ + Get current execution context from task + + :return: context + :rtype: :obj:`dict` + """ + task = asyncio.Task.current_task() + context = getattr(task, 'context', None) + if context is None: + context = task.context = {} + return context + + +def get_value(key, default=None): + """ + Get value from task + + :param key: + :param default: + :return: value + """ + return get_current_state().get(key, default) + + +def check_value(key): + """ + Key in context? + + :param key: + :return: + """ + return key in get_current_state() + + +def set_value(key, value): + """ + Set value + + :param key: + :param value: + :return: + """ + get_current_state()[key] = value + + +def del_value(key): + """ + Remove value from context + + :param key: + :return: + """ + del get_current_state()[key] + + +def update_state(data=None, **kwargs): + """ + Update multiple state items + + :param data: + :param kwargs: + :return: + """ + if data is None: + data = {} + state = get_current_state() + state.update(data, **kwargs) + + +def check_configured(): + print('CONFIGURED', get_value(CONFIGURED)) + return get_value(CONFIGURED) From a57c91067ebb85e77cda510b78c2bbca9dc7926c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 26 Aug 2017 18:02:01 +0300 Subject: [PATCH 07/20] Optimize state filter. --- aiogram/dispatcher/__init__.py | 47 +++++++++++++++++++++++++++++++--- aiogram/dispatcher/filters.py | 13 +++++++--- aiogram/dispatcher/webhook.py | 15 +++++++++-- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index 0230484d..3933a04a 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -3,16 +3,20 @@ import functools import logging import typing -from .filters import CommandsFilter, RegexpFilter, ContentTypeFilter, generate_default_filters +from .filters import CommandsFilter, ContentTypeFilter, RegexpFilter, USER_STATE, generate_default_filters from .handler import Handler -from .storage import DisabledStorage, BaseStorage, FSMContext +from .storage import BaseStorage, DisabledStorage, FSMContext from .webhook import BaseResponse from ..bot import Bot from ..types.message import ContentType -from ..utils.exceptions import TelegramAPIError, NetworkError +from ..utils import context +from ..utils.exceptions import NetworkError, TelegramAPIError log = logging.getLogger(__name__) +MODE = 'MODE' +LONG_POOLING = 'long-pooling' + class Dispatcher: """ @@ -79,7 +83,7 @@ class Dispatcher: """ tasks = [] for update in updates: - tasks.append(self.updates_handler.notify(update)) + tasks.append(self.process_update(update)) return await asyncio.gather(*tasks) async def process_update(self, update): @@ -90,23 +94,56 @@ class Dispatcher: :return: """ self.last_update_id = update.update_id + has_context = context.check_configured() if update.message: + if has_context: + state = self.storage.get_state(chat=update.message.chat.id, + user=update.message.from_user.id) + context.set_value(USER_STATE, await state) return await self.message_handlers.notify(update.message) if update.edited_message: + if has_context: + state = self.storage.get_state(chat=update.edited_message.chat.id, + user=update.edited_message.from_user.id) + context.set_value(USER_STATE, await state) return await self.edited_message_handlers.notify(update.edited_message) if update.channel_post: + if has_context: + state = self.storage.get_state(chat=update.message.chat.id, + user=update.message.from_user.id) + context.set_value(USER_STATE, await state) return await self.channel_post_handlers.notify(update.channel_post) if update.edited_channel_post: + if has_context: + state = self.storage.get_state(chat=update.edited_channel_post.chat.id, + user=update.edited_channel_post.from_user.id) + context.set_value(USER_STATE, await state) return await self.edited_channel_post_handlers.notify(update.edited_channel_post) if update.inline_query: + if has_context: + state = self.storage.get_state(user=update.inline_query.from_user.id) + context.set_value(USER_STATE, await state) return await self.inline_query_handlers.notify(update.inline_query) if update.chosen_inline_result: + if has_context: + state = self.storage.get_state(user=update.chosen_inline_result.from_user.id) + context.set_value(USER_STATE, await state) return await self.chosen_inline_result_handlers.notify(update.chosen_inline_result) if update.callback_query: + if has_context: + state = self.storage.get_state(chat=update.callback_query.message.chat.id, + user=update.callback_query.from_user.id) + context.set_value(USER_STATE, await state) return await self.callback_query_handlers.notify(update.callback_query) if update.shipping_query: + if has_context: + state = self.storage.get_state(user=update.shipping_query.from_user.id) + context.set_value(USER_STATE, await state) return await self.shipping_query_handlers.notify(update.shipping_query) if update.pre_checkout_query: + if has_context: + state = self.storage.get_state(user=update.pre_checkout_query.from_user.id) + context.set_value(USER_STATE, await state) return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query) async def start_pooling(self, timeout=20, relax=0.1, limit=None): @@ -121,6 +158,7 @@ class Dispatcher: if self._pooling: raise RuntimeError('Pooling already started') log.info('Start pooling.') + context.set_value(MODE, LONG_POOLING) self._pooling = True offset = None @@ -730,6 +768,7 @@ class Dispatcher: :param func: :return: """ + def process_response(task): response = task.result() self.loop.create_task(response.execute_response(self.bot)) diff --git a/aiogram/dispatcher/filters.py b/aiogram/dispatcher/filters.py index d62f5310..d4e114a4 100644 --- a/aiogram/dispatcher/filters.py +++ b/aiogram/dispatcher/filters.py @@ -1,8 +1,11 @@ import inspect import re +from aiogram.utils import context from ..utils.helper import Helper, HelperMode, Item +USER_STATE = 'USER_STATE' + async def check_filter(filter_, args, kwargs): if not callable(filter_): @@ -102,10 +105,14 @@ class StateFilter(AsyncFilter): if self.state == '*': return True - chat, user = self.get_target(obj) + if context.check_value(USER_STATE): + context_state = context.get_value(USER_STATE) + return self.state == context_state + else: + chat, user = self.get_target(obj) - if chat or user: - return await self.dispatcher.storage.get_state(chat=chat, user=user) == self.state + if chat or user: + return await self.dispatcher.storage.get_state(chat=chat, user=user) == self.state return False diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 64f4b566..a52ae618 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -3,13 +3,14 @@ import asyncio.tasks import datetime import functools import typing -from typing import Union, Dict, Optional +from typing import Dict, Optional, Union from aiohttp import web from .. import types from ..bot import api -from ..bot.base import Integer, String, Boolean, Float +from ..bot.base import Boolean, Float, Integer, String +from ..utils import context from ..utils import json from ..utils.deprecated import warn_deprecated as warn from ..utils.exceptions import TimeoutWarning @@ -20,6 +21,10 @@ BOT_DISPATCHER_KEY = 'BOT_DISPATCHER' RESPONSE_TIMEOUT = 55 +WEBHOOK = 'webhook' +WEBHOOK_CONNECTION = 'WEBHOOK_CONNECTION' +WEBHOOK_REQUEST = 'WEBHOOK_REQUEST' + class WebhookRequestHandler(web.View): """ @@ -71,6 +76,11 @@ class WebhookRequestHandler(web.View): :return: :class:`aiohttp.web.Response` """ + + context.update_state({'CALLER': WEBHOOK, + WEBHOOK_CONNECTION: True, + WEBHOOK_REQUEST: self.request}) + dispatcher = self.get_dispatcher() update = await self.parse_update(dispatcher.bot) @@ -113,6 +123,7 @@ class WebhookRequestHandler(web.View): if fut.done(): return fut.result() else: + context.set_value(WEBHOOK_CONNECTION, False) fut.remove_done_callback(cb) fut.add_done_callback(self.respond_via_request) finally: From 77e2d4c44a309accaa0a948a5af5073f0a2f517d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 26 Aug 2017 18:07:11 +0300 Subject: [PATCH 08/20] Annotate context manager. --- aiogram/utils/context.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/aiogram/utils/context.py b/aiogram/utils/context.py index 7e38ec62..70c27b5b 100644 --- a/aiogram/utils/context.py +++ b/aiogram/utils/context.py @@ -1,3 +1,10 @@ +""" +Need setup task factory: + >>> from aiogram.utils import context + >>> loop = asyncio.get_event_loop() + >>> loop.set_task_factory(context.task_factory) +""" + import asyncio import typing @@ -101,5 +108,8 @@ def update_state(data=None, **kwargs): def check_configured(): - print('CONFIGURED', get_value(CONFIGURED)) + """ + Check loop is configured + :return: + """ return get_value(CONFIGURED) From a5d983a52216ad9da9d07cefdcaaae90d5ba8f15 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 28 Aug 2017 16:08:09 +0300 Subject: [PATCH 09/20] Oops. Repair Dispatcher._process_pooling_updates --- aiogram/dispatcher/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index 3933a04a..b9a04fad 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -188,12 +188,11 @@ class Dispatcher: :param updates: list of updates. """ need_to_call = [] - for update in await self.process_updates(updates): - for responses in update: - for response in responses: - if not isinstance(response, BaseResponse): - continue - need_to_call.append(response.execute_response(self.bot)) + for response in await self.process_updates(updates): + for response in response: + if not isinstance(response, BaseResponse): + continue + need_to_call.append(response.execute_response(self.bot)) if need_to_call: try: asyncio.gather(*need_to_call) From 458c1c24ba237bada3951f1d699ef0ede9e1e883 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 28 Aug 2017 16:20:52 +0300 Subject: [PATCH 10/20] Implement universal bot executor. --- aiogram/utils/executor.py | 77 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 aiogram/utils/executor.py diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py new file mode 100644 index 00000000..a49cc099 --- /dev/null +++ b/aiogram/utils/executor.py @@ -0,0 +1,77 @@ +import asyncio + +from aiohttp import web + +from aiogram.bot.api import log +from aiogram.dispatcher import Dispatcher +from aiogram.dispatcher.webhook import BOT_DISPATCHER_KEY, get_new_configured_app +from aiogram.utils import context + + +async def _startup(dispatcher: Dispatcher, skip_updates=False, callback=None): + user = await dispatcher.bot.me + log.info(f"Bot: {user.full_name} [@{user.username}]") + + if callable(callback): + await callback(dispatcher) + + if skip_updates: + count = await dispatcher.skip_updates() + if count: + log.warning(f"Skipped {count} updates.") + + +async def _wh_startup(app): + callback = app.get('_startup_callback', None) + dispatcher = app.get(BOT_DISPATCHER_KEY, None) + skip_updates = app.get('_skip_updates', False) + await _startup(dispatcher, skip_updates=skip_updates, callback=callback) + + +async def _shutdown(dispatcher: Dispatcher, callback=None): + if callable(callback): + await callback(dispatcher) + + dispatcher.storage.close() + await dispatcher.storage.wait_closed() + + +async def _wh_shutdown(app): + callback = app.get('_shutdown_callback', None) + dispatcher = app.get(BOT_DISPATCHER_KEY, None) + await _shutdown(dispatcher, callback=callback) + + +def start_pooling(dispatcher, *, loop=None, skip_updates=False, on_startup=None, on_shutdown=None): + log.warning('Start bot with long-pooling.') + if loop is None: + loop = asyncio.get_event_loop() + + loop.set_task_factory(context.task_factory) + + loop.create_task(dispatcher.start_pooling()) + try: + loop.run_until_complete(_startup(dispatcher, skip_updates=skip_updates, callback=on_startup)) + loop.run_forever() + except (KeyboardInterrupt, SystemExit): + pass + finally: + loop.run_until_complete(_shutdown(dispatcher, callback=on_shutdown)) + log.warning("Goodbye!") + + +def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None, + **kwargs): + log.warning('Start bot with webhook.') + if loop is None: + loop = asyncio.get_event_loop() + + app = get_new_configured_app(dispatcher, webhook_path) + app['_startup_callback'] = on_startup + app['_shutdown_callback'] = on_shutdown + app['_skip_updates'] = skip_updates + + app.on_startup.append(_wh_startup) + app.on_shutdown.append(_wh_shutdown) + + web.run_app(app, loop=loop, **kwargs) From 6696903ace75de759e51c9ed7933615d71f6fe34 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 28 Aug 2017 16:21:09 +0300 Subject: [PATCH 11/20] Add example. --- examples/adwanced_executor_example.py | 133 ++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 examples/adwanced_executor_example.py diff --git a/examples/adwanced_executor_example.py b/examples/adwanced_executor_example.py new file mode 100644 index 00000000..a1dcdcc6 --- /dev/null +++ b/examples/adwanced_executor_example.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +""" +II this example used ArgumentParser for configuring Your bot. + +Provided to start bot with webhook: + python adwanced_executor_example.py \ + --token TOKEN_HERE \ + --host 0.0.0.0 \ + --port 8084 \ + --host-name dev.illemius.xyz \ + --webhook-port 443 + +Or long pooling: + python adwanced_executor_example.py --token TOKEN_HERE + +So... In this example found small trouble: + can't get bot instance in handlers. + + +If you want to automatic change getting updates method use executor utils (from aiogram.utils.executor) +""" +# TODO: Move token to environment variables. + +import argparse +import logging +import ssl +import sys + +from aiogram import Bot +from aiogram.dispatcher import Dispatcher +from aiogram.dispatcher.webhook import * +from aiogram.utils.executor import start_pooling, start_webhook + +logging.basicConfig(level=logging.INFO) + +# Configure arguments parser. +parser = argparse.ArgumentParser(description='Python telegram bot') +parser.add_argument('--token', '-t', nargs='?', type=str, default=None, help='Set working directory') +parser.add_argument('--sock', help='UNIX Socket path') +parser.add_argument('--host', help='Webserver host') +parser.add_argument('--port', type=int, help='Webserver port') +parser.add_argument('--cert', help='Path to SSL certificate') +parser.add_argument('--pkey', help='Path to SSL private key') +parser.add_argument('--host-name', help='Set webhook host name') +parser.add_argument('--webhook-port', type=int, help='Port for webhook (default=port)') +parser.add_argument('--webhook-path', default='/webhook', help='Port for webhook (default=port)') + + +async def cmd_start(message: types.Message): + return SendMessage(message.chat.id, f"Hello, {message.from_user.full_name}!") + + +def setup_handlers(dispatcher: Dispatcher): + # This example has only one messages handler + dispatcher.register_message_handler(cmd_start, commands=['start', 'welcome']) + + +async def on_startup(dispatcher, url=None, cert=None): + setup_handlers(dispatcher) + + bot = dispatcher.bot + + # Get current webhook status + webhook = await bot.get_webhook_info() + + if url: + # If URL is bad + if webhook.url != url: + # If URL doesnt match with by current remove webhook + if not webhook.url: + await bot.delete_webhook() + + # Set new URL for webhook + if cert: + with open(cert, 'rb') as cert_file: + await bot.set_webhook(url, certificate=cert_file) + else: + await bot.set_webhook(url) + elif webhook.url: + # Otherwise remove webhook. + await bot.delete_webhook() + + +async def on_shutdown(dispatcher): + print('Shutdown.') + + +def main(arguments): + args = parser.parse_args(arguments) + token = args.token + sock = args.sock + host = args.host + port = args.port + cert = args.cert + pkey = args.pkey + host_name = args.host_name or host + webhook_port = args.webhook_port or port + webhook_path = args.webhook_path + + # Fi webhook path + if not webhook_path.startswith('/'): + webhook_path = '/' + webhook_path + + # Generate webhook URL + webhook_url = f"https://{host_name}:{webhook_port}{webhook_path}" + + # Create bot & dispatcher instances. + bot = Bot(token) + dispatcher = Dispatcher(bot) + + if (sock or host) and host_name: + if cert and pkey: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + ssl_context.load_cert_chain(cert, pkey) + else: + ssl_context = None + + start_webhook(dispatcher, webhook_path, + on_startup=functools.partial(on_startup, url=webhook_url, cert=cert), + on_shutdown=on_shutdown, + host=host, port=port, path=sock, ssl_context=ssl_context) + else: + start_pooling(dispatcher, on_startup=on_startup, on_shutdown=on_shutdown) + + +if __name__ == '__main__': + argv = sys.argv[1:] + + if not len(argv): + parser.print_help() + sys.exit(1) + + main(argv) From 4771368cb068d0e4a61b8951fa5ceb8563d8a96b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 1 Sep 2017 17:20:43 +0300 Subject: [PATCH 12/20] Change domain name in example. --- examples/adwanced_executor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/adwanced_executor_example.py b/examples/adwanced_executor_example.py index a1dcdcc6..16194059 100644 --- a/examples/adwanced_executor_example.py +++ b/examples/adwanced_executor_example.py @@ -7,7 +7,7 @@ Provided to start bot with webhook: --token TOKEN_HERE \ --host 0.0.0.0 \ --port 8084 \ - --host-name dev.illemius.xyz \ + --host-name example.com \ --webhook-port 443 Or long pooling: From fb6a9af9faab472699bfe9dc9e7ccac3c92b2c2e Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 7 Sep 2017 20:44:06 +0300 Subject: [PATCH 13/20] Fix sphinx warning. Unknown interpreted text role "method". --- 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 a52ae618..a4fa604b 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -264,7 +264,7 @@ class ReplyToMixin: class SendMessage(BaseResponse, ReplyToMixin): """ You can send message with webhook by using this instance of this object. - All arguments is equal with :method:`Bot.send_message` method. + All arguments is equal with Bot.send_message method. """ __slots__ = ('chat_id', 'text', 'parse_mode', From 3c2a37542b32d8cc13465c1cf88c80f66da37f5b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 11 Sep 2017 01:06:30 +0300 Subject: [PATCH 14/20] Deprecate middlewares. --- aiogram/dispatcher/middlewares.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aiogram/dispatcher/middlewares.py b/aiogram/dispatcher/middlewares.py index 6c7eafb8..635ee7b2 100644 --- a/aiogram/dispatcher/middlewares.py +++ b/aiogram/dispatcher/middlewares.py @@ -1,7 +1,9 @@ +from aiogram.utils.deprecated import deprecated from . import Handler from .handler import SkipHandler +@deprecated class Middleware: def __init__(self, handler, filters=None): self.handler: Handler = handler From 7e53111773a34b10e38bd16b4a8650a30711a983 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 12 Sep 2017 00:29:04 +0300 Subject: [PATCH 15/20] Update `setup.py` --- setup.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) mode change 100644 => 100755 setup.py diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 78e7990c..4952be85 --- a/setup.py +++ b/setup.py @@ -1,15 +1,44 @@ +#!/usr/bin/env python3 + +import string from distutils.core import setup from setuptools import PackageFinder from aiogram import __version__ as version +ALLOWED_SYMBOLS = string.ascii_letters + string.digits + '_-' + def get_description(): + """ + Read full description from 'README.rst' + + :return: description + :rtype: str + """ with open('README.rst', encoding='utf-8') as f: return f.read() +def get_requirements(): + """ + Read requirements from 'requirements txt' + + :return: requirements + :rtype: list + """ + requirements = [] + with open('requirements.txt', 'r') as file: + for line in file.readlines(): + line = line.strip() + if not line or line.startswith('#'): + continue + requirements.append(line) + + return requirements + + setup( name='aiogram', version=version, @@ -18,15 +47,15 @@ setup( license='MIT', author='Alex Root Junior', author_email='jroot.junior@gmail.com', - description='Telegram bot API framework based on asyncio', + description='Is are pretty simple and fully asynchronously library for Telegram Bot API', long_description=get_description(), classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python :: 3.6', 'Environment :: Console', 'Framework :: AsyncIO', 'Topic :: Software Development :: Libraries :: Application Frameworks', 'License :: OSI Approved :: MIT License', ], - install_requires=['aiohttp'] + install_requires=get_requirements() ) From d33f01eb10981696c8fc0524bf0cd634fcb51a8c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 12 Sep 2017 01:47:37 +0300 Subject: [PATCH 16/20] Implement paginator and split long text utils. --- aiogram/utils/parts.py | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 aiogram/utils/parts.py diff --git a/aiogram/utils/parts.py b/aiogram/utils/parts.py new file mode 100644 index 00000000..e03f7bcc --- /dev/null +++ b/aiogram/utils/parts.py @@ -0,0 +1,59 @@ +import typing + +MAX_MESSAGE_LENGTH = 4096 + + +def split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]: + """ + Split long text + + :param text: + :param length: + :return: list of parts + :rtype: :obj:`typing.List[str]` + """ + return [text[i:i + length] for i in range(0, len(text), length)] + + +def safe_split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]: + """ + Split long text + + :param text: + :param length: + :return: + """ + # TODO: More informative description + + temp_text = text + parts = [] + while temp_text: + if len(temp_text) > length: + try: + split_pos = temp_text[:length].rindex(' ') + except ValueError: + split_pos = length + if split_pos < length // 4 * 3: + split_pos = length + parts.append(temp_text[:split_pos]) + temp_text = temp_text[split_pos:].lstrip() + else: + parts.append(temp_text) + break + return parts + + +def paginate(data: typing.Iterable, page: int = 0, limit: int = 10) -> typing.Iterable: + """ + Slice data over pages + + :param data: any iterable object + :type data: :obj:`typing.Iterable` + :param page: number of page + :type page: :obj:`int` + :param limit: items per page + :type limit: :obj:`int` + :return: sliced object + :rtype: :obj:`typing.Iterable` + """ + return data[page * limit:page * limit + limit] From dc7abb12dc48fda55d42fade7dd3b7e5ee8f2ac3 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 12 Sep 2017 03:31:41 +0300 Subject: [PATCH 17/20] DANGER! Change key scheme in Redis. --- aiogram/contrib/fsm_storage/redis.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/aiogram/contrib/fsm_storage/redis.py b/aiogram/contrib/fsm_storage/redis.py index a6bf2b30..4fb4b897 100644 --- a/aiogram/contrib/fsm_storage/redis.py +++ b/aiogram/contrib/fsm_storage/redis.py @@ -81,7 +81,7 @@ class RedisStorage(BaseStorage): :return: """ chat, user = self.check_address(chat=chat, user=user) - addr = f"{chat}:{user}" + addr = f"fsm:{chat}:{user}" conn = await self.redis data = await conn.execute('GET', addr) @@ -104,7 +104,7 @@ class RedisStorage(BaseStorage): data = {} chat, user = self.check_address(chat=chat, user=user) - addr = f"{chat}:{user}" + addr = f"fsm:{chat}:{user}" record = {'state': state, 'data': data} @@ -138,3 +138,19 @@ class RedisStorage(BaseStorage): data = [] data.update(data, **kwargs) await self.set_data(chat=chat, user=user, data=data) + + async def get_states_list(self) -> typing.List[typing.Tuple[int]]: + """ + Get list of all stored chat's and user's + + :return: list of tuples where first element is chat id and second is user id + """ + conn = await self.redis + result = [] + + keys = await conn.execute('KEYS', 'fsm:*') + for item in keys: + *_, chat, user = item.decode('utf-8').split(':') + result.append((chat, user)) + + return result From bac44d58a61c23924560a08ba248c70645bb878a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 12 Sep 2017 03:32:56 +0300 Subject: [PATCH 18/20] Small improvements of markdown/html utils. --- aiogram/utils/markdown.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index 7e6b7cf6..da08a400 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -11,6 +11,13 @@ MD_SYMBOLS = ( ('
', '
'), ) +HTML_QUOTES_MAP = { + '<': '<', + '>': '>', + '&': '&', + '"': '"' +} + def _join(*content, sep=' '): return sep.join(map(str, content)) @@ -27,6 +34,22 @@ def _md(string, symbols=('', '')): return start + string + end +def quote_html(content): + """ + Quote HTML symbols + + All <, > and & symbols that are not a part of a tag or an HTML entity + must be replaced with the corresponding HTML entities (< with <, > with > and & with &). + + :param content: str + :return: str + """ + new_content = '' + for symbol in content: + new_content += HTML_QUOTES_MAP[symbol] if symbol in '<>&"' else symbol + return new_content + + def text(*content, sep=' '): """ Join all elements with separator @@ -57,7 +80,7 @@ def hbold(*content, sep=' '): :param sep: :return: """ - return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[4]) + return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[4]) def italic(*content, sep=' '): @@ -79,7 +102,7 @@ def hitalic(*content, sep=' '): :param sep: :return: """ - return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[5]) + return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[5]) def code(*content, sep=' '): @@ -101,7 +124,7 @@ def hcode(*content, sep=' '): :param sep: :return: """ - return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[6]) + return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[6]) def pre(*content, sep='\n'): @@ -123,7 +146,7 @@ def hpre(*content, sep='\n'): :param sep: :return: """ - return _md(_join(*content, sep=sep), symbols=MD_SYMBOLS[7]) + return _md(quote_html(_join(*content, sep=sep)), symbols=MD_SYMBOLS[7]) def link(title, url): @@ -134,7 +157,7 @@ def link(title, url): :param url: :return: """ - return "[{0}]({1})".format(_escape(title), url) + return "[{0}]({1})".format(title, url) def hlink(title, url): @@ -145,7 +168,7 @@ def hlink(title, url): :param url: :return: """ - return "{1}".format(url, _escape(title)) + return "{1}".format(url, quote_html(title)) def escape_md(*content, sep=' '): From 534dea6de7a9d681aefe18d9752521018a900fd3 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 15 Sep 2017 00:11:32 +0300 Subject: [PATCH 19/20] Implemented error handler in dispatcher (requested by @Oleg_Oleg_Oleg) --- aiogram/dispatcher/__init__.py | 144 +++++++++++++++++++++------------ aiogram/dispatcher/filters.py | 13 +++ 2 files changed, 104 insertions(+), 53 deletions(-) diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index b9a04fad..d1bdb6f5 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -3,7 +3,8 @@ import functools import logging import typing -from .filters import CommandsFilter, ContentTypeFilter, RegexpFilter, USER_STATE, generate_default_filters +from .filters import CommandsFilter, ContentTypeFilter, RegexpFilter, USER_STATE, generate_default_filters, \ + ExceptionsFilter from .handler import Handler from .storage import BaseStorage, DisabledStorage, FSMContext from .webhook import BaseResponse @@ -52,6 +53,8 @@ class Dispatcher: self.updates_handler.register(self.process_update) + self.errors_handlers = Handler(self, once=False) + self._pooling = False def __del__(self): @@ -93,58 +96,64 @@ class Dispatcher: :param update: :return: """ - self.last_update_id = update.update_id - has_context = context.check_configured() - if update.message: - if has_context: - state = self.storage.get_state(chat=update.message.chat.id, - user=update.message.from_user.id) - context.set_value(USER_STATE, await state) - return await self.message_handlers.notify(update.message) - if update.edited_message: - if has_context: - state = self.storage.get_state(chat=update.edited_message.chat.id, - user=update.edited_message.from_user.id) - context.set_value(USER_STATE, await state) - return await self.edited_message_handlers.notify(update.edited_message) - if update.channel_post: - if has_context: - state = self.storage.get_state(chat=update.message.chat.id, - user=update.message.from_user.id) - context.set_value(USER_STATE, await state) - return await self.channel_post_handlers.notify(update.channel_post) - if update.edited_channel_post: - if has_context: - state = self.storage.get_state(chat=update.edited_channel_post.chat.id, - user=update.edited_channel_post.from_user.id) - context.set_value(USER_STATE, await state) - return await self.edited_channel_post_handlers.notify(update.edited_channel_post) - if update.inline_query: - if has_context: - state = self.storage.get_state(user=update.inline_query.from_user.id) - context.set_value(USER_STATE, await state) - return await self.inline_query_handlers.notify(update.inline_query) - if update.chosen_inline_result: - if has_context: - state = self.storage.get_state(user=update.chosen_inline_result.from_user.id) - context.set_value(USER_STATE, await state) - return await self.chosen_inline_result_handlers.notify(update.chosen_inline_result) - if update.callback_query: - if has_context: - state = self.storage.get_state(chat=update.callback_query.message.chat.id, - user=update.callback_query.from_user.id) - context.set_value(USER_STATE, await state) - return await self.callback_query_handlers.notify(update.callback_query) - if update.shipping_query: - if has_context: - state = self.storage.get_state(user=update.shipping_query.from_user.id) - context.set_value(USER_STATE, await state) - return await self.shipping_query_handlers.notify(update.shipping_query) - if update.pre_checkout_query: - if has_context: - state = self.storage.get_state(user=update.pre_checkout_query.from_user.id) - context.set_value(USER_STATE, await state) - return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query) + try: + self.last_update_id = update.update_id + has_context = context.check_configured() + if update.message: + if has_context: + state = self.storage.get_state(chat=update.message.chat.id, + user=update.message.from_user.id) + context.set_value(USER_STATE, await state) + return await self.message_handlers.notify(update.message) + if update.edited_message: + if has_context: + state = self.storage.get_state(chat=update.edited_message.chat.id, + user=update.edited_message.from_user.id) + context.set_value(USER_STATE, await state) + return await self.edited_message_handlers.notify(update.edited_message) + if update.channel_post: + if has_context: + state = self.storage.get_state(chat=update.message.chat.id, + user=update.message.from_user.id) + context.set_value(USER_STATE, await state) + return await self.channel_post_handlers.notify(update.channel_post) + if update.edited_channel_post: + if has_context: + state = self.storage.get_state(chat=update.edited_channel_post.chat.id, + user=update.edited_channel_post.from_user.id) + context.set_value(USER_STATE, await state) + return await self.edited_channel_post_handlers.notify(update.edited_channel_post) + if update.inline_query: + if has_context: + state = self.storage.get_state(user=update.inline_query.from_user.id) + context.set_value(USER_STATE, await state) + return await self.inline_query_handlers.notify(update.inline_query) + if update.chosen_inline_result: + if has_context: + state = self.storage.get_state(user=update.chosen_inline_result.from_user.id) + context.set_value(USER_STATE, await state) + return await self.chosen_inline_result_handlers.notify(update.chosen_inline_result) + if update.callback_query: + if has_context: + state = self.storage.get_state(chat=update.callback_query.message.chat.id, + user=update.callback_query.from_user.id) + context.set_value(USER_STATE, await state) + return await self.callback_query_handlers.notify(update.callback_query) + if update.shipping_query: + if has_context: + state = self.storage.get_state(user=update.shipping_query.from_user.id) + context.set_value(USER_STATE, await state) + return await self.shipping_query_handlers.notify(update.shipping_query) + if update.pre_checkout_query: + if has_context: + state = self.storage.get_state(user=update.pre_checkout_query.from_user.id) + context.set_value(USER_STATE, await state) + return await self.pre_checkout_query_handlers.notify(update.pre_checkout_query) + except Exception as e: + err = await self.errors_handlers.notify(self, update, e) + if err: + return err + raise async def start_pooling(self, timeout=20, relax=0.1, limit=None): """ @@ -746,6 +755,35 @@ class Dispatcher: return decorator + def register_errors_handler(self, callback, *, func=None, exception=None): + """ + Register errors handler + + :param callback: + :param func: + :param exception: you can make handler for specific errors type + """ + filters_set = [] + if func is not None: + filters_set.append(func) + if exception is not None: + filters_set.append(ExceptionsFilter(exception)) + self.errors_handlers.register(callback, filters_set) + + def errors_handler(self, *, func=None, exception=None): + """ + Decorator for registering errors handler + + :param func: + :param exception: you can make handler for specific errors type + :return: + """ + def decorator(callback): + self.register_errors_handler(callback, func=func, exception=exception) + return callback + + return decorator + def current_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None) -> FSMContext: diff --git a/aiogram/dispatcher/filters.py b/aiogram/dispatcher/filters.py index d4e114a4..51ae0e1f 100644 --- a/aiogram/dispatcher/filters.py +++ b/aiogram/dispatcher/filters.py @@ -125,6 +125,19 @@ class StatesListFilter(StateFilter): return False +class ExceptionsFilter(Filter): + def __init__(self, exception): + self.exception = exception + + def check(self, dispatcher, update, exception): + try: + raise exception + except self.exception: + return True + except: + return False + + def generate_default_filters(dispatcher, *args, **kwargs): filters_set = [] From 986ffa0ebd0e17e8b01b0c3c9a5f42c1d6323ba6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 16 Sep 2017 20:00:00 +0300 Subject: [PATCH 20/20] Annotate another eecution method in example echo bot. --- examples/echo_bot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/echo_bot.py b/examples/echo_bot.py index 80c616cb..2e8eba35 100644 --- a/examples/echo_bot.py +++ b/examples/echo_bot.py @@ -41,3 +41,7 @@ if __name__ == '__main__': loop.run_until_complete(main()) except KeyboardInterrupt: loop.stop() + + # Also you can use another execution method + # >>> from aiogram.utils.executor import start_pooling + # >>> start_pooling(dp, loop=loop, on_startup=main, on_shutdown=shutdown)