From a2259b7e1643d40cba052c3ef1d18d82dc61c8e4 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 15 Aug 2017 06:28:13 +0300 Subject: [PATCH 01/10] Go back to dev. --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 620e5606..be669f7c 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, 3, 5, stage=Stage.DEV, build=0) +VERSION = Version(0, 4, 1, stage=Stage.DEV, build=0) __version__ = VERSION.version From be28a92aba42891110e40251e6a6cacccba99763 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 16 Aug 2017 19:14:50 +0300 Subject: [PATCH 02/10] Implement reply mixin for webhook responses. --- aiogram/dispatcher/webhook.py | 41 ++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 53d2e42c..0f33bd73 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -156,7 +156,22 @@ class BaseResponse: return await bot.request(self.method, self.cleanup()) -class SendMessage(BaseResponse): +class ReplyToMixin: + """ + Mixin for responses where from which can reply to messages. + """ + def reply(self, message: typing.Union[int, types.Message]): + """ + Reply to message + + :param message: :obj:`int` or :obj:`types.Message` + :return: self + """ + setattr(self, 'reply_to_message_id', message.message_id if isinstance(message, types.Message) else message) + return self + + +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. @@ -245,7 +260,7 @@ class ForwardMessage(BaseResponse): } -class SendPhoto(BaseResponse): +class SendPhoto(BaseResponse, ReplyToMixin): """ Use that response type for send photo on to webhook. """ @@ -294,7 +309,7 @@ class SendPhoto(BaseResponse): } -class SendAudio(BaseResponse): +class SendAudio(BaseResponse, ReplyToMixin): """ Use that response type for send audio on to webhook. """ @@ -356,7 +371,7 @@ class SendAudio(BaseResponse): } -class SendDocument(BaseResponse): +class SendDocument(BaseResponse, ReplyToMixin): """ Use that response type for send document on to webhook. """ @@ -406,7 +421,7 @@ class SendDocument(BaseResponse): } -class SendVideo(BaseResponse): +class SendVideo(BaseResponse, ReplyToMixin): """ Use that response type for send video on to webhook. """ @@ -469,7 +484,7 @@ class SendVideo(BaseResponse): } -class SendVoice(BaseResponse): +class SendVoice(BaseResponse, ReplyToMixin): """ Use that response type for send voice on to webhook. """ @@ -523,7 +538,7 @@ class SendVoice(BaseResponse): } -class SendVideoNote(BaseResponse): +class SendVideoNote(BaseResponse, ReplyToMixin): """ Use that response type for send video note on to webhook. """ @@ -576,7 +591,7 @@ class SendVideoNote(BaseResponse): } -class SendLocation(BaseResponse): +class SendLocation(BaseResponse, ReplyToMixin): """ Use that response type for send location on to webhook. """ @@ -621,7 +636,7 @@ class SendLocation(BaseResponse): } -class SendVenue(BaseResponse): +class SendVenue(BaseResponse, ReplyToMixin): """ Use that response type for send venue on to webhook. """ @@ -680,7 +695,7 @@ class SendVenue(BaseResponse): } -class SendContact(BaseResponse): +class SendContact(BaseResponse, ReplyToMixin): """ Use that response type for send contact on to webhook. """ @@ -1278,7 +1293,7 @@ class DeleteMessage(BaseResponse): } -class SendSticker(BaseResponse): +class SendSticker(BaseResponse, ReplyToMixin): """ Use that response type for send sticker on to webhook. """ @@ -1524,7 +1539,7 @@ class AnswerInlineQuery(BaseResponse): } -class SendInvoice(BaseResponse): +class SendInvoice(BaseResponse, ReplyToMixin): """ Use that response type for send invoice on to webhook. """ @@ -1705,7 +1720,7 @@ class AnswerPreCheckoutQuery(BaseResponse): } -class SendGame(BaseResponse): +class SendGame(BaseResponse, ReplyToMixin): """ Use that response type for send game on to webhook. """ From d4f1b72d9235afef32c43379471eeaaaca31645a Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 16 Aug 2017 19:34:14 +0300 Subject: [PATCH 03/10] Small optimization and stop using bare-except. --- aiogram/dispatcher/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index d2603500..ba60050e 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging import typing +from aiogram.utils.exceptions import TelegramAPIError, NetworkError from .filters import CommandsFilter, RegexpFilter, ContentTypeFilter, generate_default_filters from .handler import Handler from .storage import DisabledStorage, BaseStorage, FSMContext @@ -77,7 +78,7 @@ class Dispatcher: """ tasks = [] for update in updates: - tasks.append(self.loop.create_task(self.updates_handler.notify(update))) + tasks.append(self.updates_handler.notify(update)) return await asyncio.gather(*tasks) async def process_update(self, update): @@ -125,10 +126,9 @@ class Dispatcher: while self._pooling: try: updates = await self.bot.get_updates(limit=limit, offset=offset, timeout=timeout) - except Exception as e: - log.exception('Cause exception while getting updates') - if relax: - await asyncio.sleep(relax) + except NetworkError: + log.exception('Cause exception while getting updates.') + await asyncio.sleep(15) continue if updates: @@ -137,7 +137,8 @@ class Dispatcher: self.loop.create_task(self._process_pooling_updates(updates)) - await asyncio.sleep(relax) + if relax: + await asyncio.sleep(relax) log.warning('Pooling is stopped.') @@ -157,7 +158,7 @@ class Dispatcher: if need_to_call: try: asyncio.gather(*need_to_call) - except Exception as e: + except TelegramAPIError: log.exception('Cause exception while processing updates.') def stop_pooling(self): From 09fa911ffaa5a6c782e66112fe43fac248f7aa11 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 22 Aug 2017 20:28:22 +0300 Subject: [PATCH 04/10] Add ConflictError (309) --- aiogram/bot/api.py | 4 +++- aiogram/utils/exceptions.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 14cd0dba..9cd8fdf4 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -7,7 +7,7 @@ import aiohttp from ..utils import json from ..utils.exceptions import ValidationError, TelegramAPIError, BadRequest, Unauthorized, NetworkError, RetryAfter, \ - MigrateToChat + MigrateToChat, ConflictError from ..utils.helper import Helper, HelperMode, Item # Main aiogram logger @@ -66,6 +66,8 @@ async def _check_result(method_name, response): raise MigrateToChat(result_json['migrate_to_chat_id']) elif response.status == HTTPStatus.BAD_REQUEST: raise BadRequest(description) + elif response.status == HTTPStatus.CONFLICT: + raise ConflictError(description) elif response.status in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]: raise Unauthorized(description) elif response.status == HTTPStatus.REQUEST_ENTITY_TOO_LARGE: diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 4a14f914..0bf3a030 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -1,11 +1,11 @@ -_PREFIXES = ['Error: ', '[Error]: ', 'Bad Request: '] +_PREFIXES = ['Error: ', '[Error]: ', 'Bad Request: ', 'Conflict: '] def _clean_message(text): for prefix in _PREFIXES: if text.startswith(prefix): text = text[len(prefix):] - return text + return (text[0].upper() + text[1:]).strip() class TelegramAPIError(Exception): @@ -21,6 +21,10 @@ class BadRequest(TelegramAPIError): pass +class ConflictError(TelegramAPIError): + pass + + class Unauthorized(TelegramAPIError): pass From f47b3670de11817978b6e9b7d7828f7ffc868b2f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 22 Aug 2017 20:31:23 +0300 Subject: [PATCH 05/10] So.. If response time longer than 55 seconds write result 'ok' and send response as simple HTTP request. --- aiogram/dispatcher/webhook.py | 61 +++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 0f33bd73..860b0cc9 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -1,4 +1,8 @@ +import asyncio +import asyncio.tasks import datetime +import functools +import time import typing from typing import Union, Dict, Optional @@ -13,6 +17,8 @@ from ..utils.payload import prepare_arg DEFAULT_WEB_PATH = '/webhook' BOT_DISPATCHER_KEY = 'BOT_DISPATCHER' +RESPONSE_TIMEOUT = 55 + class WebhookRequestHandler(web.View): """ @@ -35,6 +41,10 @@ class WebhookRequestHandler(web.View): """ + def __init__(self, request): + self._start_time = time.time() + super(WebhookRequestHandler, self).__init__(request) + def get_dispatcher(self): """ Get Dispatcher instance from environment @@ -66,12 +76,56 @@ class WebhookRequestHandler(web.View): """ dispatcher = self.get_dispatcher() update = await self.parse_update(dispatcher.bot) - results = await dispatcher.process_update(update) + results = await self.process_update(update) + response = self.get_response(results) + + if response: + return response.get_web_response() + return web.Response(text='ok') + + async def process_update(self, update): + dispatcher = self.get_dispatcher() + loop = dispatcher.loop + + waiter = loop.create_future() + 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.add_done_callback(cb) + + try: + try: + await waiter + except asyncio.futures.CancelledError: + fut.remove_done_callback(cb) + fut.cancel() + raise + + if fut.done(): + return fut.result() + else: + fut.remove_done_callback(cb) + fut.add_done_callback(self.response_task) + finally: + timeout_handle.cancel() + + def response_task(self, task): + dispatcher = self.get_dispatcher() + loop = dispatcher.loop + + results = task.result() + response = self.get_response(results) + if response is not None: + asyncio.ensure_future(response.execute_response(self.get_dispatcher().bot), loop=loop) + + def get_response(self, results): + if results is None: + return None for result in results: if isinstance(result, BaseResponse): - return result.get_web_response() - return web.Response(text='ok') + return result def configure_app(dispatcher, app: web.Application, path=DEFAULT_WEB_PATH): @@ -160,6 +214,7 @@ class ReplyToMixin: """ Mixin for responses where from which can reply to messages. """ + def reply(self, message: typing.Union[int, types.Message]): """ Reply to message From 0b21996b870c8e855fa92b227a995ef1669442b6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 22 Aug 2017 20:31:41 +0300 Subject: [PATCH 06/10] Add async_task decorator. --- aiogram/dispatcher/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index ba60050e..1053dda1 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -1,14 +1,15 @@ import asyncio +import functools import logging import typing -from aiogram.utils.exceptions import TelegramAPIError, NetworkError from .filters import CommandsFilter, RegexpFilter, ContentTypeFilter, generate_default_filters from .handler import Handler from .storage import DisabledStorage, BaseStorage, FSMContext from .webhook import BaseResponse from ..bot import Bot from ..types.message import ContentType +from ..utils.exceptions import TelegramAPIError, NetworkError log = logging.getLogger(__name__) @@ -712,3 +713,15 @@ class Dispatcher: chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None) -> FSMContext: return FSMContext(storage=self.storage, chat=chat, user=user) + + def async_task(self, func): + def process_response(task): + response = task.result() + self.loop.create_task(response.execute_response(self.bot)) + + @functools.wraps(func) + async def wrapper(*args, **kwargs): + task = self.loop.create_task(func(*args, **kwargs)) + task.add_done_callback(process_response) + + return wrapper From 6696c8e9adfe7dee7cbc0527ce9bf9a1bc2c28ed Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 23 Aug 2017 22:23:57 +0300 Subject: [PATCH 07/10] More annotations and add warning. --- aiogram/dispatcher/__init__.py | 15 ++++++++++++++ aiogram/dispatcher/webhook.py | 38 +++++++++++++++++++++++++++------- aiogram/utils/exceptions.py | 8 +++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index 1053dda1..0230484d 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -715,6 +715,21 @@ class Dispatcher: return FSMContext(storage=self.storage, chat=chat, user=user) def async_task(self, func): + """ + Execute handler as task and return None. + Use that decorator for slow handlers (with timeouts) + + .. code-block:: python3 + + @dp.message_handler(commands=['command']) + @dp.async_task + async def cmd_with_timeout(message: types.Message): + await asyncio.sleep(120) + return SendMessage(message.chat.id, 'KABOOM').reply(message) + + :param func: + :return: + """ def process_response(task): response = task.result() self.loop.create_task(response.execute_response(self.bot)) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 860b0cc9..64f4b566 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -2,7 +2,6 @@ import asyncio import asyncio.tasks import datetime import functools -import time import typing from typing import Union, Dict, Optional @@ -12,6 +11,8 @@ from .. import types from ..bot import api from ..bot.base import Integer, String, Boolean, Float from ..utils import json +from ..utils.deprecated import warn_deprecated as warn +from ..utils.exceptions import TimeoutWarning from ..utils.payload import prepare_arg DEFAULT_WEB_PATH = '/webhook' @@ -41,10 +42,6 @@ class WebhookRequestHandler(web.View): """ - def __init__(self, request): - self._start_time = time.time() - super(WebhookRequestHandler, self).__init__(request) - def get_dispatcher(self): """ Get Dispatcher instance from environment @@ -85,9 +82,19 @@ class WebhookRequestHandler(web.View): return web.Response(text='ok') async def process_update(self, update): + """ + Need respond in less than 60 seconds in to webhook. + + So... If you respond greater than 55 seconds webhook automatically respond 'ok' + and execute callback response via simple HTTP request. + + :param update: + :return: + """ dispatcher = self.get_dispatcher() loop = dispatcher.loop + # Analog of `asyncio.wait_for` but without cancelling task waiter = loop.create_future() timeout_handle = loop.call_later(RESPONSE_TIMEOUT, asyncio.tasks._release_waiter, waiter) cb = functools.partial(asyncio.tasks._release_waiter, waiter) @@ -107,11 +114,22 @@ class WebhookRequestHandler(web.View): return fut.result() else: fut.remove_done_callback(cb) - fut.add_done_callback(self.response_task) + fut.add_done_callback(self.respond_via_request) finally: timeout_handle.cancel() - def response_task(self, task): + def respond_via_request(self, task): + """ + Handle response after 55 second. + + :param task: + :return: + """ + warn(f"Detected slow response into webhook. " + f"(Greater than {RESPONSE_TIMEOUT} seconds)\n" + f"Recommended to use 'async_task' decorator from Dispatcher for handler with long timeouts.", + TimeoutWarning) + dispatcher = self.get_dispatcher() loop = dispatcher.loop @@ -121,6 +139,12 @@ class WebhookRequestHandler(web.View): asyncio.ensure_future(response.execute_response(self.get_dispatcher().bot), loop=loop) def get_response(self, results): + """ + Get response object from results. + + :param results: list + :return: + """ if results is None: return None for result in results: diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 0bf3a030..edbf5bcd 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -13,6 +13,14 @@ class TelegramAPIError(Exception): super(TelegramAPIError, self).__init__(_clean_message(message)) +class AIOGramWarning(Warning): + pass + + +class TimeoutWarning(AIOGramWarning): + pass + + class ValidationError(TelegramAPIError): pass From c8349112d03df208147be061b5ae679aaba747fe Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 23 Aug 2017 22:51:27 +0300 Subject: [PATCH 08/10] Bot API 3.3. --- aiogram/types/chat.py | 10 ++++++++-- aiogram/types/message.py | 24 ++++++++++++++---------- aiogram/types/user.py | 19 +++++++++++++++++-- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 1c2ac77f..7e5ec498 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -11,7 +11,9 @@ class Chat(Deserializable): """ def __init__(self, id, type, title, username, first_name, last_name, all_members_are_administrators, photo, - description, invite_link): + description, invite_link, pinned_message): + from .message import Message + self.id: int = id self.type: str = type self.title: str = title @@ -22,9 +24,12 @@ class Chat(Deserializable): self.photo: ChatPhoto = photo self.description: str = description self.invite_link: str = invite_link + self.pinned_message: Message = pinned_message @classmethod def de_json(cls, raw_data) -> 'Chat': + from .message import Message + id: int = raw_data.get('id') type: str = raw_data.get('type') title: str = raw_data.get('title') @@ -35,9 +40,10 @@ class Chat(Deserializable): photo = raw_data.get('photo') description = raw_data.get('description') invite_link = raw_data.get('invite_link') + pinned_message: Message = Message.deserialize(raw_data.get('pinned_message')) return Chat(id, type, title, username, first_name, last_name, all_members_are_administrators, photo, - description, invite_link) + description, invite_link, pinned_message) @property def full_name(self): diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 6e6feb3b..de70af6b 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -29,11 +29,11 @@ class Message(Deserializable): """ def __init__(self, message_id, from_user, date, chat, forward_from, forward_from_chat, forward_from_message_id, - forward_date, reply_to_message, edit_date, text, entities, audio, document, game, photo, sticker, - video, voice, video_note, new_chat_members, caption, contact, location, venue, left_chat_member, - new_chat_title, new_chat_photo, delete_chat_photo, group_chat_created, supergroup_chat_created, - channel_chat_created, migrate_to_chat_id, migrate_from_chat_id, pinned_message, invoice, - successful_payment, content_type): + forward_signature, forward_date, reply_to_message, edit_date, author_signature, text, entities, audio, + document, game, photo, sticker, video, voice, video_note, new_chat_members, caption, contact, location, + venue, left_chat_member, new_chat_title, new_chat_photo, delete_chat_photo, group_chat_created, + supergroup_chat_created, channel_chat_created, migrate_to_chat_id, migrate_from_chat_id, + pinned_message, invoice, successful_payment, content_type): self.message_id: int = message_id self.from_user: User = from_user self.date: datetime.datetime = date @@ -41,9 +41,11 @@ class Message(Deserializable): self.forward_from: User = forward_from self.forward_from_chat: Chat = forward_from_chat self.forward_from_message_id: int = forward_from_message_id + self.forward_signature: str = forward_signature self.forward_date: datetime.datetime = forward_date self.reply_to_message: Message = reply_to_message self.edit_date: datetime.datetime = edit_date + self.author_signature: str = author_signature self.text: str = text self.entities = entities self.audio = audio @@ -83,9 +85,11 @@ class Message(Deserializable): forward_from = User.deserialize(raw_data.get('forward_from', {})) forward_from_chat = Chat.deserialize(raw_data.get('forward_from_chat', {})) forward_from_message_id = raw_data.get('forward_from_message_id') + forward_signature = raw_data.get('forward_signature') forward_date = cls._parse_date(raw_data.get('forward_date', 0)) reply_to_message = Message.deserialize(raw_data.get('reply_to_message', {})) edit_date = cls._parse_date(raw_data.get('edit_date', 0)) + author_signature = raw_data.get('author_signature') text = raw_data.get('text') entities = MessageEntity.deserialize(raw_data.get('entities')) audio = Audio.deserialize(raw_data.get('audio')) @@ -142,11 +146,11 @@ class Message(Deserializable): content_type = ContentType.UNKNOWN[0] return Message(message_id, from_user, date, chat, forward_from, forward_from_chat, forward_from_message_id, - forward_date, reply_to_message, edit_date, text, entities, audio, document, game, photo, sticker, - video, voice, video_note, new_chat_members, caption, contact, location, venue, left_chat_member, - new_chat_title, new_chat_photo, delete_chat_photo, group_chat_created, supergroup_chat_created, - channel_chat_created, migrate_to_chat_id, migrate_from_chat_id, pinned_message, invoice, - successful_payment, content_type) + forward_signature, forward_date, reply_to_message, edit_date, author_signature, text, entities, + audio, document, game, photo, sticker, video, voice, video_note, new_chat_members, caption, + contact, location, venue, left_chat_member, new_chat_title, new_chat_photo, delete_chat_photo, + group_chat_created, supergroup_chat_created, channel_chat_created, migrate_to_chat_id, + migrate_from_chat_id, pinned_message, invoice, successful_payment, content_type) def is_command(self): """ diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 50507d71..165abf73 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -1,3 +1,5 @@ +from ..utils.markdown import link, hlink + try: import babel except ImportError: @@ -13,8 +15,9 @@ class User(Deserializable): https://core.telegram.org/bots/api#user """ - def __init__(self, id, first_name, last_name, username, language_code): + def __init__(self, id, is_bot, first_name, last_name, username, language_code): self.id: int = id + self.is_bot: bool = is_bot self.first_name: str = first_name self.last_name: str = last_name self.username: str = username @@ -23,12 +26,13 @@ class User(Deserializable): @classmethod def de_json(cls, raw_data: str or dict) -> 'User': id = raw_data.get('id') + is_bot = raw_data.get('is_bot') first_name = raw_data.get('first_name') last_name = raw_data.get('last_name') username = raw_data.get('username') language_code = raw_data.get('language_code') - return User(id, first_name, last_name, username, language_code) + return User(id, is_bot, first_name, last_name, username, language_code) @property def full_name(self): @@ -69,5 +73,16 @@ class User(Deserializable): setattr(self, '_locale', babel.core.Locale.parse(self.language_code, sep='-')) return getattr(self, '_locale') + @property + def url(self): + return f"tg://user?id={self.id}" + + def get_mention(self, name=None, as_html=False): + if name is None: + name = self.mention + if as_html: + return hlink(name, self.url) + return link(name, self.url) + async def get_user_profile_photos(self, offset=None, limit=None): return await self.bot.get_user_profile_photos(self.id, offset, limit) From 62417da6a794bc28f27bb0b461ae6577688a5f03 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 23 Aug 2017 22:55:40 +0300 Subject: [PATCH 09/10] Get mention in private chats. --- aiogram/types/chat.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 7e5ec498..aa6dd95e 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -1,3 +1,4 @@ +from aiogram.utils.markdown import hlink, link from .base import Deserializable from .chat_photo import ChatPhoto from ..utils.helper import Helper, HelperMode, Item @@ -65,6 +66,20 @@ class Chat(Deserializable): return self.full_name return None + @property + def user_url(self): + if self.type != ChatType.PRIVATE: + raise TypeError('This property available only in private chats.') + + return f"tg://user?id={self.id}" + + def get_mention(self, name=None, as_html=False): + if name is None: + name = self.mention + if as_html: + return hlink(name, self.user_url) + return link(name, self.user_url) + async def set_photo(self, photo): return await self.bot.set_chat_photo(self.id, photo) From e9a3ca6c8eb45112a53d95a9c216e66832404bd5 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 23 Aug 2017 23:27:03 +0300 Subject: [PATCH 10/10] Refactor filter checker. --- aiogram/dispatcher/filters.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/aiogram/dispatcher/filters.py b/aiogram/dispatcher/filters.py index d1968968..d62f5310 100644 --- a/aiogram/dispatcher/filters.py +++ b/aiogram/dispatcher/filters.py @@ -5,18 +5,13 @@ from ..utils.helper import Helper, HelperMode, Item async def check_filter(filter_, args, kwargs): - # TODO: Refactor that shit. + if not callable(filter_): + raise TypeError('Filter must be callable and/or awaitable!') - if any((inspect.isasyncgen(filter_), - inspect.iscoroutine(filter_), - inspect.isawaitable(filter_), - inspect.isasyncgenfunction(filter_), - inspect.iscoroutinefunction(filter_))): + if inspect.isawaitable(filter_) or inspect.iscoroutinefunction(filter_): return await filter_(*args, **kwargs) - elif callable(filter_): - return filter_(*args, **kwargs) else: - return True + return filter_(*args, **kwargs) async def check_filters(filters, args, kwargs):