From 7b71a72bdef58f107c786b334b89c141d161560b Mon Sep 17 00:00:00 2001 From: Olegt0rr Date: Sun, 18 Mar 2018 14:41:34 +0300 Subject: [PATCH 01/94] Split WEBHOOK and WEBAPP settings --- examples/webhook_example.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/webhook_example.py b/examples/webhook_example.py index 6a5317cf..1a4b8198 100644 --- a/examples/webhook_example.py +++ b/examples/webhook_example.py @@ -25,6 +25,12 @@ WEBHOOK_SSL_PRIV = './webhook_pkey.pem' # Path to the ssl private key WEBHOOK_URL = f"https://{WEBHOOK_HOST}:{WEBHOOK_PORT}{WEBHOOK_URL_PATH}" +# Web app settings: +# Use LAN address to listen webhooks +# User any available port in range from 1024 to 49151 if you're using proxy, or WEBHOOK_PORT if you're using direct webhook handling +WEBAPP_HOST = 'localhost' +WEBAPP_PORT = 3001 + BAD_CONTENT = ContentType.PHOTO & ContentType.DOCUMENT & ContentType.STICKER & ContentType.AUDIO loop = asyncio.get_event_loop() @@ -160,7 +166,7 @@ if __name__ == '__main__': context.load_cert_chain(WEBHOOK_SSL_CERT, WEBHOOK_SSL_PRIV) # Start web-application. - web.run_app(app, host=WEBHOOK_HOST, port=WEBHOOK_PORT, ssl_context=context) + web.run_app(app, host=WEBAPP_HOST, port=WEBAPP_PORT, ssl_context=context) # Note: # If you start your bot using nginx or Apache web server, SSL context is not required. # Otherwise you need to set `ssl_context` parameter. From 23fc14a7a42f453dfce9f409adc8ce22e5039367 Mon Sep 17 00:00:00 2001 From: Olegt0rr Date: Sun, 18 Mar 2018 23:54:52 +0300 Subject: [PATCH 02/94] web.run_app doesn't wait for loop kwarg Fix error: run_app() got an unexpected keyword argument 'loop' --- aiogram/utils/executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index ed8947eb..13f9fdf9 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -87,5 +87,5 @@ def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, app.on_startup.append(_wh_startup) app.on_shutdown.append(_wh_shutdown) - web.run_app(app, loop=loop, **kwargs) + web.run_app(app, **kwargs) return app From d2d8f4b51c4c953653a8b66c10402e39086914ba Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 19 Mar 2018 01:31:24 +0200 Subject: [PATCH 03/94] Change API version number. --- aiogram/bot/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 6b952f39..29a53606 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -161,7 +161,7 @@ class Methods(Helper): """ Helper for Telegram API Methods listed on https://core.telegram.org/bots/api - List is updated to Bot API 3.5 + List is updated to Bot API 3.6 """ mode = HelperMode.lowerCamelCase From 5467d91cf872e1d1046e7daf4bd4e73c343de4ad Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 19 Mar 2018 01:59:59 +0200 Subject: [PATCH 04/94] regexp filter for callback query #19 --- aiogram/dispatcher/filters.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/aiogram/dispatcher/filters.py b/aiogram/dispatcher/filters.py index 3b3b4d51..96ee47b0 100644 --- a/aiogram/dispatcher/filters.py +++ b/aiogram/dispatcher/filters.py @@ -2,7 +2,7 @@ import asyncio import inspect import re -from ..types import ContentType +from ..types import CallbackQuery, ContentType, Message from ..utils import context from ..utils.helper import Helper, HelperMode, Item @@ -127,9 +127,12 @@ class RegexpFilter(Filter): def __init__(self, regexp): self.regexp = re.compile(regexp, flags=re.IGNORECASE | re.MULTILINE) - def check(self, message): - if message.text: - return bool(self.regexp.search(message.text)) + def check(self, obj): + if isinstance(obj, Message) and obj.text: + return bool(self.regexp.search(obj.text)) + elif isinstance(obj, CallbackQuery) and obj.data: + return bool(self.regexp.search(obj.data)) + return False class RegexpCommandsFilter(AsyncFilter): @@ -168,7 +171,7 @@ class ContentTypeFilter(Filter): def check(self, message): return ContentType.ANY[0] in self.content_types or \ - message.content_type in self.content_types + message.content_type in self.content_types class CancelFilter(Filter): From c144156f43a339170be1d9fb2c7a9e62f7f9478a Mon Sep 17 00:00:00 2001 From: Olegt0rr Date: Sun, 25 Mar 2018 10:56:07 +0300 Subject: [PATCH 05/94] Swap around checking of VENUE and LOCATION content --- 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 5b267a6b..d3ee1833 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -95,10 +95,10 @@ class Message(base.TelegramObject): return ContentType.VOICE[0] if self.contact: return ContentType.CONTACT[0] - if self.location: - return ContentType.LOCATION[0] if self.venue: return ContentType.VENUE[0] + if self.location: + return ContentType.LOCATION[0] if self.new_chat_members: return ContentType.NEW_CHAT_MEMBERS[0] if self.left_chat_member: From c149a6a4902175b09aa1e0fbdc973d7d41a26b42 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 25 Mar 2018 14:26:42 +0300 Subject: [PATCH 06/94] Change version. --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index f7c17691..d957be85 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 2, 1, stage=Stage.DEV, build=0) +VERSION = Version(1, 2, 1, stage=Stage.FINAL, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version From c86f1470ba586a96912e84758b0024cbb660c118 Mon Sep 17 00:00:00 2001 From: Olegt0rr Date: Mon, 26 Mar 2018 00:23:24 +0300 Subject: [PATCH 07/94] InlineKeyboardMarkup insert bug fix --- aiogram/types/inline_keyboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/inline_keyboard.py b/aiogram/types/inline_keyboard.py index 7c859e6e..56094709 100644 --- a/aiogram/types/inline_keyboard.py +++ b/aiogram/types/inline_keyboard.py @@ -64,7 +64,7 @@ class InlineKeyboardMarkup(base.TelegramObject): :param button: """ - if self.inline_keyboard and len(self.inline_keyboard[-1] < self.row_width): + if self.inline_keyboard and len(self.inline_keyboard[-1]) < self.row_width: self.inline_keyboard[-1].append(button) else: self.add(button) From d0dc3fab8cb39ae3eba4327138c09e1d47540643 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 28 Mar 2018 20:46:55 +0300 Subject: [PATCH 08/94] WTF is going on? Fixed chat actions util. --- aiogram/types/chat.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index 1d6db5a9..f00cb861 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -521,7 +521,7 @@ class ChatActions(helper.Helper): :param sleep: sleep timeout :return: """ - await cls._do(cls.UPLOAD_PHOTO, sleep) + await cls._do(cls.RECORD_VIDEO, sleep) @classmethod async def upload_video(cls, sleep=None): @@ -531,7 +531,7 @@ class ChatActions(helper.Helper): :param sleep: sleep timeout :return: """ - await cls._do(cls.RECORD_VIDEO, sleep) + await cls._do(cls.UPLOAD_VIDEO, sleep) @classmethod async def record_audio(cls, sleep=None): @@ -541,7 +541,7 @@ class ChatActions(helper.Helper): :param sleep: sleep timeout :return: """ - await cls._do(cls.UPLOAD_VIDEO, sleep) + await cls._do(cls.RECORD_AUDIO, sleep) @classmethod async def upload_audio(cls, sleep=None): @@ -551,7 +551,7 @@ class ChatActions(helper.Helper): :param sleep: sleep timeout :return: """ - await cls._do(cls.RECORD_AUDIO, sleep) + await cls._do(cls.UPLOAD_AUDIO, sleep) @classmethod async def upload_document(cls, sleep=None): @@ -561,7 +561,7 @@ class ChatActions(helper.Helper): :param sleep: sleep timeout :return: """ - await cls._do(cls.UPLOAD_AUDIO, sleep) + await cls._do(cls.UPLOAD_DOCUMENT, sleep) @classmethod async def find_location(cls, sleep=None): @@ -571,7 +571,7 @@ class ChatActions(helper.Helper): :param sleep: sleep timeout :return: """ - await cls._do(cls.UPLOAD_DOCUMENT, sleep) + await cls._do(cls.FIND_LOCATION, sleep) @classmethod async def record_video_note(cls, sleep=None): @@ -581,7 +581,7 @@ class ChatActions(helper.Helper): :param sleep: sleep timeout :return: """ - await cls._do(cls.FIND_LOCATION, sleep) + await cls._do(cls.RECORD_VIDEO_NOTE, sleep) @classmethod async def upload_video_note(cls, sleep=None): @@ -591,4 +591,4 @@ class ChatActions(helper.Helper): :param sleep: sleep timeout :return: """ - await cls._do(cls.RECORD_VIDEO_NOTE, sleep) + await cls._do(cls.UPLOAD_VIDEO_NOTE, sleep) From 8cac7c33034552364440ca8d1c5efa69aa8c9e75 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 30 Mar 2018 19:06:52 +0300 Subject: [PATCH 09/94] Bump version number. --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index f7c17691..30e8ebd8 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 2, 1, stage=Stage.DEV, build=0) +VERSION = Version(1, 2, 2, stage=Stage.DEV, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version From 5571f3d4eccabefa7e8a1414a95b35e87b20eb93 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 30 Mar 2018 19:32:23 +0300 Subject: [PATCH 10/94] More detailed exceptions. --- aiogram/bot/api.py | 42 +++++++++- aiogram/dispatcher/storage.py | 16 +++- aiogram/utils/deprecated.py | 4 +- aiogram/utils/exceptions.py | 153 +++++++++++++++++++++++++++++++++- 4 files changed, 205 insertions(+), 10 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 29a53606..bb92f8b2 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -1,12 +1,12 @@ -import os import logging +import os from http import HTTPStatus import aiohttp from .. import types -from ..utils import json from ..utils import exceptions +from ..utils import json from ..utils.helper import Helper, HelperMode, Item # Main aiogram logger @@ -67,10 +67,48 @@ async def _check_result(method_name, response): elif 'migrate_to_chat_id' in result_json: raise exceptions.MigrateToChat(result_json['migrate_to_chat_id']) elif response.status == HTTPStatus.BAD_REQUEST: + if exceptions.MessageNotModified.check(description): + exceptions.MessageNotModified.throw() + elif exceptions.MessageToForwardNotFound.check(description): + exceptions.MessageToForwardNotFound.throw() + elif exceptions.MessageIdentifierNotSpecified.check(description): + exceptions.MessageIdentifierNotSpecified.throw() + elif exceptions.ChatNotFound.check(description): + exceptions.ChatNotFound.throw() + elif exceptions.InvalidQueryID.check(description): + exceptions.InvalidQueryID.throw() + elif exceptions.InvalidHTTPUrlContent.check(description): + exceptions.InvalidHTTPUrlContent.throw() + elif exceptions.GroupDeactivated.check(description): + exceptions.GroupDeactivated.throw() + elif exceptions.WrongFileIdentifier.check(description): + exceptions.WrongFileIdentifier.throw() + elif exceptions.InvalidPeerID.check(description): + exceptions.InvalidPeerID.throw() + elif exceptions.WebhookRequireHTTPS.check(description): + exceptions.WebhookRequireHTTPS.throw() + elif exceptions.BadWebhookPort.check(description): + exceptions.BadWebhookPort.throw() + elif exceptions.CantParseUrl.check(description): + exceptions.CantParseUrl.throw() raise exceptions.BadRequest(description) + elif response.status == HTTPStatus.NOT_FOUND: + if exceptions.MethodNotKnown.check(description): + exceptions.MethodNotKnown.throw() + raise exceptions.NotFound(description) elif response.status == HTTPStatus.CONFLICT: + if exceptions.TerminatedByOtherGetUpdates.match(description): + exceptions.TerminatedByOtherGetUpdates.throw() + if exceptions.CantGetUpdates.match(description): + exceptions.CantGetUpdates.throw() raise exceptions.ConflictError(description) elif response.status in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]: + if exceptions.BotKicked.match(description): + exceptions.BotKicked.throw() + elif exceptions.BotBlocked.match(description): + exceptions.BotBlocked.throw() + elif exceptions.UserDeactivated.match(description): + exceptions.UserDeactivated.throw() raise exceptions.Unauthorized(description) elif response.status == HTTPStatus.REQUEST_ENTITY_TOO_LARGE: raise exceptions.NetworkError('File too large for uploading. ' diff --git a/aiogram/dispatcher/storage.py b/aiogram/dispatcher/storage.py index b7e733d7..76a23ee6 100644 --- a/aiogram/dispatcher/storage.py +++ b/aiogram/dispatcher/storage.py @@ -1,5 +1,8 @@ import typing +from ..utils.deprecated import warn_deprecated as warn +from ..utils.exceptions import FSMStorageWarning + # Leak bucket KEY = 'key' LAST_CALL = 'called_at' @@ -324,22 +327,29 @@ class DisabledStorage(BaseStorage): chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Dict: + self._warn() return {} async def update_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None, **kwargs): - pass + self._warn() 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): - pass + self._warn() async def set_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, data: typing.Dict = None): - pass + self._warn() + + @staticmethod + def _warn(): + warn(f"You haven’t set any storage yet so no states and no data will be saved. \n" + f"You can connect MemoryStorage for debug purposes or non-essential data.", + FSMStorageWarning, 5) diff --git a/aiogram/utils/deprecated.py b/aiogram/utils/deprecated.py index baf837c7..1ea2561d 100644 --- a/aiogram/utils/deprecated.py +++ b/aiogram/utils/deprecated.py @@ -69,7 +69,7 @@ def deprecated(reason): raise TypeError(repr(type(reason))) -def warn_deprecated(message, warning=DeprecationWarning): +def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2): warnings.simplefilter('always', warning) - warnings.warn(message, category=warning, stacklevel=2) + warnings.warn(message, category=warning, stacklevel=stacklevel) warnings.simplefilter('default', warning) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index d0a40c0d..f7296eae 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -1,6 +1,42 @@ +""" +TelegramAPIError + ValidationError + Throttled + BadRequest + MessageError + MessageNotModified + MessageToForwardNotFound + MessageToDeleteNotFound + MessageIdentifierNotSpecified + ChatNotFound + InvalidQueryID + InvalidPeerID + InvalidHTTPUrlContent + WrongFileIdentifier + GroupDeactivated + BadWebhook + WebhookRequireHTTPS + BadWebhookPort + CantParseUrl + NotFound + MethodNotKnown + ConflictError + TerminatedByOtherGetUpdates + CantGetUpdates + Unauthorized + BotKicked + BotBlocked + UserDeactivated + NetworkError + RetryAfter + MigrateToChat + +AIOGramWarning + TimeoutWarning +""" import time -_PREFIXES = ['Error: ', '[Error]: ', 'Bad Request: ', 'Conflict: '] +_PREFIXES = ['Error: ', '[Error]: ', 'Bad Request: ', 'Conflict: ', 'Not Found: '] def _clean_message(text): @@ -11,10 +47,23 @@ def _clean_message(text): class TelegramAPIError(Exception): - def __init__(self, message): + def __init__(self, message=None): super(TelegramAPIError, self).__init__(_clean_message(message)) +class _MatchErrorMixin: + match = '' + text = None + + @classmethod + def check(cls, message): + return cls.match in message + + @classmethod + def throw(cls): + raise cls(cls.text or cls.match) + + class AIOGramWarning(Warning): pass @@ -23,6 +72,10 @@ class TimeoutWarning(AIOGramWarning): pass +class FSMStorageWarning(AIOGramWarning): + pass + + class ValidationError(TelegramAPIError): pass @@ -31,14 +84,108 @@ class BadRequest(TelegramAPIError): pass +class MessageError(BadRequest): + pass + + +class MessageNotModified(MessageError, _MatchErrorMixin): + match = 'message is not modified' + + +class MessageToForwardNotFound(MessageError, _MatchErrorMixin): + match = 'message to forward not found' + + +class MessageToDeleteNotFound(MessageError, _MatchErrorMixin): + match = 'message to delete not found' + + +class MessageIdentifierNotSpecified(MessageError, _MatchErrorMixin): + match = 'message identifier is not specified' + + +class ChatNotFound(BadRequest, _MatchErrorMixin): + match = 'chat not found' + + +class InvalidQueryID(BadRequest, _MatchErrorMixin): + match = 'QUERY_ID_INVALID' + text = 'Invalid query ID' + + +class InvalidPeerID(BadRequest, _MatchErrorMixin): + match = 'PEER_ID_INVALID' + text = 'Invalid peer ID' + + +class InvalidHTTPUrlContent(BadRequest, _MatchErrorMixin): + match = 'Failed to get HTTP URL content' + + +class WrongFileIdentifier(BadRequest, _MatchErrorMixin): + match = 'wrong file identifier/HTTP URL specified' + + +class GroupDeactivated(BadRequest, _MatchErrorMixin): + match = 'group is deactivated' + + +class BadWebhook(BadRequest): + pass + + +class WebhookRequireHTTPS(BadRequest, _MatchErrorMixin): + match = 'HTTPS url must be provided for webhook' + text = 'bad webhook: ' + match + + +class BadWebhookPort(BadRequest, _MatchErrorMixin): + match = 'Webhook can be set up only on ports 80, 88, 443 or 8443' + text = 'bad webhook: ' + match + + +class CantParseUrl(BadRequest, _MatchErrorMixin): + match = 'can\'t parse URL' + + +class NotFound(TelegramAPIError): + pass + + +class MethodNotKnown(NotFound, _MatchErrorMixin): + match = 'method not found' + + class ConflictError(TelegramAPIError): pass +class TerminatedByOtherGetUpdates(ConflictError, _MatchErrorMixin): + match = 'terminated by other getUpdates request' + text = 'Terminated by other getUpdates request; ' \ + 'Make sure that only one bot instance is running' + + +class CantGetUpdates(ConflictError, _MatchErrorMixin): + match = 'can\'t use getUpdates method while webhook is active' + + class Unauthorized(TelegramAPIError): pass +class BotKicked(Unauthorized, _MatchErrorMixin): + match = 'Bot was kicked from a chat' + + +class BotBlocked(Unauthorized, _MatchErrorMixin): + match = 'bot was blocked by the user' + + +class UserDeactivated(Unauthorized, _MatchErrorMixin): + match = 'user is deactivated' + + class NetworkError(TelegramAPIError): pass @@ -55,7 +202,7 @@ class MigrateToChat(TelegramAPIError): self.migrate_to_chat_id = chat_id -class Throttled(Exception): +class Throttled(TelegramAPIError): def __init__(self, **kwargs): from ..dispatcher.storage import DELTA, EXCEEDED_COUNT, KEY, LAST_CALL, RATE_LIMIT, RESULT self.key = kwargs.pop(KEY, '') From 210082691ab3385e22cf8d87ed33e1a98ef09411 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Tue, 3 Apr 2018 20:58:22 +0300 Subject: [PATCH 11/94] First pytests --- dev_requirements.txt | 2 ++ tests/test_bot.py | 67 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 93a20597..45b8e229 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -10,3 +10,5 @@ wheel>=0.30.0 rethinkdb>=2.3.0 sphinx>=1.6.6 sphinx-rtd-theme>=0.2.4 +aresponses +pytest diff --git a/tests/test_bot.py b/tests/test_bot.py index 9c0f860d..529fa34b 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1,4 +1,65 @@ -import aiogram +import aresponses +import pytest -# bot = aiogram.Bot('123456789:AABBCCDDEEFFaabbccddeeff-1234567890') -# TODO: mock for aiogram.bot.api.request and then test all AI methods. +from aiogram import Bot, types + +TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' + + +@pytest.yield_fixture() +@pytest.mark.asyncio +async def bot(event_loop): + """ Bot fixture """ + _bot = Bot(TOKEN, loop=event_loop) + yield _bot + await _bot.close() + + +@pytest.mark.asyncio +async def test_get_bot(bot, event_loop): + """ GetMe method test """ + _body = '{"ok":true,"result":{"id":492189143,"is_bot":true,' \ + '"first_name":"Dev Tester","username":"MiscDevTesterBot"}}' + _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'} + async with aresponses.ResponsesMockServer(loop=event_loop) as server: + server.add(server.ANY, response=server.Response(text=_body, + status=200, + reason='OK', + headers=_headers)) + bot_user = await bot.me + assert isinstance(bot_user, types.User) + + +@pytest.mark.asyncio +async def test_send_message(bot, event_loop): + """ SendMessage method test """ + message_text = 'Test message' + chat_id = -1234567890 + _body = """{"ok":true,"result":{"message_id":74,"from":{"id":492189143,"is_bot":true,"first_name":"Dev Tester", + "username":"MiscDevTesterBot"},"chat":{"id":66812456,"first_name":"O","username":"Oleg_Oleg_Oleg","type":"private"}, + "date":1522774794,"text":"Test message"}}""" + _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'} + + async with aresponses.ResponsesMockServer(loop=event_loop) as server: + server.add(server.ANY, response=server.Response(text=_body, + status=200, + reason='OK', + headers=_headers)) + msg: types.Message = await bot.send_message(chat_id=chat_id, text=message_text) + assert msg.text == message_text From 4ba810fe25d1acfb2a0d79fc60ca15c4bfc845a4 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Tue, 3 Apr 2018 22:38:46 +0300 Subject: [PATCH 12/94] Test dataset filling --- tests/test_bot.py | 2 +- tests/types/dataset.py | 392 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 360 insertions(+), 34 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 529fa34b..779ff10a 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -17,7 +17,7 @@ async def bot(event_loop): @pytest.mark.asyncio async def test_get_bot(bot, event_loop): - """ GetMe method test """ + """ getMe method test """ _body = '{"ok":true,"result":{"id":492189143,"is_bot":true,' \ '"first_name":"Dev Tester","username":"MiscDevTesterBot"}}' _headers = {'Server': 'nginx/1.12.2', diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 0a991a4a..8fb7109b 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -1,12 +1,17 @@ +"""" +Dict data set for Telegram message types +""" + USER = { "id": 12345678, "is_bot": False, "first_name": "FirstName", "last_name": "LastName", "username": "username", - "language_code": "ru-RU" + "language_code": "ru" } + CHAT = { "id": 12345678, "first_name": "FirstName", @@ -15,14 +20,23 @@ CHAT = { "type": "private" } -MESSAGE = { - "message_id": 11223, - "from": USER, - "chat": CHAT, - "date": 1508709711, - "text": "Hi, world!" + +PHOTO = { + "file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg", + "file_size": 1101, + "width": 90, + "height": 51 } + +AUDIO = { + "duration": 123, + "mime_type": "audio/mpeg3", + "file_id": "CQADAgdwadgawd0ChI_rXPyrAg", + "file_size": 12345678 +} + + DOCUMENT = { "file_name": "test.docx", "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -30,26 +44,6 @@ DOCUMENT = { "file_size": 21331 } -MESSAGE_WITH_DOCUMENT = { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508768012, - "document": DOCUMENT, - "caption": "doc description" -} - -UPDATE = { - "update_id": 128526, - "message": MESSAGE -} - -PHOTO = { - "file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg", - "file_size": 1101, - "width": 90, - "height": 51 -} ANIMATION = { "file_name": "a9b0e0ca537aa344338f80978f0896b7.gif.mp4", @@ -59,6 +53,7 @@ ANIMATION = { "file_size": 65837 } + GAME = { "title": "Karate Kido", "description": "No trees were harmed in the making of this game :)", @@ -66,10 +61,341 @@ GAME = { "animation": ANIMATION } -MESSAGE_WITH_GAME = { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508824810, - "game": GAME + +INVOICE = { + "title": "Working Time Machine", + "description": "Want to visit your great-great-great-grandparents? " + "Make a fortune at the races? " + "Shake hands with Hammurabi and take a stroll in the Hanging Gardens? " + "Order our Working Time Machine today!", + "start_parameter": "time-machine-example", + "currency": "USD", + "total_amount": 6250 +} + + +LOCATION = { + "latitude": 55.693416, + "longitude": 37.624605 +} + + +SHIPPING_ADDRESS = { + "country_code": "US", + "state": "State", + "city": "DefaultCity", + "street_line1": "Central", + "street_line2": "Middle", + "post_code": "424242" +} + + +STICKER = { + "width": 512, + "height": 512, + "emoji": "πŸ› ", + "set_name": "StickerSet", + "thumb": { + "file_id": "AAbbCCddEEffGGhh1234567890", + "file_size": 1234, + "width": 128, + "height": 128 + }, + "file_id": "AAbbCCddEEffGGhh1234567890", + "file_size": 12345 +} + + +SUCCESSFUL_PAYMENT = { + "currency": "USD", + "total_amount": 6250, + "invoice_payload": "HAPPY FRIDAYS COUPON", + "telegram_payment_charge_id": "_", + "provider_payment_charge_id": "12345678901234_test" +} + + +VIDEO = { + "duration": 52, + "width": 853, + "height": 480, + "mime_type": "video/quicktime", + "thumb": PHOTO, + "file_id": "BAADAgpAADdawy_JxS72kRvV3cortAg", + "file_size": 10099782 +} + + +VOICE = { + "duration": 1, + "mime_type": "audio/ogg", + "file_id": "AwADawAgADADy_JxS2gopIVIIxlhAg", + "file_size": 4321 +} + + +CALLBACK_QUERY = {} + + +CHANNEL_POST = {} + + +CHOSEN_INLINE_RESULT = {} + + +EDITED_CHANNEL_POST = {} + + +EDITED_MESSAGE = { + "update_id": 123456789, + "edited_message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508825372, + "edit_date": 1508825379, + "text": "hi there (edited)" + } +} + + +FORWARDED_MESSAGE = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508912492, + "forward_from": USER, + "forward_date": 1508912176, + "text": "message text" + } +} + + +INLINE_QUERY = {} + + +MESSAGE = { + "update_id": 128526, + "message": { + "message_id": 11223, + "from": USER, + "chat": CHAT, + "date": 1508709711, + "text": "Hi, world!" + } +} + + +MESSAGE_WITH_AUDIO = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508739776, + "audio": AUDIO + } +} + + +MESSAGE_WITH_AUTHOR_SIGNATURE = {} + + +MESSAGE_WITH_CHANNEL_CHAT_CREATED = {} + + +MESSAGE_WITH_CONTACT = {} + + +MESSAGE_WITH_DELETE_CHAT_PHOTO = {} + + +MESSAGE_WITH_DOCUMENT = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508768012, + "document": DOCUMENT, + "caption": "doc description" + } +} + + +MESSAGE_WITH_EDIT_DATE = {} + + +MESSAGE_WITH_ENTITIES = {} + + +MESSAGE_WITH_GAME = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508824810, + "game": GAME + } +} + + +MESSAGE_WITH_GROUP_CHAT_CREATED = {} + + +MESSAGE_WITH_INVOICE = { + "update_id": 123456789, + "message": { + "message_id": 9772, + "from": USER, + "chat": CHAT, + "date": 1508761719, + "invoice": INVOICE + } +} + + +MESSAGE_WITH_LEFT_CHAT_MEMBER = {} + + +MESSAGE_WITH_LOCATION = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508755473, + "location": LOCATION + } +} + + +MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = {} + + +MESSAGE_WITH_MIGRATE_TO_CHAT_ID = {} + + +MESSAGE_WITH_NEW_CHAT_MEMBERS = {} + + +MESSAGE_WITH_NEW_CHAT_PHOTO = {} + + +MESSAGE_WITH_NEW_CHAT_TITLE = {} + + +MESSAGE_WITH_PHOTO = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508825154, + "photo": [PHOTO, PHOTO, PHOTO, PHOTO], # 4 sizes of one photo + "caption": "photo description" + } +} + + +MESSAGE_WITH_PINNED_MESSAGE = {} + + +MESSAGE_WITH_REPLY_TO_MESSAGE = {} + + +MESSAGE_WITH_STICKER = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508771450, + "sticker": STICKER + } +} + + +MESSAGE_WITH_SUCCESSFUL_PAYMENT = { + "update_id": 167784957, + "message": { + "message_id": 9768, + "from": USER, + "chat": CHAT, + "date": 1508761169, + "successful_payment": SUCCESSFUL_PAYMENT + } +} + + +MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {} + + +MESSAGE_WITH_VENUE = {} + + +MESSAGE_WITH_VIDEO = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508756494, + "video": VIDEO, + "caption": "description" + } +} + + +MESSAGE_WITH_VIDEO_NOTE = {} + + +MESSAGE_WITH_VOICE = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508768403, + "voice": VOICE + } +} + + +PRE_CHECKOUT_QUERY = { + "update_id": 167784956, + "pre_checkout_query": { + "id": "262181558630368727", + "from": USER, + "currency": "USD", + "total_amount": 6250, + "invoice_payload": "HAPPY FRIDAYS COUPON" + } +} + + +REPLY_MESSAGE = { + "update_id": 123456789, + "message": { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508751866, + "reply_to_message": MESSAGE, + "text": "Reply to quoted message" + } +} + + +SHIPPING_QUERY = { + "update_id": 167784984, + "shipping_query": { + "id": "262181558684397422", + "from": USER, + "invoice_payload": "HAPPY FRIDAYS COUPON", + "shipping_address": SHIPPING_ADDRESS + } } From c3e77288661edecb2890c234739755e28eb91924 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 00:38:16 +0300 Subject: [PATCH 13/94] Pytest part two --- tests/test_bot.py | 96 +++++++++------- tests/types/dataset.py | 255 +++++++++++++---------------------------- 2 files changed, 134 insertions(+), 217 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 779ff10a..b26d6968 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -6,6 +6,33 @@ from aiogram import Bot, types TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' +class FakeTelegram(aresponses.ResponsesMockServer): + def __init__(self, message_dict, **kwargs): + super().__init__(**kwargs) + self._body, self._headers = self.parse_data(message_dict) + + async def __aenter__(self): + await super().__aenter__() + _response = self.Response(text=self._body, headers=self._headers, status=200, reason='OK') + self.add(self.ANY, response=_response) + + @staticmethod + def parse_data(message_dict): + import json + + _body = '{"ok":true,"result":' + json.dumps(message_dict) + '}' + _headers = {'Server': 'nginx/1.12.2', + 'Date': 'Tue, 03 Apr 2018 16:59:54 GMT', + 'Content-Type': 'application/json', + 'Content-Length': str(len(_body)), + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Expose-Headers': 'Content-Length,Content-Type,Date,Server,Connection', + 'Strict-Transport-Security': 'max-age=31536000; includeSubdomains'} + return _body, _headers + + @pytest.yield_fixture() @pytest.mark.asyncio async def bot(event_loop): @@ -16,50 +43,35 @@ async def bot(event_loop): @pytest.mark.asyncio -async def test_get_bot(bot, event_loop): +async def test_get_me(bot: Bot, event_loop): """ getMe method test """ - _body = '{"ok":true,"result":{"id":492189143,"is_bot":true,' \ - '"first_name":"Dev Tester","username":"MiscDevTesterBot"}}' - _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'} - async with aresponses.ResponsesMockServer(loop=event_loop) as server: - server.add(server.ANY, response=server.Response(text=_body, - status=200, - reason='OK', - headers=_headers)) - bot_user = await bot.me - assert isinstance(bot_user, types.User) + from .types.dataset import USER + user = types.User(**USER) + + async with FakeTelegram(message_dict=USER, loop=event_loop): + result = await bot.get_me() + assert result == user @pytest.mark.asyncio -async def test_send_message(bot, event_loop): - """ SendMessage method test """ - message_text = 'Test message' - chat_id = -1234567890 - _body = """{"ok":true,"result":{"message_id":74,"from":{"id":492189143,"is_bot":true,"first_name":"Dev Tester", - "username":"MiscDevTesterBot"},"chat":{"id":66812456,"first_name":"O","username":"Oleg_Oleg_Oleg","type":"private"}, - "date":1522774794,"text":"Test message"}}""" - _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'} +async def test_send_message(bot: Bot, event_loop): + """ sendMessage method test """ + from .types.dataset import MESSAGE + msg = types.Message(**MESSAGE) - async with aresponses.ResponsesMockServer(loop=event_loop) as server: - server.add(server.ANY, response=server.Response(text=_body, - status=200, - reason='OK', - headers=_headers)) - msg: types.Message = await bot.send_message(chat_id=chat_id, text=message_text) - assert msg.text == message_text + async with FakeTelegram(message_dict=MESSAGE, loop=event_loop): + result = await bot.send_message(chat_id=msg.chat.id, text=msg.text) + assert result == msg + + +@pytest.mark.asyncio +async def test_forward_message(bot: Bot, event_loop): + """ forwardMessage method test """ + from .types.dataset import FORWARDED_MESSAGE + msg = types.Message(**FORWARDED_MESSAGE) + from_chat = -1234567890 + + async with FakeTelegram(message_dict=FORWARDED_MESSAGE, loop=event_loop): + result = await bot.forward_message(chat_id=msg.chat.id, from_chat_id=from_chat, + message_id=msg.forward_from_message_id) + assert result == msg diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 8fb7109b..61c24563 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -11,7 +11,6 @@ USER = { "language_code": "ru" } - CHAT = { "id": 12345678, "first_name": "FirstName", @@ -20,7 +19,6 @@ CHAT = { "type": "private" } - PHOTO = { "file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg", "file_size": 1101, @@ -28,7 +26,6 @@ PHOTO = { "height": 51 } - AUDIO = { "duration": 123, "mime_type": "audio/mpeg3", @@ -36,7 +33,6 @@ AUDIO = { "file_size": 12345678 } - DOCUMENT = { "file_name": "test.docx", "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -44,7 +40,6 @@ DOCUMENT = { "file_size": 21331 } - ANIMATION = { "file_name": "a9b0e0ca537aa344338f80978f0896b7.gif.mp4", "mime_type": "video/mp4", @@ -53,7 +48,6 @@ ANIMATION = { "file_size": 65837 } - GAME = { "title": "Karate Kido", "description": "No trees were harmed in the making of this game :)", @@ -61,7 +55,6 @@ GAME = { "animation": ANIMATION } - INVOICE = { "title": "Working Time Machine", "description": "Want to visit your great-great-great-grandparents? " @@ -73,13 +66,11 @@ INVOICE = { "total_amount": 6250 } - LOCATION = { "latitude": 55.693416, "longitude": 37.624605 } - SHIPPING_ADDRESS = { "country_code": "US", "state": "State", @@ -89,7 +80,6 @@ SHIPPING_ADDRESS = { "post_code": "424242" } - STICKER = { "width": 512, "height": 512, @@ -105,7 +95,6 @@ STICKER = { "file_size": 12345 } - SUCCESSFUL_PAYMENT = { "currency": "USD", "total_amount": 6250, @@ -114,7 +103,6 @@ SUCCESSFUL_PAYMENT = { "provider_payment_charge_id": "12345678901234_test" } - VIDEO = { "duration": 52, "width": 853, @@ -125,7 +113,6 @@ VIDEO = { "file_size": 10099782 } - VOICE = { "duration": 1, "mime_type": "audio/ogg", @@ -133,22 +120,15 @@ VOICE = { "file_size": 4321 } - CALLBACK_QUERY = {} - CHANNEL_POST = {} - CHOSEN_INLINE_RESULT = {} - EDITED_CHANNEL_POST = {} - EDITED_MESSAGE = { - "update_id": 123456789, - "edited_message": { "message_id": 12345, "from": USER, "chat": CHAT, @@ -156,246 +136,171 @@ EDITED_MESSAGE = { "edit_date": 1508825379, "text": "hi there (edited)" } -} - FORWARDED_MESSAGE = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508912492, - "forward_from": USER, - "forward_date": 1508912176, - "text": "message text" - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508912492, + "forward_from": USER, + "forward_date": 1508912176, + "text": "message text" } - INLINE_QUERY = {} - MESSAGE = { - "update_id": 128526, - "message": { - "message_id": 11223, - "from": USER, - "chat": CHAT, - "date": 1508709711, - "text": "Hi, world!" - } + "message_id": 11223, + "from": USER, + "chat": CHAT, + "date": 1508709711, + "text": "Hi, world!" } - MESSAGE_WITH_AUDIO = { - "update_id": 123456789, - "message": { "message_id": 12345, "from": USER, "chat": CHAT, "date": 1508739776, "audio": AUDIO } -} - MESSAGE_WITH_AUTHOR_SIGNATURE = {} - MESSAGE_WITH_CHANNEL_CHAT_CREATED = {} - MESSAGE_WITH_CONTACT = {} - MESSAGE_WITH_DELETE_CHAT_PHOTO = {} - MESSAGE_WITH_DOCUMENT = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508768012, - "document": DOCUMENT, - "caption": "doc description" - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508768012, + "document": DOCUMENT, + "caption": "doc description" } - MESSAGE_WITH_EDIT_DATE = {} - MESSAGE_WITH_ENTITIES = {} - MESSAGE_WITH_GAME = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508824810, - "game": GAME - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508824810, + "game": GAME } - MESSAGE_WITH_GROUP_CHAT_CREATED = {} - MESSAGE_WITH_INVOICE = { - "update_id": 123456789, - "message": { - "message_id": 9772, - "from": USER, - "chat": CHAT, - "date": 1508761719, - "invoice": INVOICE - } + "message_id": 9772, + "from": USER, + "chat": CHAT, + "date": 1508761719, + "invoice": INVOICE } - MESSAGE_WITH_LEFT_CHAT_MEMBER = {} - MESSAGE_WITH_LOCATION = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508755473, - "location": LOCATION - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508755473, + "location": LOCATION } - MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = {} - MESSAGE_WITH_MIGRATE_TO_CHAT_ID = {} - MESSAGE_WITH_NEW_CHAT_MEMBERS = {} - MESSAGE_WITH_NEW_CHAT_PHOTO = {} - MESSAGE_WITH_NEW_CHAT_TITLE = {} - MESSAGE_WITH_PHOTO = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508825154, - "photo": [PHOTO, PHOTO, PHOTO, PHOTO], # 4 sizes of one photo - "caption": "photo description" - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508825154, + "photo": [PHOTO, PHOTO, PHOTO, PHOTO], # 4 sizes of one photo + "caption": "photo description" } - MESSAGE_WITH_PINNED_MESSAGE = {} - MESSAGE_WITH_REPLY_TO_MESSAGE = {} - MESSAGE_WITH_STICKER = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508771450, - "sticker": STICKER - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508771450, + "sticker": STICKER } - MESSAGE_WITH_SUCCESSFUL_PAYMENT = { - "update_id": 167784957, - "message": { - "message_id": 9768, - "from": USER, - "chat": CHAT, - "date": 1508761169, - "successful_payment": SUCCESSFUL_PAYMENT - } + "message_id": 9768, + "from": USER, + "chat": CHAT, + "date": 1508761169, + "successful_payment": SUCCESSFUL_PAYMENT } - MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {} - MESSAGE_WITH_VENUE = {} - MESSAGE_WITH_VIDEO = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508756494, - "video": VIDEO, - "caption": "description" - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508756494, + "video": VIDEO, + "caption": "description" } - MESSAGE_WITH_VIDEO_NOTE = {} - MESSAGE_WITH_VOICE = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508768403, - "voice": VOICE - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508768403, + "voice": VOICE } - PRE_CHECKOUT_QUERY = { - "update_id": 167784956, - "pre_checkout_query": { - "id": "262181558630368727", - "from": USER, - "currency": "USD", - "total_amount": 6250, - "invoice_payload": "HAPPY FRIDAYS COUPON" - } + "id": "262181558630368727", + "from": USER, + "currency": "USD", + "total_amount": 6250, + "invoice_payload": "HAPPY FRIDAYS COUPON" } - REPLY_MESSAGE = { - "update_id": 123456789, - "message": { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508751866, - "reply_to_message": MESSAGE, - "text": "Reply to quoted message" - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508751866, + "reply_to_message": MESSAGE, + "text": "Reply to quoted message" } - SHIPPING_QUERY = { - "update_id": 167784984, - "shipping_query": { - "id": "262181558684397422", - "from": USER, - "invoice_payload": "HAPPY FRIDAYS COUPON", - "shipping_address": SHIPPING_ADDRESS - } + "id": "262181558684397422", + "from": USER, + "invoice_payload": "HAPPY FRIDAYS COUPON", + "shipping_address": SHIPPING_ADDRESS +} + +UPDATE = { + "update_id": 123456789, + "message": MESSAGE } From 24a77883930a1126fda4f4394946f83656bb5d79 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 11:26:43 +0300 Subject: [PATCH 14/94] Added tests sendAudio, sendDocument. Added parse_mode descriptions to methods with caption --- aiogram/bot/bot.py | 18 +++++++++ tests/test_bot.py | 27 +++++++++++++- tests/types/dataset.py | 85 +++++++++++++++++++++++++++++++----------- 3 files changed, 108 insertions(+), 22 deletions(-) diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index f38de2c6..fefdef9f 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -249,6 +249,9 @@ class Bot(BaseBot): :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_to_message_id: If the message is a reply, ID of the original message @@ -295,6 +298,9 @@ class Bot(BaseBot): :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 @@ -343,6 +349,9 @@ class Bot(BaseBot): :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 your bot's message. + :type parse_mode: :obj:`typing.Union[base.String, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. :type disable_notification: :obj:`typing.Union[base.Boolean, None]` :param reply_to_message_id: If the message is a reply, ID of the original message @@ -394,6 +403,9 @@ class Bot(BaseBot): :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 your bot's message. + :type parse_mode: :obj:`typing.Union[base.String, None]` :param supports_streaming: Pass True, if the uploaded video is suitable for streaming :type supports_streaming: :obj:`typing.Union[base.Boolean, None]` :param disable_notification: Sends the message silently. Users will receive a notification with no sound. @@ -441,6 +453,9 @@ class Bot(BaseBot): :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 your bot's message. + :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. @@ -1305,6 +1320,9 @@ class Bot(BaseBot): :type inline_message_id: :obj:`typing.Union[base.String, None]` :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, diff --git a/tests/test_bot.py b/tests/test_bot.py index b26d6968..c297eacb 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -37,7 +37,7 @@ class FakeTelegram(aresponses.ResponsesMockServer): @pytest.mark.asyncio async def bot(event_loop): """ Bot fixture """ - _bot = Bot(TOKEN, loop=event_loop) + _bot = Bot(TOKEN, loop=event_loop, parse_mode=types.ParseMode.MARKDOWN) yield _bot await _bot.close() @@ -75,3 +75,28 @@ async def test_forward_message(bot: Bot, event_loop): result = await bot.forward_message(chat_id=msg.chat.id, from_chat_id=from_chat, message_id=msg.forward_from_message_id) assert result == msg + + +@pytest.mark.asyncio +async def test_send_audio(bot: Bot, event_loop): + """ sendAudio method test """ + from .types.dataset import MESSAGE_WITH_AUDIO + msg = types.Message(**MESSAGE_WITH_AUDIO) + + async with FakeTelegram(message_dict=MESSAGE_WITH_AUDIO, loop=event_loop): + result = await bot.send_audio(chat_id=msg.chat.id, audio=msg.audio.file_id, caption=msg.caption, + parse_mode=types.ParseMode.HTML, duration=msg.audio.duration, + performer=msg.audio.performer, title=msg.audio.title, disable_notification=False) + assert result == msg + + +@pytest.mark.asyncio +async def test_send_document(bot: Bot, event_loop): + """ sendDocument method test """ + from .types.dataset import MESSAGE_WITH_DOCUMENT + msg = types.Message(**MESSAGE_WITH_DOCUMENT) + + async with FakeTelegram(message_dict=MESSAGE_WITH_DOCUMENT, loop=event_loop): + result = await bot.send_document(chat_id=msg.chat.id, document=msg.document.file_id, caption=msg.caption, + parse_mode=types.ParseMode.HTML, disable_notification=False) + assert result == msg diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 61c24563..259f7980 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -27,10 +27,12 @@ PHOTO = { } AUDIO = { - "duration": 123, + "duration": 236, "mime_type": "audio/mpeg3", - "file_id": "CQADAgdwadgawd0ChI_rXPyrAg", - "file_size": 12345678 + "title": "The Best Song", + "performer": "The Best Singer", + "file_id": "CQADAgADbQEAAsnrIUpNoRRNsH7_hAI", + "file_size": 9507774 } DOCUMENT = { @@ -48,6 +50,43 @@ ANIMATION = { "file_size": 65837 } +ENTITY_BOLD = { + "offset": 5, + "length": 2, + "type": "bold" +} + +ENTITY_ITALIC = { + "offset": 8, + "length": 1, + "type": "italic" +} + +ENTITY_LINK = { + "offset": 10, + "length": 6, + "type": "text_link", + "url": "http://google.com/" +} + +ENTITY_CODE = { + "offset": 17, + "length": 7, + "type": "code" +} + +ENTITY_PRE = { + "offset": 30, + "length": 4, + "type": "pre" +} + +ENTITY_MENTION = { + "offset": 47, + "length": 9, + "type": "mention" +} + GAME = { "title": "Karate Kido", "description": "No trees were harmed in the making of this game :)", @@ -129,22 +168,25 @@ CHOSEN_INLINE_RESULT = {} EDITED_CHANNEL_POST = {} EDITED_MESSAGE = { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508825372, - "edit_date": 1508825379, - "text": "hi there (edited)" - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508825372, + "edit_date": 1508825379, + "text": "hi there (edited)" +} FORWARDED_MESSAGE = { "message_id": 12345, "from": USER, "chat": CHAT, - "date": 1508912492, - "forward_from": USER, - "forward_date": 1508912176, - "text": "message text" + "date": 1522828529, + "forward_from_chat": CHAT, + "forward_from_message_id": 123, + "forward_date": 1522749037, + "text": "Forwarded text with entities from public channel ", + "entities": [ENTITY_BOLD, ENTITY_CODE, ENTITY_ITALIC, ENTITY_LINK, + ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE] } INLINE_QUERY = {} @@ -158,12 +200,13 @@ MESSAGE = { } MESSAGE_WITH_AUDIO = { - "message_id": 12345, - "from": USER, - "chat": CHAT, - "date": 1508739776, - "audio": AUDIO - } + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508739776, + "audio": AUDIO, + "caption": "This is my favourite song" +} MESSAGE_WITH_AUTHOR_SIGNATURE = {} @@ -179,7 +222,7 @@ MESSAGE_WITH_DOCUMENT = { "chat": CHAT, "date": 1508768012, "document": DOCUMENT, - "caption": "doc description" + "caption": "Read my document" } MESSAGE_WITH_EDIT_DATE = {} From 0c3ce1cf00033706a355720608ea66e45188a0ad Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 12:42:01 +0300 Subject: [PATCH 15/94] Added tests sendPhoto --- tests/test_bot.py | 18 +++++++++++++++--- tests/types/dataset.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index c297eacb..8937f4fb 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -49,7 +49,7 @@ async def test_get_me(bot: Bot, event_loop): user = types.User(**USER) async with FakeTelegram(message_dict=USER, loop=event_loop): - result = await bot.get_me() + result = await bot.me assert result == user @@ -69,14 +69,26 @@ async def test_forward_message(bot: Bot, event_loop): """ forwardMessage method test """ from .types.dataset import FORWARDED_MESSAGE msg = types.Message(**FORWARDED_MESSAGE) - from_chat = -1234567890 async with FakeTelegram(message_dict=FORWARDED_MESSAGE, loop=event_loop): - result = await bot.forward_message(chat_id=msg.chat.id, from_chat_id=from_chat, + result = await bot.forward_message(chat_id=msg.chat.id, from_chat_id=msg.forward_from_chat.id, message_id=msg.forward_from_message_id) assert result == msg +@pytest.mark.asyncio +async def test_photo_message(bot: Bot, event_loop): + """ sendPhoto method test """ + from .types.dataset import MESSAGE_WITH_PHOTO, PHOTO + msg = types.Message(**MESSAGE_WITH_PHOTO) + photo = types.PhotoSize(**PHOTO) + + async with FakeTelegram(message_dict=MESSAGE_WITH_PHOTO, loop=event_loop): + result = await bot.send_photo(msg.chat.id, photo=photo.file_id, caption=msg.caption, + parse_mode=types.ParseMode.HTML, disable_notification=False) + assert result == msg + + @pytest.mark.asyncio async def test_send_audio(bot: Bot, event_loop): """ sendAudio method test """ diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 259f7980..8bc91037 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -272,7 +272,7 @@ MESSAGE_WITH_PHOTO = { "from": USER, "chat": CHAT, "date": 1508825154, - "photo": [PHOTO, PHOTO, PHOTO, PHOTO], # 4 sizes of one photo + "photo": [PHOTO, PHOTO, PHOTO, PHOTO], "caption": "photo description" } From c49d9c35fe291b96e72d829f03c613aa965c15fa Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 12:48:28 +0300 Subject: [PATCH 16/94] Added test sendVideo --- tests/test_bot.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 8937f4fb..7ab61d5f 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -112,3 +112,18 @@ async def test_send_document(bot: Bot, event_loop): result = await bot.send_document(chat_id=msg.chat.id, document=msg.document.file_id, caption=msg.caption, parse_mode=types.ParseMode.HTML, disable_notification=False) assert result == msg + + +@pytest.mark.asyncio +async def test_send_video(bot: Bot, event_loop): + """ sendVideo method test """ + from .types.dataset import MESSAGE_WITH_VIDEO, VIDEO + msg = types.Message(**MESSAGE_WITH_VIDEO) + video = types.Video(**VIDEO) + + async with FakeTelegram(message_dict=MESSAGE_WITH_VIDEO, loop=event_loop): + result = await bot.send_video(chat_id=msg.chat.id, video=video.file_id, duration=video.duration, + width=video.width, height=video.height, caption=msg.caption, + parse_mode=types.ParseMode.HTML, supports_streaming=True, + disable_notification=False) + assert result == msg From 7af8d2da94101adfb4da1de50ea93bc03c2d7a17 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 12:52:47 +0300 Subject: [PATCH 17/94] Added test sendVoice --- tests/test_bot.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 7ab61d5f..9726f995 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -127,3 +127,17 @@ async def test_send_video(bot: Bot, event_loop): parse_mode=types.ParseMode.HTML, supports_streaming=True, disable_notification=False) assert result == msg + + +@pytest.mark.asyncio +async def test_send_voice(bot: Bot, event_loop): + """ sendVoice method test """ + from .types.dataset import MESSAGE_WITH_VOICE, VOICE + msg = types.Message(**MESSAGE_WITH_VOICE) + voice = types.Voice(**VOICE) + + async with FakeTelegram(message_dict=MESSAGE_WITH_VOICE, loop=event_loop): + result = await bot.send_voice(chat_id=msg.chat.id, voice=voice.file_id, caption=msg.caption, + parse_mode=types.ParseMode.HTML, duration=voice.duration, + disable_notification=False) + assert result == msg From 5a7d01ae7137d45ba5430784140c34e6ab656b49 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 13:44:43 +0300 Subject: [PATCH 18/94] Added test sendVideoNote --- tests/test_bot.py | 14 ++++++++++++++ tests/types/dataset.py | 16 +++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 9726f995..ef8161ed 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -141,3 +141,17 @@ async def test_send_voice(bot: Bot, event_loop): parse_mode=types.ParseMode.HTML, duration=voice.duration, disable_notification=False) assert result == msg + + +@pytest.mark.asyncio +async def test_send_video_note(bot: Bot, event_loop): + """ sendVideoNote method test """ + from .types.dataset import MESSAGE_WITH_VIDEO_NOTE, VIDEO_NOTE + msg = types.Message(**MESSAGE_WITH_VIDEO_NOTE) + video_note = types.VideoNote(**VIDEO_NOTE) + + async with FakeTelegram(message_dict=MESSAGE_WITH_VIDEO_NOTE, loop=event_loop): + result = await bot.send_video_note(chat_id=msg.chat.id, video_note=video_note.file_id, + duration=video_note.duration, length=video_note.length, + disable_notification=False) + assert result == msg diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 8bc91037..6336a6e4 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -152,6 +152,14 @@ VIDEO = { "file_size": 10099782 } +VIDEO_NOTE = { + "duration": 4, + "length": 240, + "thumb": PHOTO, + "file_id": "AbCdEfGhIjKlMnOpQrStUvWxYz", + "file_size": 186562 +} + VOICE = { "duration": 1, "mime_type": "audio/ogg", @@ -309,7 +317,13 @@ MESSAGE_WITH_VIDEO = { "caption": "description" } -MESSAGE_WITH_VIDEO_NOTE = {} +MESSAGE_WITH_VIDEO_NOTE = { + "message_id": 55934, + "from": USER, + "chat": CHAT, + "date": 1522835890, + "video_note": VIDEO_NOTE +} MESSAGE_WITH_VOICE = { "message_id": 12345, From 6094fd13a3ccafa075e0c6f260ee51a185a655c9 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 13:51:18 +0300 Subject: [PATCH 19/94] Fixed test description (with file_id) --- tests/test_bot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index ef8161ed..ac0b99e6 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -78,7 +78,7 @@ async def test_forward_message(bot: Bot, event_loop): @pytest.mark.asyncio async def test_photo_message(bot: Bot, event_loop): - """ sendPhoto method test """ + """ sendPhoto method test with file_id """ from .types.dataset import MESSAGE_WITH_PHOTO, PHOTO msg = types.Message(**MESSAGE_WITH_PHOTO) photo = types.PhotoSize(**PHOTO) @@ -91,7 +91,7 @@ async def test_photo_message(bot: Bot, event_loop): @pytest.mark.asyncio async def test_send_audio(bot: Bot, event_loop): - """ sendAudio method test """ + """ sendAudio method test with file_id """ from .types.dataset import MESSAGE_WITH_AUDIO msg = types.Message(**MESSAGE_WITH_AUDIO) @@ -104,7 +104,7 @@ async def test_send_audio(bot: Bot, event_loop): @pytest.mark.asyncio async def test_send_document(bot: Bot, event_loop): - """ sendDocument method test """ + """ sendDocument method test with file_id """ from .types.dataset import MESSAGE_WITH_DOCUMENT msg = types.Message(**MESSAGE_WITH_DOCUMENT) @@ -116,7 +116,7 @@ async def test_send_document(bot: Bot, event_loop): @pytest.mark.asyncio async def test_send_video(bot: Bot, event_loop): - """ sendVideo method test """ + """ sendVideo method test with file_id """ from .types.dataset import MESSAGE_WITH_VIDEO, VIDEO msg = types.Message(**MESSAGE_WITH_VIDEO) video = types.Video(**VIDEO) @@ -131,7 +131,7 @@ async def test_send_video(bot: Bot, event_loop): @pytest.mark.asyncio async def test_send_voice(bot: Bot, event_loop): - """ sendVoice method test """ + """ sendVoice method test with file_id """ from .types.dataset import MESSAGE_WITH_VOICE, VOICE msg = types.Message(**MESSAGE_WITH_VOICE) voice = types.Voice(**VOICE) @@ -145,7 +145,7 @@ async def test_send_voice(bot: Bot, event_loop): @pytest.mark.asyncio async def test_send_video_note(bot: Bot, event_loop): - """ sendVideoNote method test """ + """ sendVideoNote method test with file_id """ from .types.dataset import MESSAGE_WITH_VIDEO_NOTE, VIDEO_NOTE msg = types.Message(**MESSAGE_WITH_VIDEO_NOTE) video_note = types.VideoNote(**VIDEO_NOTE) From b689a084edecb1415b7f9cd1817ff1c6850d0eb0 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 16:00:55 +0300 Subject: [PATCH 20/94] Add sendMediaGroup test --- tests/test_bot.py | 14 ++++++++++++++ tests/types/dataset.py | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index ac0b99e6..f219f5b2 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -155,3 +155,17 @@ async def test_send_video_note(bot: Bot, event_loop): duration=video_note.duration, length=video_note.length, disable_notification=False) assert result == msg + + +@pytest.mark.asyncio +async def test_send_media_group(bot: Bot, event_loop): + """ sendMediaGroup method test with file_id """ + from .types.dataset import MESSAGE_WITH_MEDIA_GROUP, PHOTO + msg = types.Message(**MESSAGE_WITH_MEDIA_GROUP) + photo = types.PhotoSize(**PHOTO) + media = [types.InputMediaPhoto(media=photo.file_id), types.InputMediaPhoto(media=photo.file_id)] + + async with FakeTelegram(message_dict=[MESSAGE_WITH_MEDIA_GROUP, MESSAGE_WITH_MEDIA_GROUP], loop=event_loop): + result = await bot.send_media_group(msg.chat.id, media=media, disable_notification=False) + assert len(result) == len(media) + assert result.pop().media_group_id diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 6336a6e4..270dc4cb 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -284,6 +284,15 @@ MESSAGE_WITH_PHOTO = { "caption": "photo description" } +MESSAGE_WITH_MEDIA_GROUP = { + "message_id": 55966, + "from": USER, + "chat": CHAT, + "date": 1522843665, + "media_group_id": "12182749320567362", + "photo": [PHOTO, PHOTO, PHOTO, PHOTO] +} + MESSAGE_WITH_PINNED_MESSAGE = {} MESSAGE_WITH_REPLY_TO_MESSAGE = {} From 21439b8532a5fcd4132b0a364cacd04527e0ada9 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 16:03:22 +0300 Subject: [PATCH 21/94] Correct test names --- tests/test_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index f219f5b2..11a638a9 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -77,7 +77,7 @@ async def test_forward_message(bot: Bot, event_loop): @pytest.mark.asyncio -async def test_photo_message(bot: Bot, event_loop): +async def test_send_photo(bot: Bot, event_loop): """ sendPhoto method test with file_id """ from .types.dataset import MESSAGE_WITH_PHOTO, PHOTO msg = types.Message(**MESSAGE_WITH_PHOTO) From d1cea909794cc958898840e0694d6bea91212883 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 16:31:48 +0300 Subject: [PATCH 22/94] Add sendLocation test --- tests/test_bot.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 11a638a9..d0776251 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -169,3 +169,16 @@ async def test_send_media_group(bot: Bot, event_loop): result = await bot.send_media_group(msg.chat.id, media=media, disable_notification=False) assert len(result) == len(media) assert result.pop().media_group_id + + +@pytest.mark.asyncio +async def test_send_location(bot: Bot, event_loop): + """ sendLocation method test with file_id """ + from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION + msg = types.Message(**MESSAGE_WITH_LOCATION) + location = types.Location(**LOCATION) + + async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop): + result = await bot.send_location(msg.chat.id, latitude=location.latitude, longitude=location.longitude, + live_period=10, disable_notification=False) + assert result == msg From e3da438d9f0502b3b795f110640e9b33bff5c878 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 16:48:43 +0300 Subject: [PATCH 23/94] Add editLocation and stopLocation test --- tests/test_bot.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index d0776251..13cb43ab 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -173,7 +173,7 @@ async def test_send_media_group(bot: Bot, event_loop): @pytest.mark.asyncio async def test_send_location(bot: Bot, event_loop): - """ sendLocation method test with file_id """ + """ sendLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) location = types.Location(**LOCATION) @@ -182,3 +182,40 @@ async def test_send_location(bot: Bot, event_loop): result = await bot.send_location(msg.chat.id, latitude=location.latitude, longitude=location.longitude, live_period=10, disable_notification=False) assert result == msg + + +@pytest.mark.asyncio +async def test_edit_message_live_location(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 bot message + async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop): + result = await bot.edit_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id, + latitude=location.latitude, longitude=location.longitude) + assert result == msg + + # 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, + latitude=location.latitude, longitude=location.longitude) + assert isinstance(result, bool) and result is True + + +@pytest.mark.asyncio +async def test_stop_message_live_location(bot: Bot, event_loop): + """ editMessageLiveLocation method test""" + from .types.dataset import MESSAGE_WITH_LOCATION + msg = types.Message(**MESSAGE_WITH_LOCATION) + + # stopping bot message + async with FakeTelegram(message_dict=MESSAGE_WITH_LOCATION, loop=event_loop): + result = await bot.stop_message_live_location(chat_id=msg.chat.id, message_id=msg.message_id) + assert result == msg + + # 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) + assert isinstance(result, bool) and result is True From 486978abe46b449be7b5fc01ce30a75888db1ae7 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 16:55:14 +0300 Subject: [PATCH 24/94] Add sendVenue test --- tests/test_bot.py | 19 +++++++++++++++++-- tests/types/dataset.py | 20 +++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 13cb43ab..c8fb6472 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -186,7 +186,7 @@ async def test_send_location(bot: Bot, event_loop): @pytest.mark.asyncio async def test_edit_message_live_location(bot: Bot, event_loop): - """ editMessageLiveLocation method test""" + """ editMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) location = types.Location(**LOCATION) @@ -206,7 +206,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): - """ editMessageLiveLocation method test""" + """ stopMessageLiveLocation method test """ from .types.dataset import MESSAGE_WITH_LOCATION msg = types.Message(**MESSAGE_WITH_LOCATION) @@ -219,3 +219,18 @@ async def test_stop_message_live_location(bot: Bot, event_loop): 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) assert isinstance(result, bool) and result is True + + +@pytest.mark.asyncio +async def test_send_venue(bot: Bot, event_loop): + """ sendVenue method test """ + from .types.dataset import MESSAGE_WITH_VENUE, VENUE, LOCATION + msg = types.Message(**MESSAGE_WITH_VENUE) + location = types.Location(**LOCATION) + venue = types.Venue(**VENUE) + + async with FakeTelegram(message_dict=MESSAGE_WITH_VENUE, loop=event_loop): + result = await bot.send_venue(msg.chat.id, latitude=location.latitude, longitude=location.longitude, + title=venue.title, address=venue.address, foursquare_id=venue.foursquare_id, + disable_notification=False) + assert result == msg diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 270dc4cb..d2a5eecd 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -106,8 +106,15 @@ INVOICE = { } LOCATION = { - "latitude": 55.693416, - "longitude": 37.624605 + "latitude": 50.693416, + "longitude": 30.624605 +} + +VENUE = { + "location": LOCATION, + "title": "Venue Name", + "address": "Venue Address", + "foursquare_id": "4e6f2cec483bad563d150f98" } SHIPPING_ADDRESS = { @@ -315,7 +322,14 @@ MESSAGE_WITH_SUCCESSFUL_PAYMENT = { MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {} -MESSAGE_WITH_VENUE = {} +MESSAGE_WITH_VENUE = { + "message_id": 56004, + "from": USER, + "chat": CHAT, + "date": 1522849819, + "location": LOCATION, + "venue": VENUE +} MESSAGE_WITH_VIDEO = { "message_id": 12345, From 3e77aa277583e82b89fbe65e463e999de35cd0aa Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 17:05:17 +0300 Subject: [PATCH 25/94] Add sendContact test --- tests/test_bot.py | 13 +++++++++++++ tests/types/dataset.py | 14 +++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index c8fb6472..8799cafe 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -234,3 +234,16 @@ async def test_send_venue(bot: Bot, event_loop): title=venue.title, address=venue.address, foursquare_id=venue.foursquare_id, disable_notification=False) assert result == msg + + +@pytest.mark.asyncio +async def test_send_contact(bot: Bot, event_loop): + """ sendContact method test """ + from .types.dataset import MESSAGE_WITH_CONTACT, CONTACT + msg = types.Message(**MESSAGE_WITH_CONTACT) + contact = types.Contact(**CONTACT) + + async with FakeTelegram(message_dict=MESSAGE_WITH_CONTACT, loop=event_loop): + result = await bot.send_contact(msg.chat.id, phone_number=contact.phone_number, first_name=contact.first_name, + last_name=contact.last_name, disable_notification=False) + assert result == msg diff --git a/tests/types/dataset.py b/tests/types/dataset.py index d2a5eecd..87f49620 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -35,6 +35,12 @@ AUDIO = { "file_size": 9507774 } +CONTACT = { + "phone_number": "88005553535", + "first_name": "John", + "last_name": "Smith", +} + DOCUMENT = { "file_name": "test.docx", "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", @@ -227,7 +233,13 @@ MESSAGE_WITH_AUTHOR_SIGNATURE = {} MESSAGE_WITH_CHANNEL_CHAT_CREATED = {} -MESSAGE_WITH_CONTACT = {} +MESSAGE_WITH_CONTACT = { + "message_id": 56006, + "from": USER, + "chat": CHAT, + "date": 1522850298, + "contact": CONTACT +} MESSAGE_WITH_DELETE_CHAT_PHOTO = {} From c0e4d480dd191794b8e93f860260181c1056b1d0 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 17:09:20 +0300 Subject: [PATCH 26/94] Add sendChatAction test --- tests/test_bot.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 8799cafe..e221a69d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -247,3 +247,15 @@ async def test_send_contact(bot: Bot, event_loop): result = await bot.send_contact(msg.chat.id, phone_number=contact.phone_number, first_name=contact.first_name, last_name=contact.last_name, disable_notification=False) assert result == msg + + +@pytest.mark.asyncio +async def test_send_chat_action(bot: Bot, event_loop): + """ sendChatAction method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.send_chat_action(chat_id=chat.id, action=types.ChatActions.TYPING) + assert isinstance(result, bool) + assert result is True From b95e977c120dc3d36484c0b9a9582c87d6cdb3f2 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 17:29:53 +0300 Subject: [PATCH 27/94] Add getUserProfilePhotos test --- tests/test_bot.py | 11 +++++++++++ tests/types/dataset.py | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index e221a69d..8480edf0 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -259,3 +259,14 @@ async def test_send_chat_action(bot: Bot, event_loop): result = await bot.send_chat_action(chat_id=chat.id, action=types.ChatActions.TYPING) assert isinstance(result, bool) assert result is True + + +@pytest.mark.asyncio +async def test_get_user_profile_photo(bot: Bot, event_loop): + """ getUserProfilePhotos method test """ + from .types.dataset import USER_PROFILE_PHOTOS, USER + user = types.User(**USER) + + async with FakeTelegram(message_dict=USER_PROFILE_PHOTOS, loop=event_loop): + result = await bot.get_user_profile_photos(user_id=user.id, offset=1, limit=1) + assert isinstance(result, types.UserProfilePhotos) diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 87f49620..bca005c4 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -392,6 +392,12 @@ SHIPPING_QUERY = { "shipping_address": SHIPPING_ADDRESS } +USER_PROFILE_PHOTOS = { + "total_count": 1, "photos": [ + [PHOTO, PHOTO, PHOTO] + ] +} + UPDATE = { "update_id": 123456789, "message": MESSAGE From 959f41a2f099648d72529156ee682ba80ce239cc Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 18:08:18 +0300 Subject: [PATCH 28/94] Add getFile test --- tests/test_bot.py | 11 +++++++++++ tests/types/dataset.py | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 8480edf0..780b2b02 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -270,3 +270,14 @@ async def test_get_user_profile_photo(bot: Bot, event_loop): async with FakeTelegram(message_dict=USER_PROFILE_PHOTOS, loop=event_loop): result = await bot.get_user_profile_photos(user_id=user.id, offset=1, limit=1) assert isinstance(result, types.UserProfilePhotos) + + +@pytest.mark.asyncio +async def test_get_file(bot: Bot, event_loop): + """ getFile method test """ + from .types.dataset import FILE + file = types.File(**FILE) + + async with FakeTelegram(message_dict=FILE, loop=event_loop): + result = await bot.get_file(file_id=file.file_id) + assert isinstance(result, types.File) diff --git a/tests/types/dataset.py b/tests/types/dataset.py index bca005c4..bc7d9920 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -398,6 +398,12 @@ USER_PROFILE_PHOTOS = { ] } +FILE = { + "file_id": "XXXYYYZZZ", + "file_size": 5254, + "file_path": "voice\/file_8" +} + UPDATE = { "update_id": 123456789, "message": MESSAGE From 90de8b2d4b3bbc50b05b6935d800b1c5f5065f78 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 18:11:03 +0300 Subject: [PATCH 29/94] Add kickChatMember test --- tests/test_bot.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 780b2b02..010b0be3 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -281,3 +281,16 @@ async def test_get_file(bot: Bot, event_loop): async with FakeTelegram(message_dict=FILE, loop=event_loop): result = await bot.get_file(file_id=file.file_id) assert isinstance(result, types.File) + + +@pytest.mark.asyncio +async def test_kick_chat_member(bot: Bot, event_loop): + """ kickChatMember method test """ + from .types.dataset import USER, CHAT + user = types.User(**USER) + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.kick_chat_member(chat_id=chat.id, user_id=user.id, until_date=123) + assert isinstance(result, bool) + assert result is True From 28484d9352289f234229ab693f93bd41bfd88346 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 18:12:21 +0300 Subject: [PATCH 30/94] Add unbanChatMember test --- tests/test_bot.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 010b0be3..59fad67c 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -294,3 +294,16 @@ async def test_kick_chat_member(bot: Bot, event_loop): result = await bot.kick_chat_member(chat_id=chat.id, user_id=user.id, until_date=123) assert isinstance(result, bool) assert result is True + + +@pytest.mark.asyncio +async def test_unban_chat_member(bot: Bot, event_loop): + """ unbanChatMember method test """ + from .types.dataset import USER, CHAT + user = types.User(**USER) + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.unban_chat_member(chat_id=chat.id, user_id=user.id) + assert isinstance(result, bool) + assert result is True From 3548febeaf70cab5fa7eb53e176bf0faa6fa20f1 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 18:14:42 +0300 Subject: [PATCH 31/94] Add restrictChatMember test --- tests/test_bot.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 59fad67c..0c3b0667 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -307,3 +307,21 @@ async def test_unban_chat_member(bot: Bot, event_loop): result = await bot.unban_chat_member(chat_id=chat.id, user_id=user.id) assert isinstance(result, bool) assert result is True + + +@pytest.mark.asyncio +async def test_restrict_chat_member(bot: Bot, event_loop): + """ restrictChatMember method test """ + from .types.dataset import USER, CHAT + user = types.User(**USER) + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.restrict_chat_member(chat_id=chat.id, user_id=user.id, + can_add_web_page_previews=False, + can_send_media_messages=False, + can_send_messages=False, + can_send_other_messages=False, + until_date=123) + assert isinstance(result, bool) + assert result is True From dd559da6f47973469c31ad39e6d18151021dfd82 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 18:17:43 +0300 Subject: [PATCH 32/94] Add promoteChatMember test --- tests/test_bot.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 0c3b0667..4a5aa358 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -317,11 +317,24 @@ async def test_restrict_chat_member(bot: Bot, event_loop): chat = types.Chat(**CHAT) async with FakeTelegram(message_dict=True, loop=event_loop): - result = await bot.restrict_chat_member(chat_id=chat.id, user_id=user.id, - can_add_web_page_previews=False, - can_send_media_messages=False, - can_send_messages=False, - can_send_other_messages=False, - until_date=123) + result = await bot.restrict_chat_member(chat_id=chat.id, user_id=user.id, can_add_web_page_previews=False, + can_send_media_messages=False, can_send_messages=False, + can_send_other_messages=False, until_date=123) + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_promote_chat_member(bot: Bot, event_loop): + """ promoteChatMember method test """ + from .types.dataset import USER, CHAT + user = types.User(**USER) + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.promote_chat_member(chat_id=chat.id, user_id=user.id, can_change_info=True, + can_delete_messages=True, can_edit_messages=True, + can_invite_users=True, can_pin_messages=True, can_post_messages=True, + can_promote_members=True, can_restrict_members=True) assert isinstance(result, bool) assert result is True From bd701c37681bd7100505ed68bec60679d410d87d Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 18:47:32 +0300 Subject: [PATCH 33/94] Add exportChatInviteLink test --- tests/test_bot.py | 11 +++++++++++ tests/types/dataset.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index 4a5aa358..a910ee6d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -338,3 +338,14 @@ async def test_promote_chat_member(bot: Bot, event_loop): can_promote_members=True, can_restrict_members=True) assert isinstance(result, bool) assert result is True + + +@pytest.mark.asyncio +async def test_export_chat_invite_link(bot: Bot, event_loop): + """ exportChatInviteLink method test """ + from .types.dataset import CHAT, INVITE_LINK + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=INVITE_LINK, loop=event_loop): + result = await bot.export_chat_invite_link(chat_id=chat.id) + assert result == INVITE_LINK diff --git a/tests/types/dataset.py b/tests/types/dataset.py index bc7d9920..ab5cc075 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -404,6 +404,8 @@ FILE = { "file_path": "voice\/file_8" } +INVITE_LINK = 'https://t.me/joinchat/AbCdEfjKILDADwdd123' + UPDATE = { "update_id": 123456789, "message": MESSAGE From e58e084219cebb8b3c31a836b73169668bac3215 Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 19:33:28 +0300 Subject: [PATCH 34/94] Add chat methods tests --- tests/test_bot.py | 145 +++++++++++++++++++++++++++++++++++++++++ tests/types/dataset.py | 18 +++++ 2 files changed, 163 insertions(+) diff --git a/tests/test_bot.py b/tests/test_bot.py index a910ee6d..ede5e4b6 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -349,3 +349,148 @@ async def test_export_chat_invite_link(bot: Bot, event_loop): async with FakeTelegram(message_dict=INVITE_LINK, loop=event_loop): result = await bot.export_chat_invite_link(chat_id=chat.id) assert result == INVITE_LINK + + +@pytest.mark.asyncio +async def test_delete_chat_photo(bot: Bot, event_loop): + """ deleteChatPhoto method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.delete_chat_photo(chat_id=chat.id) + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_set_chat_title(bot: Bot, event_loop): + """ setChatTitle method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.set_chat_title(chat_id=chat.id, title='Test title') + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_set_chat_description(bot: Bot, event_loop): + """ setChatDescription method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.set_chat_description(chat_id=chat.id, description='Test description') + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_pin_chat_message(bot: Bot, event_loop): + """ pinChatMessage method test """ + from .types.dataset import MESSAGE + message = types.Message(**MESSAGE) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.pin_chat_message(chat_id=message.chat.id, message_id=message.message_id, + disable_notification=False) + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_unpin_chat_message(bot: Bot, event_loop): + """ unpinChatMessage method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.unpin_chat_message(chat_id=chat.id) + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_leave_chat(bot: Bot, event_loop): + """ leaveChat method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.leave_chat(chat_id=chat.id) + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_get_chat(bot: Bot, event_loop): + """ getChat method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=CHAT, loop=event_loop): + result = await bot.get_chat(chat_id=chat.id) + assert result == chat + + +@pytest.mark.asyncio +async def test_get_chat_administrators(bot: Bot, event_loop): + """ getChatAdministrators method test """ + from .types.dataset import CHAT, CHAT_MEMBER + chat = types.Chat(**CHAT) + member = types.ChatMember(**CHAT_MEMBER) + + async with FakeTelegram(message_dict=[CHAT_MEMBER, CHAT_MEMBER], loop=event_loop): + result = await bot.get_chat_administrators(chat_id=chat.id) + assert result[0] == member + + +@pytest.mark.asyncio +async def test_get_chat_members_count(bot: Bot, event_loop): + """ getChatMembersCount method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + count = 5 + + async with FakeTelegram(message_dict=count, loop=event_loop): + result = await bot.get_chat_members_count(chat_id=chat.id) + assert result == count + + +@pytest.mark.asyncio +async def test_get_chat_member(bot: Bot, event_loop): + """ getChatMember method test """ + from .types.dataset import CHAT, CHAT_MEMBER + chat = types.Chat(**CHAT) + member = types.ChatMember(**CHAT_MEMBER) + + async with FakeTelegram(message_dict=CHAT_MEMBER, loop=event_loop): + result = await bot.get_chat_member(chat_id=chat.id, user_id=member.user.id) + assert isinstance(result, types.ChatMember) + assert result == member + + +@pytest.mark.asyncio +async def test_set_chat_sticker_set(bot: Bot, event_loop): + """ setChatStickerSet method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.set_chat_sticker_set(chat_id=chat.id, sticker_set_name='aiogram_stickers') + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_delete_chat_sticker_set(bot: Bot, event_loop): + """ setChatStickerSet method test """ + from .types.dataset import CHAT + chat = types.Chat(**CHAT) + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.delete_chat_sticker_set(chat_id=chat.id) + assert isinstance(result, bool) + assert result is True diff --git a/tests/types/dataset.py b/tests/types/dataset.py index ab5cc075..1333f178 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -35,6 +35,18 @@ AUDIO = { "file_size": 9507774 } +CHAT_MEMBER = { + "user": USER, + "status": "administrator", + "can_be_edited": False, + "can_change_info": True, + "can_delete_messages": True, + "can_invite_users": True, + "can_restrict_members": True, + "can_pin_messages": True, + "can_promote_members": False +} + CONTACT = { "phone_number": "88005553535", "first_name": "John", @@ -410,3 +422,9 @@ UPDATE = { "update_id": 123456789, "message": MESSAGE } + +WEBHOOK_INFO = { + "url": "", + "has_custom_certificate": False, + "pending_update_count": 0 +} From a1030145c947f9893212c3338748dfe306d7b0ce Mon Sep 17 00:00:00 2001 From: Oleg Abramov Date: Wed, 4 Apr 2018 20:09:01 +0300 Subject: [PATCH 35/94] Add editMessage and answerCallbackQ tests --- dev_requirements.txt | 2 +- tests/test_bot.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 45b8e229..d63ed3d1 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -11,4 +11,4 @@ rethinkdb>=2.3.0 sphinx>=1.6.6 sphinx-rtd-theme>=0.2.4 aresponses -pytest +tox \ No newline at end of file diff --git a/tests/test_bot.py b/tests/test_bot.py index ede5e4b6..02a393ac 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -494,3 +494,31 @@ async def test_delete_chat_sticker_set(bot: Bot, event_loop): result = await bot.delete_chat_sticker_set(chat_id=chat.id) assert isinstance(result, bool) assert result is True + + +@pytest.mark.asyncio +async def test_answer_callback_query(bot: Bot, event_loop): + """ answerCallbackQuery method test """ + + async with FakeTelegram(message_dict=True, loop=event_loop): + result = await bot.answer_callback_query(callback_query_id='QuERyId', text='Test Answer') + assert isinstance(result, bool) + assert result is True + + +@pytest.mark.asyncio +async def test_edit_message_text(bot: Bot, event_loop): + """ editMessageText method test """ + from .types.dataset import EDITED_MESSAGE + msg = types.Message(**EDITED_MESSAGE) + + # message by bot + async with FakeTelegram(message_dict=EDITED_MESSAGE, loop=event_loop): + result = await bot.edit_message_text(text=msg.text, chat_id=msg.chat.id, message_id=msg.message_id) + assert result == msg + + # 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) + assert isinstance(result, bool) + assert result is True From ce0d9aa3c6bd1c87147662bdd96f2c6cb668aca0 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 5 Apr 2018 23:11:09 +0300 Subject: [PATCH 36/94] Temporary fix "Coroutine 'BaseBot.close' was never awaited" --- 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 57dd5895..ba101624 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -68,7 +68,8 @@ class BaseBot: self.parse_mode = parse_mode def __del__(self): - asyncio.ensure_future(self.close()) + # asyncio.ensure_future(self.close()) + pass async def close(self): """ From d2232d127dc24b87105ebf42924ea609d66b9ae3 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 5 Apr 2018 23:23:31 +0300 Subject: [PATCH 37/94] Small changes in tests --- tests/test_bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 02a393ac..c3a29687 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -218,7 +218,8 @@ async def test_stop_message_live_location(bot: Bot, event_loop): # 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) - assert isinstance(result, bool) and result is True + assert isinstance(result, bool) + assert result is True @pytest.mark.asyncio @@ -445,6 +446,7 @@ async def test_get_chat_administrators(bot: Bot, event_loop): async with FakeTelegram(message_dict=[CHAT_MEMBER, CHAT_MEMBER], loop=event_loop): result = await bot.get_chat_administrators(chat_id=chat.id) assert result[0] == member + assert len(result) == 2 @pytest.mark.asyncio From 2a8eefc3afd198324a39abb78a9e690c14b65ad5 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 5 Apr 2018 23:24:57 +0300 Subject: [PATCH 38/94] Add `.pytest_cache/` to `.gitignore` --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 925f8dcd..6689369f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.pytest_cache/ .coverage .coverage.* .cache From 6a5393570a2a52f655f6d73036e0059550e6140f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 5 Apr 2018 23:27:05 +0300 Subject: [PATCH 39/94] Cleanup `.gitignore` --- .gitignore | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 6689369f..6c2a9404 100644 --- a/.gitignore +++ b/.gitignore @@ -44,37 +44,14 @@ coverage.xml # Sphinx documentation docs/_build/ -# pyenv -.python-version - # virtualenv .venv venv/ ENV/ -### JetBrains template -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - +# JetBrains .idea/ -# User-specific stuff: -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/dictionaries - -# Sensitive or high-churn files: -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.xml -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml - -## File-based project format: -*.iws - # Current project experiment.py From 2ba34454f49515e171c77b5daf5290b8c28a0cc2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 5 Apr 2018 23:28:18 +0300 Subject: [PATCH 40/94] Upd Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index aac5cbce..f21ec8ae 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ clean: find . -name '*.pyo' -exec $(RM) {} + find . -name '*~' -exec $(RM) {} + find . -name '__pycache__' -exec $(RM) {} + - $(RM) build/ dist/ docs/build/ .tox/ .cache/ *.egg-info + $(RM) build/ dist/ docs/build/ .tox/ .cache/ .pytest_cache/ *.egg-info tag: @echo "Add tag: '$(AIOGRAM_VERSION)'" From f0552ad6f57cfe190b3256b3227d2104e698a3c6 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 7 Apr 2018 01:52:26 +0300 Subject: [PATCH 41/94] Fix aiohttp ClientConnectorSSLError: SSL certificate verify failed #33 --- aiogram/bot/base.py | 10 +++++++--- requirements.txt | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index ba101624..e37d234b 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -1,8 +1,10 @@ import asyncio import io +import ssl from typing import Dict, List, Optional, Union import aiohttp +import certifi from . import api from ..types import ParseMode, base @@ -55,9 +57,11 @@ class BaseBot: self.loop = loop # aiohttp main session - self.session = aiohttp.ClientSession( - connector=aiohttp.TCPConnector(limit=connections_limit), - loop=self.loop, json_serialize=json.dumps) + ssl_context = ssl.create_default_context(cafile=certifi.where()) + connector = aiohttp.TCPConnector(limit=connections_limit, ssl_context=ssl_context, + loop=self.loop) + self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, + json_serialize=json.dumps, loop=self.loop) # Temp sessions self._temp_sessions = [] diff --git a/requirements.txt b/requirements.txt index 8a63284c..c5ac837e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ aiohttp>=2.3.5 Babel>=2.5.1 +certifi>=2018.01.18 From d2c5896b5c877f86895a6c5fe7feb170234181ed Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 7 Apr 2018 23:28:55 +0300 Subject: [PATCH 42/94] Oops. Remove duplicated argument. --- 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 e37d234b..56f56caa 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -61,7 +61,7 @@ class BaseBot: connector = aiohttp.TCPConnector(limit=connections_limit, ssl_context=ssl_context, loop=self.loop) self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, - json_serialize=json.dumps, loop=self.loop) + json_serialize=json.dumps) # Temp sessions self._temp_sessions = [] From e04c4d05daeffb97600515ac4ed6df8e63dad6de Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Apr 2018 16:42:47 +0300 Subject: [PATCH 43/94] Fix unwanted disabling webhook when skip_updates is enabled. --- aiogram/utils/executor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index cc53ba6e..62e1cd1e 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -11,15 +11,15 @@ 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: await dispatcher.reset_webhook(True) count = await dispatcher.skip_updates() if count: log.warning(f"Skipped {count} updates.") + if callable(callback): + await callback(dispatcher) + async def _wh_startup(app): callback = app.get('_startup_callback', None) From 5dacbdd2a5a1401083dc064195690327d581591f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Apr 2018 17:22:10 +0300 Subject: [PATCH 44/94] Returned back `InputFile.from_url(url)` method. --- aiogram/types/input_file.py | 83 +++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index ff5ab1f0..9b510b09 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -1,10 +1,15 @@ import io import logging import os +import time + +import aiohttp from . import base from ..bot import api +CHUNK_SIZE = 65536 + log = logging.getLogger('aiogram') @@ -76,6 +81,84 @@ class InputFile(base.TelegramObject): """ return self.file + @classmethod + async def from_url(cls, url, filename=None, chunk_size=65536): + """ + Download file from URL + + Manually is not required action. You can send urls instead! + + :param url: target URL + :param filename: optional. set custom file name + :param chunk_size: + + :return: InputFile + """ + conf = { + 'downloaded': True, + 'url': url + } + + # Let's do magic with the filename + if filename: + filename_prefix, _, ext = filename.rpartition('.') + file_suffix = '.' + ext if ext else '' + else: + filename_prefix, _, ext = url.rpartition('/')[-1].rpartition('.') + file_suffix = '.' + ext if ext else '' + filename = filename_prefix + file_suffix + + async with aiohttp.ClientSession() as session: + start = time.time() + async with session.get(url) as response: + # Save file in memory + file = await cls._process_stream(response, io.BytesIO(), chunk_size=chunk_size) + + log.debug(f"File successful downloaded at {round(time.time() - start, 2)} seconds from '{url}'") + return cls(file, filename, conf=conf) + + def save(self, filename, chunk_size=65536): + """ + Write file to disk + + :param filename: + :param chunk_size: + """ + with open(filename, 'wb') as fp: + while True: + # Chunk writer + data = self.file.read(chunk_size) + if not data: + break + fp.write(data) + # Flush all data + fp.flush() + + # Go to start of file. + if self.file.seekable(): + self.file.seek(0) + + @classmethod + async def _process_stream(cls, response, writer, chunk_size=65536): + """ + Transfer data + + :param response: + :param writer: + :param chunk_size: + :return: + """ + while True: + chunk = await response.content.read(chunk_size) + if not chunk: + break + writer.write(chunk) + + if writer.seekable(): + writer.seek(0) + + return writer + def to_python(self): raise TypeError('Object of this type is not exportable!') From 8fd45bbee41c557db29748ddd65345734a18762d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Apr 2018 17:23:24 +0300 Subject: [PATCH 45/94] Use CHUNK_SIZE. --- aiogram/types/input_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index 9b510b09..aaf0bf86 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -82,7 +82,7 @@ class InputFile(base.TelegramObject): return self.file @classmethod - async def from_url(cls, url, filename=None, chunk_size=65536): + async def from_url(cls, url, filename=None, chunk_size=CHUNK_SIZE): """ Download file from URL @@ -117,7 +117,7 @@ class InputFile(base.TelegramObject): log.debug(f"File successful downloaded at {round(time.time() - start, 2)} seconds from '{url}'") return cls(file, filename, conf=conf) - def save(self, filename, chunk_size=65536): + def save(self, filename, chunk_size=CHUNK_SIZE): """ Write file to disk From b808c48789aefce9f09c236f5065d57bfd4d6c49 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Apr 2018 17:23:58 +0300 Subject: [PATCH 46/94] Use CHUNK_SIZE. [2] --- aiogram/types/input_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index aaf0bf86..9d42c6b7 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -139,7 +139,7 @@ class InputFile(base.TelegramObject): self.file.seek(0) @classmethod - async def _process_stream(cls, response, writer, chunk_size=65536): + async def _process_stream(cls, response, writer, chunk_size=CHUNK_SIZE): """ Transfer data From f0de1c5761e3b89bbad33a7dca7d78bb38a0549d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Apr 2018 17:31:58 +0300 Subject: [PATCH 47/94] Add PhotoAsInputFileRequired exception. // Photo should be uploaded as an InputFile --- aiogram/bot/api.py | 2 ++ aiogram/utils/exceptions.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index bb92f8b2..123c5359 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -91,6 +91,8 @@ async def _check_result(method_name, response): exceptions.BadWebhookPort.throw() elif exceptions.CantParseUrl.check(description): exceptions.CantParseUrl.throw() + elif exceptions.PhotoAsInputFileRequired.check(description): + exceptions.PhotoAsInputFileRequired.throw() raise exceptions.BadRequest(description) elif response.status == HTTPStatus.NOT_FOUND: if exceptions.MethodNotKnown.check(description): diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index f7296eae..16fef497 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -130,6 +130,10 @@ class GroupDeactivated(BadRequest, _MatchErrorMixin): match = 'group is deactivated' +class PhotoAsInputFileRequired(BadRequest, _MatchErrorMixin): + match = 'Photo should be uploaded as an InputFile' + + class BadWebhook(BadRequest): pass From 967dbdaad34722e4b054e04194bc823d1bdee306 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Apr 2018 18:16:52 +0300 Subject: [PATCH 48/94] Descriptions of errors. --- aiogram/utils/exceptions.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 16fef497..b7bacb1e 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -20,6 +20,7 @@ TelegramAPIError CantParseUrl NotFound MethodNotKnown + PhotoAsInputFileRequired ConflictError TerminatedByOtherGetUpdates CantGetUpdates @@ -89,14 +90,23 @@ class MessageError(BadRequest): class MessageNotModified(MessageError, _MatchErrorMixin): + """ + Will be raised when you try to set new text is equals to current text. + """ match = 'message is not modified' class MessageToForwardNotFound(MessageError, _MatchErrorMixin): + """ + Will be raised when you try to forward very old or deleted or unknown message. + """ match = 'message to forward not found' class MessageToDeleteNotFound(MessageError, _MatchErrorMixin): + """ + Will be raised when you try to delete very old or deleted or unknown message. + """ match = 'message to delete not found' @@ -131,6 +141,9 @@ class GroupDeactivated(BadRequest, _MatchErrorMixin): class PhotoAsInputFileRequired(BadRequest, _MatchErrorMixin): + """ + Will be raised when you try to set chat photo from file ID. + """ match = 'Photo should be uploaded as an InputFile' From 15c85c53db5f387c766ecc712e77b46d2fd2bc4d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 8 Apr 2018 18:43:41 +0300 Subject: [PATCH 49/94] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 30e8ebd8..49193bed 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 2, 2, stage=Stage.DEV, build=0) +VERSION = Version(1, 2, 3, stage=Stage.DEV, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version From ca3aa504cf6169966855f3ab7a8e1efe77fced0f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 9 Apr 2018 00:30:38 +0300 Subject: [PATCH 50/94] Wow. Fail. Wrong method name. --- aiogram/bot/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 123c5359..4ef89d68 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -99,17 +99,17 @@ async def _check_result(method_name, response): exceptions.MethodNotKnown.throw() raise exceptions.NotFound(description) elif response.status == HTTPStatus.CONFLICT: - if exceptions.TerminatedByOtherGetUpdates.match(description): + if exceptions.TerminatedByOtherGetUpdates.check(description): exceptions.TerminatedByOtherGetUpdates.throw() - if exceptions.CantGetUpdates.match(description): + if exceptions.CantGetUpdates.check(description): exceptions.CantGetUpdates.throw() raise exceptions.ConflictError(description) elif response.status in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]: - if exceptions.BotKicked.match(description): + if exceptions.BotKicked.check(description): exceptions.BotKicked.throw() - elif exceptions.BotBlocked.match(description): + elif exceptions.BotBlocked.check(description): exceptions.BotBlocked.throw() - elif exceptions.UserDeactivated.match(description): + elif exceptions.UserDeactivated.check(description): exceptions.UserDeactivated.throw() raise exceptions.Unauthorized(description) elif response.status == HTTPStatus.REQUEST_ENTITY_TOO_LARGE: From 77867fe109b4034a968039facb7f56ce191b67a2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 9 Apr 2018 00:31:28 +0300 Subject: [PATCH 51/94] Use setuptools instead of distutils. (no) --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index be9694df..390a4d79 100755 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 import sys -from distutils.core import setup from warnings import warn from pip.req import parse_requirements -from setuptools import PackageFinder +from setuptools import PackageFinder, setup from aiogram import Stage, VERSION @@ -47,6 +46,7 @@ setup( name='aiogram', version=VERSION.version, packages=PackageFinder.find(exclude=('tests', 'tests.*', 'examples.*', 'docs',)), + requires_python='>=3.6', url='https://github.com/aiogram/aiogram', license='MIT', author='Alex Root Junior', From c0fcaa6f456bfd690e26610eb4922d6d2c2e61a7 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 14 Apr 2018 02:40:16 +0300 Subject: [PATCH 52/94] More API Exceptions. --- aiogram/bot/api.py | 5 +++++ aiogram/utils/exceptions.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 4ef89d68..7c4b35b7 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -60,6 +60,7 @@ async def _check_result(method_name, response): description = result_json.get('description') or body + # TODO: refactor the detection of error types if HTTPStatus.OK <= response.status <= HTTPStatus.IM_USED: return result_json.get('result') elif 'retry_after' in result_json: @@ -93,6 +94,8 @@ async def _check_result(method_name, response): exceptions.CantParseUrl.throw() elif exceptions.PhotoAsInputFileRequired.check(description): exceptions.PhotoAsInputFileRequired.throw() + elif exceptions.ToMuchMessages.check(description): + exceptions.ToMuchMessages.throw() raise exceptions.BadRequest(description) elif response.status == HTTPStatus.NOT_FOUND: if exceptions.MethodNotKnown.check(description): @@ -111,6 +114,8 @@ async def _check_result(method_name, response): exceptions.BotBlocked.throw() elif exceptions.UserDeactivated.check(description): exceptions.UserDeactivated.throw() + elif exceptions.CantInitiateConversation.check(description): + exceptions.UserDeactivated.throw() raise exceptions.Unauthorized(description) elif response.status == HTTPStatus.REQUEST_ENTITY_TOO_LARGE: raise exceptions.NetworkError('File too large for uploading. ' diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index b7bacb1e..7b52091e 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -21,6 +21,7 @@ TelegramAPIError NotFound MethodNotKnown PhotoAsInputFileRequired + ToMuchMessages ConflictError TerminatedByOtherGetUpdates CantGetUpdates @@ -28,6 +29,7 @@ TelegramAPIError BotKicked BotBlocked UserDeactivated + CantInitiateConversation NetworkError RetryAfter MigrateToChat @@ -147,6 +149,13 @@ class PhotoAsInputFileRequired(BadRequest, _MatchErrorMixin): match = 'Photo should be uploaded as an InputFile' +class ToMuchMessages(BadRequest, _MatchErrorMixin): + """ + Will be raised when you try to send media group with more than 10 items. + """ + match = 'Too much messages to send as an album' + + class BadWebhook(BadRequest): pass @@ -203,6 +212,10 @@ class UserDeactivated(Unauthorized, _MatchErrorMixin): match = 'user is deactivated' +class CantInitiateConversation(Unauthorized, _MatchErrorMixin): + match = 'bot can\'t initiate conversation with a user' + + class NetworkError(TelegramAPIError): pass From 8d3a43c17ddfee7f416b14f1e1d5897f47df1adf Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 14 Apr 2018 20:51:14 +0300 Subject: [PATCH 53/94] Fix `setup.py` for pip 10.0.0 --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 390a4d79..b4f22c4f 100755 --- a/setup.py +++ b/setup.py @@ -3,8 +3,11 @@ import sys from warnings import warn -from pip.req import parse_requirements from setuptools import PackageFinder, setup +try: + from pip.req import parse_requirements +except ModuleNotFoundError: # pip >= 10.0.0 + from pip._internal.req import parse_requirements from aiogram import Stage, VERSION @@ -46,7 +49,6 @@ setup( name='aiogram', version=VERSION.version, packages=PackageFinder.find(exclude=('tests', 'tests.*', 'examples.*', 'docs',)), - requires_python='>=3.6', url='https://github.com/aiogram/aiogram', license='MIT', author='Alex Root Junior', From 5bfbc1aec8c806f752a1867176be4eab3026467b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Apr 2018 03:51:56 +0300 Subject: [PATCH 54/94] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index efcb58b3..6eff73fe 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 2, 3, stage=Stage.FINAL, build=0) +VERSION = Version(1, 3, 0, stage=Stage.DEV, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version From 0de77cdea6b5552695b984e59dbb63ed7607ae94 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Apr 2018 04:02:01 +0300 Subject: [PATCH 55/94] Update `requirements.txt` --- dev_requirements.txt | 16 ++++++++-------- requirements.txt | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index d63ed3d1..469056b8 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,14 +1,14 @@ -r requirements.txt ujson>=1.35 -emoji>=0.4.5 -pytest>=3.3.0 +emoji>=0.5.0 +pytest>=3.5.0 pytest-asyncio>=0.8.0 uvloop>=0.9.1 -aioredis>=1.0.0 -wheel>=0.30.0 +aioredis>=1.1.0 +wheel>=0.31.0 rethinkdb>=2.3.0 -sphinx>=1.6.6 -sphinx-rtd-theme>=0.2.4 -aresponses -tox \ No newline at end of file +sphinx>=1.7.2 +sphinx-rtd-theme>=0.3.0 +aresponses>=1.0.0 +tox>=3.0.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c5ac837e..456d515a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -aiohttp>=2.3.5 -Babel>=2.5.1 +aiohttp>=3.1.3 +Babel>=2.5.3 certifi>=2018.01.18 From 41c764971be60aa937cb145204ef03abf5f3ee72 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Apr 2018 04:12:24 +0300 Subject: [PATCH 56/94] Use `find_packages()` instead of for `PackageFinder.find()` in `setup.py` --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b4f22c4f..e6485ca1 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,8 @@ import sys from warnings import warn -from setuptools import PackageFinder, setup +from setuptools import find_packages, setup + try: from pip.req import parse_requirements except ModuleNotFoundError: # pip >= 10.0.0 @@ -48,7 +49,7 @@ install_requires = get_requirements() setup( name='aiogram', version=VERSION.version, - packages=PackageFinder.find(exclude=('tests', 'tests.*', 'examples.*', 'docs',)), + packages=find_packages(exclude=('tests', 'tests.*', 'examples.*', 'docs',)), url='https://github.com/aiogram/aiogram', license='MIT', author='Alex Root Junior', From 70ad68cf4a3263afa06f091e3a13e16f69eeae60 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Apr 2018 04:26:31 +0300 Subject: [PATCH 57/94] Removed obsolete code --- aiogram/dispatcher/__init__.py | 20 -------------------- aiogram/utils/executor.py | 6 ------ 2 files changed, 26 deletions(-) diff --git a/aiogram/dispatcher/__init__.py b/aiogram/dispatcher/__init__.py index 8f89d4bf..edd09667 100644 --- a/aiogram/dispatcher/__init__.py +++ b/aiogram/dispatcher/__init__.py @@ -15,7 +15,6 @@ from .webhook import BaseResponse from ..bot import Bot from ..types.message import ContentType from ..utils import context -from ..utils.deprecated import deprecated from ..utils.exceptions import NetworkError, TelegramAPIError, Throttled log = logging.getLogger(__name__) @@ -200,17 +199,6 @@ class Dispatcher: return await self.bot.delete_webhook() - @deprecated('The old method was renamed to `start_polling`') - async def start_pooling(self, *args, **kwargs): - """ - Start long-lopping - - :param args: - :param kwargs: - :return: - """ - return await self.start_polling(*args, **kwargs) - async def start_polling(self, timeout=20, relax=0.1, limit=None, reset_webhook=None): """ Start long-polling @@ -276,10 +264,6 @@ class Dispatcher: except TelegramAPIError: log.exception('Cause exception while processing updates.') - @deprecated('The old method was renamed to `stop_polling`') - def stop_pooling(self): - return self.stop_polling() - def stop_polling(self): """ Break long-polling process. @@ -298,10 +282,6 @@ class Dispatcher: """ await asyncio.shield(self._close_waiter, loop=self.loop) - @deprecated('The old method was renamed to `is_polling`') - def is_pooling(self): - return self.is_polling() - def is_polling(self): """ Check if polling is enabled diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 62e1cd1e..51ecd924 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -1,7 +1,6 @@ from aiohttp import web from . import context -from .deprecated import deprecated from ..bot.api import log from ..dispatcher import Dispatcher from ..dispatcher.webhook import BOT_DISPATCHER_KEY, get_new_configured_app @@ -48,11 +47,6 @@ async def _wh_shutdown(app): await _shutdown(dispatcher, callback=callback) -@deprecated('The old function was renamed to `start_polling`') -def start_pooling(*args, **kwargs): - return start_polling(*args, **kwargs) - - def start_polling(dispatcher, *, loop=None, skip_updates=False, on_startup=None, on_shutdown=None): log.warning('Start bot with long-polling.') From 96042d13ecc6afe62cb3db65be326fbdb1ef36fc Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Apr 2018 06:18:49 +0300 Subject: [PATCH 58/94] Refactoring of executor util --- aiogram/utils/executor.py | 276 ++++++++++++++++++++++++++++---------- 1 file changed, 204 insertions(+), 72 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 51ecd924..b375f135 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -1,87 +1,219 @@ +import asyncio +import datetime +import functools +import secrets +from warnings import warn + from aiohttp import web from . import context from ..bot.api import log -from ..dispatcher import Dispatcher -from ..dispatcher.webhook import BOT_DISPATCHER_KEY, get_new_configured_app +from ..dispatcher.webhook import BOT_DISPATCHER_KEY, WebhookRequestHandler + +APP_EXECUTOR_KEY = 'APP_EXECUTOR' -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 skip_updates: - await dispatcher.reset_webhook(True) - count = await dispatcher.skip_updates() - if count: - log.warning(f"Skipped {count} updates.") - - if callable(callback): - await callback(dispatcher) - - -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) - - if dispatcher.is_polling(): - dispatcher.stop_polling() - # await dispatcher.wait_closed() - - await dispatcher.storage.close() - await dispatcher.storage.wait_closed() - - await dispatcher.bot.close() - - -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_polling(dispatcher, *, loop=None, skip_updates=False, - on_startup=None, on_shutdown=None): - log.warning('Start bot with long-polling.') - if loop is None: - loop = dispatcher.loop - - loop.set_task_factory(context.task_factory) - - try: - loop.run_until_complete(_startup(dispatcher, skip_updates, on_startup)) - loop.create_task(dispatcher.start_polling(reset_webhook=True)) - loop.run_forever() - except (KeyboardInterrupt, SystemExit): +def _setup_callbacks(executor, on_startup, on_shutdown): + if on_startup is None: pass - finally: - loop.run_until_complete(_shutdown(dispatcher, callback=on_shutdown)) - log.warning("Goodbye!") + elif callable(on_startup): + executor.on_startup(on_startup) + else: + for callback in on_startup: + executor.on_startup(callback) + + if on_shutdown is None: + pass + elif callable(on_shutdown): + executor.on_shutdown(on_shutdown) + else: + for callback in on_shutdown: + executor.on_shutdown(callback) + + +def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True, + on_startup=None, on_shutdown=None): + executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) + _setup_callbacks(executor, on_startup, on_shutdown) + + executor.start_polling(reset_webhook=reset_webhook) def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None, check_ip=False, **kwargs): - log.warning('Start bot with webhook.') - if loop is None: - loop = dispatcher.loop + executor = Executor(dispatcher, skip_updates=skip_updates, check_ip=check_ip, loop=loop) + _setup_callbacks(executor, on_startup, on_shutdown) - loop.set_task_factory(context.task_factory) + executor.start_webhook(webhook_path, **kwargs) - app = get_new_configured_app(dispatcher, webhook_path) - app['_startup_callback'] = on_startup - app['_shutdown_callback'] = on_shutdown - app['_skip_updates'] = skip_updates - app['_check_ip'] = check_ip - app.on_startup.append(_wh_startup) - app.on_shutdown.append(_wh_shutdown) +def start(dispatcher, func, *, loop=None, skip_updates=None, + on_startup=None, on_shutdown=None): + executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) + _setup_callbacks(executor, on_startup, on_shutdown) - web.run_app(app, **kwargs) - return app + executor.start(func) + + +class Executor: + def __init__(self, dispatcher, skip_updates=None, check_ip=False, loop=None): + if loop is None: + loop = dispatcher.loop + self.dispatcher = dispatcher + self.skip_updates = skip_updates + self.check_ip = check_ip + self.loop = loop + + self._identity = secrets.token_urlsafe(16) + self._web_app = None + + self._on_startup_webhook = [] + self._on_startup_polling = [] + self._on_shutdown_webhook = [] + self._on_shutdown_polling = [] + + self._freeze = False + + @property + def frozen(self): + return self._freeze + + def set_web_app(self, application: web.Application): + self._web_app = application + + def on_startup(self, callback: callable, polling=True, webhook=True): + self._check_frozen() + + if not webhook and not polling: + warn('This action has no effect!', UserWarning) + return + + if polling: + self._on_startup_polling.append(callback) + if webhook: + self._on_startup_webhook.append(callback) + + def on_shutdown(self, callback: callable, polling=True, webhook=True): + self._check_frozen() + + if not webhook and not polling: + warn('This action has no effect!', UserWarning) + return + + if polling: + self._on_shutdown_polling.append(callback) + if webhook: + self._on_shutdown_webhook.append(callback) + + def _check_frozen(self): + if self.frozen: + raise RuntimeError('Executor is frozen!') + + def _prepare_polling(self): + self._check_frozen() + self._freeze = True + + self.loop.set_task_factory(context.task_factory) + + def _prepare_webhook(self, path=None, handler=WebhookRequestHandler): + self._check_frozen() + self._freeze = True + + self.loop.set_task_factory(context.task_factory) + + app = self._web_app + if app is None: + self._web_app = app = web.Application() + app[BOT_DISPATCHER_KEY] = self.dispatcher + + if self._identity in self._identity: + # App is already configured + return + + if path is not None: + app.router.add_route('*', path, handler, name='webhook_handler') + + async def _wrap_callback(cb, _): + return await cb(self.dispatcher) + + for callback in self._on_startup_webhook: + app.on_startup.append(functools.partial(_wrap_callback, callback)) + for callback in self._on_shutdown_webhook: + app.on_shutdown.append(functools.partial(_wrap_callback, callback)) + + app[APP_EXECUTOR_KEY] = self + app[BOT_DISPATCHER_KEY] = self.dispatcher + app[self._identity] = datetime.datetime.now() + app['_check_ip'] = self.check_ip + + def start_webhook(self, webhook_path=None, request_handler=WebhookRequestHandler, **kwargs): + self._prepare_webhook(webhook_path, request_handler) + self.loop.run_until_complete(self._startup_webhook()) + web.run_app(self._web_app, **kwargs) + + def start_polling(self, reset_webhook=None): + self._prepare_polling() + loop: asyncio.AbstractEventLoop = self.loop + + try: + loop.run_until_complete(self._startup_polling()) + loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook)) + loop.run_forever() + except (KeyboardInterrupt, SystemExit): + loop.stop() + finally: + loop.run_until_complete(self._shutdown_polling()) + log.warning("Goodbye!") + + def start(self, func): + self._check_frozen() + self._freeze = True + loop: asyncio.AbstractEventLoop = self.loop + + try: + loop.run_until_complete(self._startup_polling()) + loop.run_until_complete(func) + except (KeyboardInterrupt, SystemExit): + loop.stop() + finally: + loop.run_until_complete(self._shutdown_polling()) + log.warning("Goodbye!") + + async def _skip_updates(self): + await self.dispatcher.reset_webhook(True) + count = await self.dispatcher.skip_updates() + if count: + log.warning(f"Skipped {count} updates.") + return count + + async def _welcome(self): + user = await self.dispatcher.bot.me + log.info(f"Bot: {user.full_name} [@{user.username}]") + + async def _shutdown(self): + self.dispatcher.stop_polling() + await self.dispatcher.storage.close() + await self.dispatcher.storage.wait_closed() + await self.dispatcher.bot.close() + + async def _startup_polling(self): + await self._welcome() + + if self.skip_updates: + await self._skip_updates() + for callback in self._on_startup_polling: + await callback(self.dispatcher) + + async def _shutdown_polling(self, wait_closed=False): + await self._shutdown() + + for callback in self._on_shutdown_polling: + await callback(self.dispatcher) + + if wait_closed: + await self.dispatcher.wait_closed() + + async def _startup_webhook(self): + await self._welcome() + if self.skip_updates: + self._skip_updates() From 0479c24e5a3e5651207b511f3c4d5fea93abd43f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Apr 2018 06:32:44 +0300 Subject: [PATCH 59/94] Optimize registering of callbacks --- aiogram/utils/executor.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index b375f135..26d2eb75 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -14,21 +14,10 @@ APP_EXECUTOR_KEY = 'APP_EXECUTOR' def _setup_callbacks(executor, on_startup, on_shutdown): - if on_startup is None: - pass - elif callable(on_startup): + if on_startup is not None: executor.on_startup(on_startup) - else: - for callback in on_startup: - executor.on_startup(callback) - - if on_shutdown is None: - pass - elif callable(on_shutdown): + if on_shutdown is not None: executor.on_shutdown(on_shutdown) - else: - for callback in on_shutdown: - executor.on_shutdown(callback) def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True, @@ -83,11 +72,15 @@ class Executor: def on_startup(self, callback: callable, polling=True, webhook=True): self._check_frozen() - if not webhook and not polling: warn('This action has no effect!', UserWarning) return + if isinstance(callback, (list, tuple, set)): + for cb in callback: + self.on_startup(cb, polling, webhook) + return + if polling: self._on_startup_polling.append(callback) if webhook: @@ -95,11 +88,15 @@ class Executor: def on_shutdown(self, callback: callable, polling=True, webhook=True): self._check_frozen() - if not webhook and not polling: warn('This action has no effect!', UserWarning) return + if isinstance(callback, (list, tuple, set)): + for cb in callback: + self.on_shutdown(cb, polling, webhook) + return + if polling: self._on_shutdown_polling.append(callback) if webhook: From bcb629cf9aec8dde25ac8eb7c6d4264dc26741ec Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 15 Apr 2018 18:44:15 +0300 Subject: [PATCH 60/94] More pydocs in `utils.executor` module. --- aiogram/utils/executor.py | 85 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 26d2eb75..9fa7121c 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -13,7 +13,7 @@ from ..dispatcher.webhook import BOT_DISPATCHER_KEY, WebhookRequestHandler APP_EXECUTOR_KEY = 'APP_EXECUTOR' -def _setup_callbacks(executor, on_startup, on_shutdown): +def _setup_callbacks(executor, on_startup=None, on_shutdown=None): if on_startup is not None: executor.on_startup(on_startup) if on_shutdown is not None: @@ -22,6 +22,16 @@ def _setup_callbacks(executor, on_startup, on_shutdown): def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True, on_startup=None, on_shutdown=None): + """ + Start bot in long-polling mode + + :param dispatcher: + :param loop: + :param skip_updates: + :param reset_webhook: + :param on_startup: + :param on_shutdown: + """ executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) _setup_callbacks(executor, on_startup, on_shutdown) @@ -30,6 +40,19 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None, check_ip=False, **kwargs): + """ + Start bot in webhook mode + + :param dispatcher: + :param webhook_path: + :param loop: + :param skip_updates: + :param on_startup: + :param on_shutdown: + :param check_ip: + :param kwargs: + :return: + """ executor = Executor(dispatcher, skip_updates=skip_updates, check_ip=check_ip, loop=loop) _setup_callbacks(executor, on_startup, on_shutdown) @@ -38,6 +61,17 @@ def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, def start(dispatcher, func, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None): + """ + Execute function + + :param dispatcher: + :param func: + :param loop: + :param skip_updates: + :param on_startup: + :param on_shutdown: + :return: + """ executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) _setup_callbacks(executor, on_startup, on_shutdown) @@ -45,6 +79,10 @@ def start(dispatcher, func, *, loop=None, skip_updates=None, class Executor: + """ + Main executor class + """ + def __init__(self, dispatcher, skip_updates=None, check_ip=False, loop=None): if loop is None: loop = dispatcher.loop @@ -68,9 +106,27 @@ class Executor: return self._freeze def set_web_app(self, application: web.Application): + """ + Change instance of aiohttp.web.Applicaton + + :param application: + """ self._web_app = application + @property + def web_app(self) -> web.Application: + if self._web_app is None: + raise RuntimeError('web.Application() is not configured!') + return self._web_app + def on_startup(self, callback: callable, polling=True, webhook=True): + """ + Register a callback for the startup process + + :param callback: + :param polling: use with polling + :param webhook: use with webhook + """ self._check_frozen() if not webhook and not polling: warn('This action has no effect!', UserWarning) @@ -87,6 +143,13 @@ class Executor: self._on_startup_webhook.append(callback) def on_shutdown(self, callback: callable, polling=True, webhook=True): + """ + Register a callback for the shutdown process + + :param callback: + :param polling: use with polling + :param webhook: use with webhook + """ self._check_frozen() if not webhook and not polling: warn('This action has no effect!', UserWarning) @@ -121,7 +184,6 @@ class Executor: app = self._web_app if app is None: self._web_app = app = web.Application() - app[BOT_DISPATCHER_KEY] = self.dispatcher if self._identity in self._identity: # App is already configured @@ -144,11 +206,24 @@ class Executor: app['_check_ip'] = self.check_ip def start_webhook(self, webhook_path=None, request_handler=WebhookRequestHandler, **kwargs): + """ + Start bot in webhook mode + + :param webhook_path: + :param request_handler: + :param kwargs: + :return: + """ self._prepare_webhook(webhook_path, request_handler) self.loop.run_until_complete(self._startup_webhook()) web.run_app(self._web_app, **kwargs) def start_polling(self, reset_webhook=None): + """ + Start bot in long-polling mode + + :param reset_webhook: + """ self._prepare_polling() loop: asyncio.AbstractEventLoop = self.loop @@ -163,6 +238,12 @@ class Executor: log.warning("Goodbye!") def start(self, func): + """ + Execute function + + :param func: + :return: + """ self._check_frozen() self._freeze = True loop: asyncio.AbstractEventLoop = self.loop From e07115a09fde68351a47648af99709e341120396 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 16 Apr 2018 00:36:14 +0300 Subject: [PATCH 61/94] Add InvalidStickersSet exception --- aiogram/bot/api.py | 2 ++ aiogram/utils/exceptions.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 7c4b35b7..443b5a2e 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -96,6 +96,8 @@ async def _check_result(method_name, response): exceptions.PhotoAsInputFileRequired.throw() elif exceptions.ToMuchMessages.check(description): exceptions.ToMuchMessages.throw() + elif exceptions.InvalidStickersSet.check(description): + exceptions.InvalidStickersSet.throw() raise exceptions.BadRequest(description) elif response.status == HTTPStatus.NOT_FOUND: if exceptions.MethodNotKnown.check(description): diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 7b52091e..f9676f1b 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -22,6 +22,7 @@ TelegramAPIError MethodNotKnown PhotoAsInputFileRequired ToMuchMessages + InvalidStickersSet ConflictError TerminatedByOtherGetUpdates CantGetUpdates @@ -156,6 +157,11 @@ class ToMuchMessages(BadRequest, _MatchErrorMixin): match = 'Too much messages to send as an album' +class InvalidStickersSet(BadRequest, _MatchErrorMixin): + match = 'STICKERSET_INVALID' + text = 'Stickers set is invalid' + + class BadWebhook(BadRequest): pass From f047bb6db31603ff2a9adbba5555ef9df032fef7 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 19 Apr 2018 03:28:34 +0300 Subject: [PATCH 62/94] Implemented support of Socks5 Proxy --- aiogram/bot/base.py | 16 ++++++++++++---- dev_requirements.txt | 3 ++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 56f56caa..f7e4ba83 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -58,10 +58,18 @@ class BaseBot: # aiohttp main session ssl_context = ssl.create_default_context(cafile=certifi.where()) - connector = aiohttp.TCPConnector(limit=connections_limit, ssl_context=ssl_context, - loop=self.loop) - self.session = aiohttp.ClientSession(connector=connector, loop=self.loop, - json_serialize=json.dumps) + + if isinstance(proxy, str) and proxy.startswith('socks5://'): + from aiosocksy.connector import ProxyClientRequest, ProxyConnector + connector = ProxyConnector(limit=connections_limit, ssl_context=ssl_context, loop=self.loop) + request_class = ProxyClientRequest + else: + connector = aiohttp.TCPConnector(limit=connections_limit, ssl_context=ssl_context, + loop=self.loop) + request_class = aiohttp.ClientRequest + + self.session = aiohttp.ClientSession(connector=connector, request_class=request_class, + loop=self.loop, json_serialize=json.dumps) # Temp sessions self._temp_sessions = [] diff --git a/dev_requirements.txt b/dev_requirements.txt index 469056b8..a66ce75d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -11,4 +11,5 @@ rethinkdb>=2.3.0 sphinx>=1.7.2 sphinx-rtd-theme>=0.3.0 aresponses>=1.0.0 -tox>=3.0.0 \ No newline at end of file +tox>=3.0.0 +aiosocksy>=0.1 From 9e97cd6727f3b0741e07455429bee258437c1bd5 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 19 Apr 2018 03:32:38 +0300 Subject: [PATCH 63/94] destroy_temp_session is awaitable --- aiogram/bot/base.py | 6 +++--- examples/proxy_and_emojize.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index f7e4ba83..3c93d136 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -110,7 +110,7 @@ class BaseBot: self._temp_sessions.append(session) return session - def destroy_temp_session(self, session: aiohttp.ClientSession): + async def destroy_temp_session(self, session: aiohttp.ClientSession): """ Destroy temporary session @@ -118,7 +118,7 @@ class BaseBot: :type session: :obj:`aiohttp.ClientSession` """ if not session.closed: - session.close() + await session.close() if session in self._temp_sessions: self._temp_sessions.remove(session) @@ -181,7 +181,7 @@ class BaseBot: dest.seek(0) return dest finally: - self.destroy_temp_session(session) + await self.destroy_temp_session(session) async def send_file(self, file_type, method, file, payload) -> Union[Dict, base.Boolean]: """ diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py index 10c4e8ec..2b5f5d2d 100644 --- a/examples/proxy_and_emojize.py +++ b/examples/proxy_and_emojize.py @@ -48,7 +48,7 @@ async def cmd_start(message: types.Message): await bot.send_message(message.chat.id, emojize(text(*content, sep='\n')), parse_mode=ParseMode.MARKDOWN) # Destroy temp session - bot.destroy_temp_session(session) + await bot.destroy_temp_session(session) # In this example you can see emoji codes: ":globe_showing_Americas:" and ":locked_with_key:" # You can find full emoji cheat sheet at https://www.webpagefx.com/tools/emoji-cheat-sheet/ From 8f9240f7ea92b7f7dd9753a3e9946d8150753415 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Thu, 19 Apr 2018 03:55:33 +0300 Subject: [PATCH 64/94] Something strange. ModuleNotFoundError is not defined when script executed via pip. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e6485ca1..0d267897 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ from setuptools import find_packages, setup try: from pip.req import parse_requirements -except ModuleNotFoundError: # pip >= 10.0.0 +except ImportError: # pip >= 10.0.0 from pip._internal.req import parse_requirements from aiogram import Stage, VERSION From a941a00abc077f6e3ff5f4e13909c524bf4c67c2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 22 Apr 2018 21:32:32 +0300 Subject: [PATCH 65/94] Deprecate temp sessions. --- aiogram/bot/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 3c93d136..5692a3e7 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -6,6 +6,7 @@ from typing import Dict, List, Optional, Union import aiohttp import certifi +from aiogram.utils.deprecated import deprecated from . import api from ..types import ParseMode, base from ..utils import json @@ -93,6 +94,7 @@ class BaseBot: if not session.closed: await session.close() + @deprecated('Manually you may use `aiohttp.ClientSession` instead of temp session') def create_temp_session(self, limit: base.Integer = 1, force_close: base.Boolean = False) -> aiohttp.ClientSession: """ Create temporary session @@ -110,6 +112,7 @@ class BaseBot: self._temp_sessions.append(session) return session + @deprecated('Manually you may use `aiohttp.ClientSession` instead of temp session') async def destroy_temp_session(self, session: aiohttp.ClientSession): """ Destroy temporary session From e3e1ed9ed75ae595e30cd0a616706f216c13aa38 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 22 Apr 2018 22:02:58 +0300 Subject: [PATCH 66/94] Add InvalidStickersSet exception --- aiogram/bot/api.py | 2 ++ aiogram/utils/exceptions.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 443b5a2e..8d2589e3 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -98,6 +98,8 @@ async def _check_result(method_name, response): exceptions.ToMuchMessages.throw() elif exceptions.InvalidStickersSet.check(description): exceptions.InvalidStickersSet.throw() + elif exceptions.ChatAdminRequired.check(description): + exceptions.ChatAdminRequired.throw() raise exceptions.BadRequest(description) elif response.status == HTTPStatus.NOT_FOUND: if exceptions.MethodNotKnown.check(description): diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index f9676f1b..c0910e48 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -23,6 +23,7 @@ TelegramAPIError PhotoAsInputFileRequired ToMuchMessages InvalidStickersSet + ChatAdminRequired ConflictError TerminatedByOtherGetUpdates CantGetUpdates @@ -162,16 +163,21 @@ class InvalidStickersSet(BadRequest, _MatchErrorMixin): text = 'Stickers set is invalid' +class ChatAdminRequired(BadRequest, _MatchErrorMixin): + match = 'CHAT_ADMIN_REQUIRED' + text = 'Admin permissions is required!' + + class BadWebhook(BadRequest): pass -class WebhookRequireHTTPS(BadRequest, _MatchErrorMixin): +class WebhookRequireHTTPS(BadWebhook, _MatchErrorMixin): match = 'HTTPS url must be provided for webhook' text = 'bad webhook: ' + match -class BadWebhookPort(BadRequest, _MatchErrorMixin): +class BadWebhookPort(BadWebhook, _MatchErrorMixin): match = 'Webhook can be set up only on ports 80, 88, 443 or 8443' text = 'bad webhook: ' + match From 573264b422b56b173f7269c1c017e8be66d776f4 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 22 Apr 2018 22:12:25 +0300 Subject: [PATCH 67/94] Upd requirements. --- dev_requirements.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index a66ce75d..ba4f32bf 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -8,7 +8,7 @@ uvloop>=0.9.1 aioredis>=1.1.0 wheel>=0.31.0 rethinkdb>=2.3.0 -sphinx>=1.7.2 +sphinx>=1.7.3 sphinx-rtd-theme>=0.3.0 aresponses>=1.0.0 tox>=3.0.0 diff --git a/requirements.txt b/requirements.txt index 456d515a..9497f245 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ aiohttp>=3.1.3 Babel>=2.5.3 -certifi>=2018.01.18 +certifi>=2018.4.16 From c672faed0bd151961a61098c37b994f3f2a09750 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 22 Apr 2018 22:16:50 +0300 Subject: [PATCH 68/94] Upd proxy example. --- examples/proxy_and_emojize.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py index 2b5f5d2d..dcc9406b 100644 --- a/examples/proxy_and_emojize.py +++ b/examples/proxy_and_emojize.py @@ -10,12 +10,13 @@ from aiogram.utils.markdown import bold, code, italic, text # Configure bot here API_TOKEN = 'BOT TOKEN HERE' -PROXY_URL = 'http://PROXY_URL' +PROXY_URL = 'http://PROXY_URL' # Or 'socks5://...' # If authentication is required in your proxy then uncomment next line and change login/password for it # PROXY_AUTH = aiohttp.BasicAuth(login='login', password='password') # And add `proxy_auth=PROXY_AUTH` argument in line 25, like this: # >>> bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL, proxy_auth=PROXY_AUTH) +# Also you can use Socks5 proxy but you need manually install aiosocksy package. # Get my ip URL GET_IP_URL = 'http://bot.whatismyipaddress.com/' From a76d95fd71f329351a5dd2375a635db1635d9c75 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 22 Apr 2018 22:27:39 +0300 Subject: [PATCH 69/94] Change stage. --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 6eff73fe..1e53b213 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 3, 0, stage=Stage.DEV, build=0) +VERSION = Version(1, 3, 0, stage=Stage.FINAL, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version From 651e0ad2f1da789cf574062fd717a520acb5887d Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 22 Apr 2018 23:53:50 +0300 Subject: [PATCH 70/94] Bump version --- aiogram/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 6eff73fe..92ab5aa9 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -20,7 +20,7 @@ else: asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 3, 0, stage=Stage.DEV, build=0) +VERSION = Version(1, 3, 1, stage=Stage.DEV, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version From 80d2b24d7e48b76f94b2458f1f00199b8442dfe5 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Mon, 23 Apr 2018 00:02:13 +0300 Subject: [PATCH 71/94] Remove deprecated temp session. --- aiogram/bot/base.py | 61 ++++++----------------------------- examples/proxy_and_emojize.py | 26 ++++++++------- 2 files changed, 24 insertions(+), 63 deletions(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 5692a3e7..c9432726 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -72,9 +72,6 @@ class BaseBot: self.session = aiohttp.ClientSession(connector=connector, request_class=request_class, loop=self.loop, json_serialize=json.dumps) - # Temp sessions - self._temp_sessions = [] - # Data stored in bot instance self._data = {} @@ -90,40 +87,6 @@ class BaseBot: """ if self.session and not self.session.closed: await self.session.close() - for session in self._temp_sessions: - if not session.closed: - await session.close() - - @deprecated('Manually you may use `aiohttp.ClientSession` instead of temp session') - def create_temp_session(self, limit: base.Integer = 1, force_close: base.Boolean = False) -> aiohttp.ClientSession: - """ - Create temporary session - - :param limit: Limit of connections - :type limit: :obj:`int` - :param force_close: Set to True to force close and do reconnect after each request (and between redirects). - :type force_close: :obj:`bool` - :return: New session - :rtype: :obj:`aiohttp.TCPConnector` - """ - session = aiohttp.ClientSession( - connector=aiohttp.TCPConnector(limit=limit, force_close=force_close), - loop=self.loop, json_serialize=json.dumps) - self._temp_sessions.append(session) - return session - - @deprecated('Manually you may use `aiohttp.ClientSession` instead of temp session') - async def destroy_temp_session(self, session: aiohttp.ClientSession): - """ - Destroy temporary session - - :param session: target session - :type session: :obj:`aiohttp.ClientSession` - """ - if not session.closed: - await session.close() - if session in self._temp_sessions: - self._temp_sessions.remove(session) async def request(self, method: base.String, data: Optional[Dict] = None, @@ -168,23 +131,19 @@ class BaseBot: if destination is None: destination = io.BytesIO() - session = self.create_temp_session() url = api.Methods.file_url(token=self.__token, path=file_path) dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb') - try: - async with session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response: - while True: - chunk = await response.content.read(chunk_size) - if not chunk: - break - dest.write(chunk) - dest.flush() - if seek: - dest.seek(0) - return dest - finally: - await self.destroy_temp_session(session) + async with self.session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response: + while True: + chunk = await response.content.read(chunk_size) + if not chunk: + break + dest.write(chunk) + dest.flush() + if seek: + dest.seek(0) + return dest async def send_file(self, file_type, method, file, payload) -> Union[Dict, base.Boolean]: """ diff --git a/examples/proxy_and_emojize.py b/examples/proxy_and_emojize.py index dcc9406b..d979243c 100644 --- a/examples/proxy_and_emojize.py +++ b/examples/proxy_and_emojize.py @@ -1,6 +1,8 @@ import asyncio import logging +import aiohttp + from aiogram import Bot, types from aiogram.dispatcher import Dispatcher from aiogram.types import ParseMode @@ -28,29 +30,29 @@ bot = Bot(token=API_TOKEN, loop=loop, proxy=PROXY_URL) dp = Dispatcher(bot) +async def fetch(url, proxy=None, proxy_auth=None): + async with aiohttp.ClientSession() as session: + async with session.get(url, proxy=proxy, proxy_auth=proxy_auth) as response: + return await response.text() + + @dp.message_handler(commands=['start']) async def cmd_start(message: types.Message): - # Create a temporary session - session = bot.create_temp_session() - content = [] # Make request (without proxy) - async with session.get(GET_IP_URL) as response: - content.append(text(':globe_showing_Americas:', bold('IP:'), code(await response.text()))) - # This line is formatted to '🌎 *IP:* `YOUR IP`' + ip = await fetch(GET_IP_URL) + content.append(text(':globe_showing_Americas:', bold('IP:'), code(ip))) + # This line is formatted to '🌎 *IP:* `YOUR IP`' # Make request through proxy - async with session.get(GET_IP_URL, proxy=bot.proxy, proxy_auth=bot.proxy_auth) as response: - content.append(text(':locked_with_key:', bold('IP:'), code(await response.text()), italic('via proxy'))) - # This line is formatted to 'πŸ” *IP:* `YOUR IP` _via proxy_' + ip = await fetch(GET_IP_URL, bot.proxy, bot.proxy_auth) + content.append(text(':locked_with_key:', bold('IP:'), code(ip), italic('via proxy'))) + # This line is formatted to 'πŸ” *IP:* `YOUR IP` _via proxy_' # Send content await bot.send_message(message.chat.id, emojize(text(*content, sep='\n')), parse_mode=ParseMode.MARKDOWN) - # Destroy temp session - await bot.destroy_temp_session(session) - # In this example you can see emoji codes: ":globe_showing_Americas:" and ":locked_with_key:" # You can find full emoji cheat sheet at https://www.webpagefx.com/tools/emoji-cheat-sheet/ # For representing emoji codes into real emoji use emoji util (aiogram.utils.emoji) From dccaae4e9c9d9e965e531249449302a80e65d5b2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 2 May 2018 13:45:47 +0300 Subject: [PATCH 72/94] Update `setup.py`. Remove versions util. --- aiogram/__init__.py | 24 ++----- aiogram/utils/versions.py | 140 -------------------------------------- dev_requirements.txt | 1 + docs/source/conf.py | 2 +- setup.py | 45 +++++++----- 5 files changed, 36 insertions(+), 176 deletions(-) delete mode 100644 aiogram/utils/versions.py diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 92ab5aa9..3fcd215c 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,27 +1,13 @@ -import warnings +import asyncio -try: - from .bot import Bot -except ImportError as e: - if e.name == 'aiohttp': - warnings.warn('Dependencies are not installed!', - category=ImportWarning) - else: - raise - -from .utils.versions import Stage, Version +from .bot import Bot try: import uvloop except ImportError: - pass + uvloop = None else: - import asyncio - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -VERSION = Version(1, 3, 1, stage=Stage.DEV, build=0) -API_VERSION = Version(3, 6) - -__version__ = VERSION.version -__api_version__ = API_VERSION.version +__version__ = '1.3.1.dev1' +__api_version__ = '3.6' diff --git a/aiogram/utils/versions.py b/aiogram/utils/versions.py deleted file mode 100644 index 1102a2e9..00000000 --- a/aiogram/utils/versions.py +++ /dev/null @@ -1,140 +0,0 @@ -import datetime -import os -import subprocess - -from .helper import Helper, HelperMode, Item - - -# Based on https://github.com/django/django/blob/master/django/utils/version.py - - -class Version: - def __init__(self, major=0, minor=0, - maintenance=0, stage='final', build=0): - self.__raw_version = None - self.__version = None - - self.version = (major, minor, maintenance, stage, build) - - @property - def version(self): - if self.__version is None: - self.__version = self.get_version() - return self.__version - - @version.setter - def version(self, version): - if not isinstance(version, (tuple, list)): - raise TypeError(f"`version` must be an instance of tuple/list, not {type(version)}") - self.__raw_version = version - self.__version = None - - @property - def major(self): - return self.__raw_version[0] - - @property - def minor(self): - return self.__raw_version[1] - - @property - def maintenance(self): - return self.__raw_version[2] - - @property - def stage(self): - return self.__raw_version[3] - - @property - def build(self): - return self.__raw_version[4] - - @property - def raw_version(self): - return self.raw_version - - @property - def pypi_development_status(self): - if self.stage == Stage.DEV: - status = '2 - Pre-Alpha' - elif self.stage == Stage.ALPHA: - status = '3 - Alpha' - elif self.stage == Stage.BETA: - status = '4 - Beta' - elif self.stage == Stage.FINAL: - status = '5 - Production/Stable' - else: - status = '1 - Planning' - return f"Development Status :: {status}" - - def get_version(self): - """ - Returns a PEP 440-compliant version number from VERSION. - :param: - :return: - """ - version = self.__raw_version - - # Now build the two parts of the version number: - # app = X.Y[.Z] - # sub = .devN - for pre-alpha releases - # | {a|b|rc}N - for alpha, beta, and rc releases - - main = self.get_main_version() - - sub = '' - if version[3] == Stage.DEV and version[4] == 0: - git_changeset = self.get_git_changeset() - if git_changeset: - sub = '.dev{0}'.format(git_changeset) - elif version[3] != Stage.FINAL: - mapping = {Stage.ALPHA: 'a', Stage.BETA: 'b', - Stage.RC: 'rc', Stage.DEV: 'dev'} - sub = mapping[version[3]] + str(version[4]) - - return str(main + sub) - - def get_main_version(self): - """ - Returns app version (X.Y[.Z]) from VERSION. - :param: - :return: - """ - version = self.__raw_version - parts = 2 if version[2] == 0 else 3 - return '.'.join(str(x) for x in version[:parts]) - - def get_git_changeset(self): - """Return a numeric identifier of the latest git changeset. - The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format. - This value isn't guaranteed to be unique, but collisions are very unlikely, - so it's sufficient for generating the development version numbers. - """ - repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - git_log = subprocess.Popen( - 'git log --pretty=format:%ct --quiet -1 HEAD', - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True, cwd=repo_dir, universal_newlines=True, - ) - timestamp = git_log.communicate()[0] - try: - timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) - except ValueError: - return None - return timestamp.strftime('%Y%m%d%H%M%S') - - def __str__(self): - return self.version - - def __repr__(self): - return '' - - -class Stage(Helper): - mode = HelperMode.lowercase - - FINAL = Item() - ALPHA = Item() - BETA = Item() - RC = Item() - DEV = Item() diff --git a/dev_requirements.txt b/dev_requirements.txt index ba4f32bf..7f6f9b19 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -13,3 +13,4 @@ sphinx-rtd-theme>=0.3.0 aresponses>=1.0.0 tox>=3.0.0 aiosocksy>=0.1 +click>=6.7 diff --git a/docs/source/conf.py b/docs/source/conf.py index e321ec91..156643ea 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -61,7 +61,7 @@ author = 'Illemius / Alex Root Junior' # built documents. # # The short X.Y version. -version = '{0}.{1}'.format(aiogram.VERSION.major, aiogram.VERSION.minor) +version = aiogram.__version__ # The full version, including alpha/beta/rc tags. release = aiogram.__version__ diff --git a/setup.py b/setup.py index 0d267897..6b24e39c 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 - +import pathlib +import re import sys -from warnings import warn from setuptools import find_packages, setup @@ -10,12 +10,25 @@ try: except ImportError: # pip >= 10.0.0 from pip._internal.req import parse_requirements -from aiogram import Stage, VERSION +WORK_DIR = pathlib.Path(__file__).parent +# Check python version MINIMAL_PY_VERSION = (3, 6) - if sys.version_info < MINIMAL_PY_VERSION: - warn('aiogram works only with Python {}+'.format('.'.join(map(str, MINIMAL_PY_VERSION)), RuntimeWarning)) + raise RuntimeError('aiogram works only with Python {}+'.format('.'.join(map(str, MINIMAL_PY_VERSION)))) + + +def get_version(): + """ + Read version + + :return: str + """ + txt = (WORK_DIR / 'aiogram' / '__init__.py').read_text('utf-8') + try: + return re.findall(r"^__version__ = '([^']+)'\r?$", txt, re.M)[0] + except IndexError: + raise RuntimeError('Unable to determine version.') def get_description(): @@ -29,35 +42,35 @@ def get_description(): return f.read() -def get_requirements(): +def get_requirements(filename=None): """ Read requirements from 'requirements txt' :return: requirements :rtype: list """ - filename = 'requirements.txt' - if VERSION.stage == Stage.DEV: - filename = 'dev_' + filename + if filename is None: + filename = 'requirements.txt' - install_reqs = parse_requirements(filename, session='hack') + file = WORK_DIR / filename + + install_reqs = parse_requirements(str(file), session='hack') return [str(ir.req) for ir in install_reqs] -install_requires = get_requirements() - setup( name='aiogram', - version=VERSION.version, + version=get_version(), packages=find_packages(exclude=('tests', 'tests.*', 'examples.*', 'docs',)), url='https://github.com/aiogram/aiogram', license='MIT', author='Alex Root Junior', - author_email='jroot.junior@gmail.com', + requires_python='>=3.6', + author_email='aiogram@illemius.xyz', description='Is a pretty simple and fully asynchronous library for Telegram Bot API', long_description=get_description(), classifiers=[ - VERSION.pypi_development_status, # Automated change classifier by build stage + 'Development Status :: 5 - Production/Stable' 'Environment :: Console', 'Framework :: AsyncIO', 'Intended Audience :: Developers', @@ -66,5 +79,5 @@ setup( 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Application Frameworks', ], - install_requires=install_requires + install_requires=get_requirements() ) From 2a5dc531d2b8c9f960726db6a5a79fe7c4e389ec Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 2 May 2018 13:49:31 +0300 Subject: [PATCH 73/94] Disable connections limit. --- 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 c9432726..21d97bcf 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -20,7 +20,7 @@ class BaseBot: def __init__(self, token: base.String, loop: Optional[Union[asyncio.BaseEventLoop, asyncio.AbstractEventLoop]] = None, - connections_limit: Optional[base.Integer] = 10, + 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): From 7df5337cb448115683c9cd0d0886565f254f6235 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 2 May 2018 13:52:12 +0300 Subject: [PATCH 74/94] Unused import --- aiogram/bot/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 21d97bcf..ab1acb7b 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -6,7 +6,6 @@ from typing import Dict, List, Optional, Union import aiohttp import certifi -from aiogram.utils.deprecated import deprecated from . import api from ..types import ParseMode, base from ..utils import json From 0b48978bd30ff30bf8a03d7fd7a78512d523dcc7 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 2 May 2018 17:02:20 +0300 Subject: [PATCH 75/94] Added new error types: PhotoDimensions, UnavailableMembers --- aiogram/bot/api.py | 6 ++++++ aiogram/utils/exceptions.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 8d2589e3..d80cd235 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -100,6 +100,12 @@ async def _check_result(method_name, response): exceptions.InvalidStickersSet.throw() elif exceptions.ChatAdminRequired.check(description): exceptions.ChatAdminRequired.throw() + elif exceptions.PhotoDimensions.check(description): + exceptions.PhotoDimensions.throw() + elif exceptions.UnavailableMembers.check(description): + exceptions.UnavailableMembers.throw() + elif exceptions.TypeOfFileMismatch.check(description): + exceptions.TypeOfFileMismatch.throw() raise exceptions.BadRequest(description) elif response.status == HTTPStatus.NOT_FOUND: if exceptions.MethodNotKnown.check(description): diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index c0910e48..fa846ca3 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -24,6 +24,9 @@ TelegramAPIError ToMuchMessages InvalidStickersSet ChatAdminRequired + PhotoDimensions + UnavailableMembers + TypeOfFileMismatch ConflictError TerminatedByOtherGetUpdates CantGetUpdates @@ -168,6 +171,19 @@ class ChatAdminRequired(BadRequest, _MatchErrorMixin): text = 'Admin permissions is required!' +class PhotoDimensions(BadRequest, _MatchErrorMixin): + match = 'PHOTO_INVALID_DIMENSIONS' + text = 'Invalid photo dimensions' + + +class UnavailableMembers(BadRequest, _MatchErrorMixin): + match = 'supergroup members are unavailable' + + +class TypeOfFileMismatch(BadRequest, _MatchErrorMixin): + match = 'type of file mismatch' + + class BadWebhook(BadRequest): pass From 8f45ebc9ed9723b851855587d53cff5038187c67 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 2 May 2018 18:08:36 +0300 Subject: [PATCH 76/94] Refactoring the detection of error types --- aiogram/bot/api.py | 61 ++------------------- aiogram/utils/exceptions.py | 104 +++++++++++++++++++++++------------- 2 files changed, 71 insertions(+), 94 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index d80cd235..f5abfc23 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -60,7 +60,6 @@ async def _check_result(method_name, response): description = result_json.get('description') or body - # TODO: refactor the detection of error types if HTTPStatus.OK <= response.status <= HTTPStatus.IM_USED: return result_json.get('result') elif 'retry_after' in result_json: @@ -68,65 +67,13 @@ async def _check_result(method_name, response): elif 'migrate_to_chat_id' in result_json: raise exceptions.MigrateToChat(result_json['migrate_to_chat_id']) elif response.status == HTTPStatus.BAD_REQUEST: - if exceptions.MessageNotModified.check(description): - exceptions.MessageNotModified.throw() - elif exceptions.MessageToForwardNotFound.check(description): - exceptions.MessageToForwardNotFound.throw() - elif exceptions.MessageIdentifierNotSpecified.check(description): - exceptions.MessageIdentifierNotSpecified.throw() - elif exceptions.ChatNotFound.check(description): - exceptions.ChatNotFound.throw() - elif exceptions.InvalidQueryID.check(description): - exceptions.InvalidQueryID.throw() - elif exceptions.InvalidHTTPUrlContent.check(description): - exceptions.InvalidHTTPUrlContent.throw() - elif exceptions.GroupDeactivated.check(description): - exceptions.GroupDeactivated.throw() - elif exceptions.WrongFileIdentifier.check(description): - exceptions.WrongFileIdentifier.throw() - elif exceptions.InvalidPeerID.check(description): - exceptions.InvalidPeerID.throw() - elif exceptions.WebhookRequireHTTPS.check(description): - exceptions.WebhookRequireHTTPS.throw() - elif exceptions.BadWebhookPort.check(description): - exceptions.BadWebhookPort.throw() - elif exceptions.CantParseUrl.check(description): - exceptions.CantParseUrl.throw() - elif exceptions.PhotoAsInputFileRequired.check(description): - exceptions.PhotoAsInputFileRequired.throw() - elif exceptions.ToMuchMessages.check(description): - exceptions.ToMuchMessages.throw() - elif exceptions.InvalidStickersSet.check(description): - exceptions.InvalidStickersSet.throw() - elif exceptions.ChatAdminRequired.check(description): - exceptions.ChatAdminRequired.throw() - elif exceptions.PhotoDimensions.check(description): - exceptions.PhotoDimensions.throw() - elif exceptions.UnavailableMembers.check(description): - exceptions.UnavailableMembers.throw() - elif exceptions.TypeOfFileMismatch.check(description): - exceptions.TypeOfFileMismatch.throw() - raise exceptions.BadRequest(description) + exceptions.BadRequest.detect(description) elif response.status == HTTPStatus.NOT_FOUND: - if exceptions.MethodNotKnown.check(description): - exceptions.MethodNotKnown.throw() - raise exceptions.NotFound(description) + exceptions.NotFound.detect(description) elif response.status == HTTPStatus.CONFLICT: - if exceptions.TerminatedByOtherGetUpdates.check(description): - exceptions.TerminatedByOtherGetUpdates.throw() - if exceptions.CantGetUpdates.check(description): - exceptions.CantGetUpdates.throw() - raise exceptions.ConflictError(description) + exceptions.ConflictError.detect(description) elif response.status in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]: - if exceptions.BotKicked.check(description): - exceptions.BotKicked.throw() - elif exceptions.BotBlocked.check(description): - exceptions.BotBlocked.throw() - elif exceptions.UserDeactivated.check(description): - exceptions.UserDeactivated.throw() - elif exceptions.CantInitiateConversation.check(description): - exceptions.UserDeactivated.throw() - raise exceptions.Unauthorized(description) + exceptions.Unauthorized.detect(description) elif response.status == HTTPStatus.REQUEST_ENTITY_TOO_LARGE: raise exceptions.NetworkError('File too large for uploading. ' 'Check telegram api limits https://core.telegram.org/bots/api#senddocument') diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index fa846ca3..99b174a1 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -63,14 +63,43 @@ class _MatchErrorMixin: match = '' text = None + __subclasses = [] + + def __init_subclass__(cls, **kwargs): + super(_MatchErrorMixin, cls).__init_subclass__(**kwargs) + # cls.match = cls.match.lower() if cls.match else '' + if not hasattr(cls, f"_{cls.__name__}__group"): + cls.__subclasses.append(cls) + @classmethod - def check(cls, message): - return cls.match in message + def check(cls, message) -> bool: + """ + Compare pattern with message + + :param message: always must be in lowercase + :return: bool + """ + return cls.match.lower() in message @classmethod def throw(cls): + """ + Throw a error + + :raise: this + """ raise cls(cls.text or cls.match) + @classmethod + def detect(cls, description): + description = description.lower() + for err in cls.__subclasses: + if err is cls: + continue + if err.check(description): + err.throw() + raise cls(description) + class AIOGramWarning(Warning): pass @@ -88,36 +117,37 @@ class ValidationError(TelegramAPIError): pass -class BadRequest(TelegramAPIError): - pass +class BadRequest(TelegramAPIError, _MatchErrorMixin): + __group = True class MessageError(BadRequest): + __group = True pass -class MessageNotModified(MessageError, _MatchErrorMixin): +class MessageNotModified(MessageError): """ Will be raised when you try to set new text is equals to current text. """ match = 'message is not modified' -class MessageToForwardNotFound(MessageError, _MatchErrorMixin): +class MessageToForwardNotFound(MessageError): """ Will be raised when you try to forward very old or deleted or unknown message. """ match = 'message to forward not found' -class MessageToDeleteNotFound(MessageError, _MatchErrorMixin): +class MessageToDeleteNotFound(MessageError): """ Will be raised when you try to delete very old or deleted or unknown message. """ match = 'message to delete not found' -class MessageIdentifierNotSpecified(MessageError, _MatchErrorMixin): +class MessageIdentifierNotSpecified(MessageError): match = 'message identifier is not specified' @@ -125,122 +155,122 @@ class ChatNotFound(BadRequest, _MatchErrorMixin): match = 'chat not found' -class InvalidQueryID(BadRequest, _MatchErrorMixin): +class InvalidQueryID(BadRequest): match = 'QUERY_ID_INVALID' text = 'Invalid query ID' -class InvalidPeerID(BadRequest, _MatchErrorMixin): +class InvalidPeerID(BadRequest): match = 'PEER_ID_INVALID' text = 'Invalid peer ID' -class InvalidHTTPUrlContent(BadRequest, _MatchErrorMixin): +class InvalidHTTPUrlContent(BadRequest): match = 'Failed to get HTTP URL content' -class WrongFileIdentifier(BadRequest, _MatchErrorMixin): +class WrongFileIdentifier(BadRequest): match = 'wrong file identifier/HTTP URL specified' -class GroupDeactivated(BadRequest, _MatchErrorMixin): +class GroupDeactivated(BadRequest): match = 'group is deactivated' -class PhotoAsInputFileRequired(BadRequest, _MatchErrorMixin): +class PhotoAsInputFileRequired(BadRequest): """ Will be raised when you try to set chat photo from file ID. """ match = 'Photo should be uploaded as an InputFile' -class ToMuchMessages(BadRequest, _MatchErrorMixin): +class ToMuchMessages(BadRequest): """ Will be raised when you try to send media group with more than 10 items. """ match = 'Too much messages to send as an album' -class InvalidStickersSet(BadRequest, _MatchErrorMixin): +class InvalidStickersSet(BadRequest): match = 'STICKERSET_INVALID' text = 'Stickers set is invalid' -class ChatAdminRequired(BadRequest, _MatchErrorMixin): +class ChatAdminRequired(BadRequest): match = 'CHAT_ADMIN_REQUIRED' text = 'Admin permissions is required!' -class PhotoDimensions(BadRequest, _MatchErrorMixin): +class PhotoDimensions(BadRequest): match = 'PHOTO_INVALID_DIMENSIONS' text = 'Invalid photo dimensions' -class UnavailableMembers(BadRequest, _MatchErrorMixin): +class UnavailableMembers(BadRequest): match = 'supergroup members are unavailable' -class TypeOfFileMismatch(BadRequest, _MatchErrorMixin): +class TypeOfFileMismatch(BadRequest): match = 'type of file mismatch' class BadWebhook(BadRequest): - pass + __group = True -class WebhookRequireHTTPS(BadWebhook, _MatchErrorMixin): +class WebhookRequireHTTPS(BadWebhook): match = 'HTTPS url must be provided for webhook' text = 'bad webhook: ' + match -class BadWebhookPort(BadWebhook, _MatchErrorMixin): +class BadWebhookPort(BadWebhook): match = 'Webhook can be set up only on ports 80, 88, 443 or 8443' text = 'bad webhook: ' + match -class CantParseUrl(BadRequest, _MatchErrorMixin): +class CantParseUrl(BadRequest): match = 'can\'t parse URL' -class NotFound(TelegramAPIError): - pass +class NotFound(TelegramAPIError, _MatchErrorMixin): + __group = True -class MethodNotKnown(NotFound, _MatchErrorMixin): +class MethodNotKnown(NotFound): match = 'method not found' -class ConflictError(TelegramAPIError): - pass +class ConflictError(TelegramAPIError, _MatchErrorMixin): + __group = True -class TerminatedByOtherGetUpdates(ConflictError, _MatchErrorMixin): +class TerminatedByOtherGetUpdates(ConflictError): match = 'terminated by other getUpdates request' text = 'Terminated by other getUpdates request; ' \ 'Make sure that only one bot instance is running' -class CantGetUpdates(ConflictError, _MatchErrorMixin): +class CantGetUpdates(ConflictError): match = 'can\'t use getUpdates method while webhook is active' -class Unauthorized(TelegramAPIError): - pass +class Unauthorized(TelegramAPIError, _MatchErrorMixin): + __group = True -class BotKicked(Unauthorized, _MatchErrorMixin): +class BotKicked(Unauthorized): match = 'Bot was kicked from a chat' -class BotBlocked(Unauthorized, _MatchErrorMixin): +class BotBlocked(Unauthorized): match = 'bot was blocked by the user' -class UserDeactivated(Unauthorized, _MatchErrorMixin): +class UserDeactivated(Unauthorized): match = 'user is deactivated' -class CantInitiateConversation(Unauthorized, _MatchErrorMixin): +class CantInitiateConversation(Unauthorized): match = 'bot can\'t initiate conversation with a user' From 8a149b1ed5fd8458dc1a5b60d198e3d7214735bf Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 2 May 2018 18:09:01 +0300 Subject: [PATCH 77/94] Add error: MessageTextIsEmpty --- aiogram/utils/exceptions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 99b174a1..cbefa859 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -8,6 +8,7 @@ TelegramAPIError MessageToForwardNotFound MessageToDeleteNotFound MessageIdentifierNotSpecified + MessageTextIsEmpty ChatNotFound InvalidQueryID InvalidPeerID @@ -151,7 +152,11 @@ class MessageIdentifierNotSpecified(MessageError): match = 'message identifier is not specified' -class ChatNotFound(BadRequest, _MatchErrorMixin): +class MessageTextIsEmpty(MessageError): + match = 'Message text is empty' + + +class ChatNotFound(BadRequest): match = 'chat not found' From b597f1e5655b543c2f8c41604283f30ef61527d8 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 2 May 2018 18:09:51 +0300 Subject: [PATCH 78/94] Fixed the RetryAfter and MigrateToChat errors --- aiogram/bot/api.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index f5abfc23..342ec62c 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -59,13 +59,14 @@ async def _check_result(method_name, response): result_json = {} description = result_json.get('description') or body + parameters = types.ResponseParameters(**result_json.get('parameters', {}) or {}) if HTTPStatus.OK <= response.status <= HTTPStatus.IM_USED: return result_json.get('result') - elif 'retry_after' in result_json: - raise exceptions.RetryAfter(result_json['retry_after']) - elif 'migrate_to_chat_id' in result_json: - raise exceptions.MigrateToChat(result_json['migrate_to_chat_id']) + elif parameters.retry_after: + raise exceptions.RetryAfter(parameters.retry_after) + elif parameters.migrate_to_chat_id: + raise exceptions.MigrateToChat(parameters.migrate_to_chat_id) elif response.status == HTTPStatus.BAD_REQUEST: exceptions.BadRequest.detect(description) elif response.status == HTTPStatus.NOT_FOUND: From 60bf8aa0ddad313afed10cb59ec70f9dcb75f440 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 2 May 2018 18:24:23 +0300 Subject: [PATCH 79/94] Cleanup --- aiogram/__init__.py | 1 + aiogram/dispatcher/handler.py | 2 +- aiogram/utils/exceptions.py | 3 +-- aiogram/utils/safe.py | 16 ---------------- 4 files changed, 3 insertions(+), 19 deletions(-) delete mode 100644 aiogram/utils/safe.py diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 3fcd215c..3cfa182e 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -1,6 +1,7 @@ import asyncio from .bot import Bot +from .dispatcher import Dispatcher try: import uvloop diff --git a/aiogram/dispatcher/handler.py b/aiogram/dispatcher/handler.py index 139c6011..8d75273f 100644 --- a/aiogram/dispatcher/handler.py +++ b/aiogram/dispatcher/handler.py @@ -1,5 +1,5 @@ -from aiogram.utils import context from .filters import check_filters +from ..utils import context class SkipHandler(BaseException): diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index cbefa859..015720c7 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -85,7 +85,7 @@ class _MatchErrorMixin: @classmethod def throw(cls): """ - Throw a error + Throw error :raise: this """ @@ -124,7 +124,6 @@ class BadRequest(TelegramAPIError, _MatchErrorMixin): class MessageError(BadRequest): __group = True - pass class MessageNotModified(MessageError): diff --git a/aiogram/utils/safe.py b/aiogram/utils/safe.py deleted file mode 100644 index 7ef5b8fc..00000000 --- a/aiogram/utils/safe.py +++ /dev/null @@ -1,16 +0,0 @@ -import typing - - -async def safe(coro: typing.Coroutine) -> (bool, typing.Any): - """ - Safety execute coroutine - - Status - returns True if success otherwise False - - :param coro: - :return: status and result - """ - try: - return True, await coro - except Exception as e: - return False, e From ddb6c81d9cb965de64b0851f5978b20fda4db724 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 May 2018 00:19:18 +0300 Subject: [PATCH 80/94] Example: Simple broadcast messages --- examples/broadcast_example.py | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 examples/broadcast_example.py diff --git a/examples/broadcast_example.py b/examples/broadcast_example.py new file mode 100644 index 00000000..468da916 --- /dev/null +++ b/examples/broadcast_example.py @@ -0,0 +1,72 @@ +import asyncio +import logging + +from aiogram import Bot, Dispatcher, types +from aiogram.utils import exceptions, executor + +API_TOKEN = 'BOT TOKEN HERE' + +logging.basicConfig(level=logging.INFO) +log = logging.getLogger('broadcast') + +loop = asyncio.get_event_loop() +bot = Bot(token=API_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML) +dp = Dispatcher(bot, loop=loop) + + +def get_users(): + """ + Return users list + + In this example returns some random ID's + """ + yield from (61043901, 78238238, 78378343, 98765431, 12345678) + + +async def send_message(user_id: int, text: str) -> bool: + """ + Safe messages sender + + :param user_id: + :param text: + :return: + """ + try: + await bot.send_message(user_id, 'Hello, World!') + except exceptions.BotBlocked: + log.error(f"Target [ID:{user_id}]: blocked by user") + except exceptions.ChatNotFound: + log.error(f"Target [ID:{user_id}]: invalid user ID") + except exceptions.RetryAfter as e: + log.error(f"Target [ID:{user_id}]: Flood limit is exceeded. Sleep {e.timeout} seconds.") + await asyncio.sleep(e.timeout) + return await send_message(user_id, text) # Recursive call + except exceptions.TelegramAPIError: + log.exception(f"Target [ID:{user_id}]: failed") + else: + log.info(f"Target [ID:{user_id}]: success") + return True + return False + + +async def broadcaster() -> int: + """ + Simple broadcaster + + :return: Count of messages + """ + count = 0 + try: + for user_id in get_users(): + if await send_message(user_id, 'Hello!'): + count += 1 + await asyncio.sleep(.05) # 20 messages per second (Limit: 30 messages per second) + finally: + log.info(f"{count} messages successful sent.") + + return count + + +if __name__ == '__main__': + # Execute broadcaster + executor.start(dp, broadcaster()) From f3580def030ef30103db1e40e848c7b8799f96a8 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 May 2018 00:21:43 +0300 Subject: [PATCH 81/94] Return result from executor. (`Executor.start(...)`) --- aiogram/utils/executor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 9fa7121c..a3b19844 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -75,7 +75,7 @@ def start(dispatcher, func, *, loop=None, skip_updates=None, executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) _setup_callbacks(executor, on_startup, on_shutdown) - executor.start(func) + return executor.start(func) class Executor: @@ -250,12 +250,14 @@ class Executor: try: loop.run_until_complete(self._startup_polling()) - loop.run_until_complete(func) + result = loop.run_until_complete(func) except (KeyboardInterrupt, SystemExit): + result = None loop.stop() finally: loop.run_until_complete(self._shutdown_polling()) log.warning("Goodbye!") + return result async def _skip_updates(self): await self.dispatcher.reset_webhook(True) From 65edaeb2b5e217ab64958d5166df326a0185dee0 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 May 2018 01:19:36 +0300 Subject: [PATCH 82/94] Add tests for token validator and widget util. --- aiogram/utils/auth_widget.py | 26 +++++++++++++++-------- tests/test_token.py | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 tests/test_token.py diff --git a/aiogram/utils/auth_widget.py b/aiogram/utils/auth_widget.py index b5cce802..800dfc12 100644 --- a/aiogram/utils/auth_widget.py +++ b/aiogram/utils/auth_widget.py @@ -3,6 +3,21 @@ import hashlib import hmac +def generate_hash(data, token): + """ + Generate secret hash + + :param data: + :param token: + :return: + """ + secret = hashlib.sha256() + secret.update(token.encode('utf-8')) + sorted_params = collections.OrderedDict(sorted(data.items())) + msg = "\n".join(["{}={}".format(k, v) for k, v in sorted_params.items() if k != 'hash']) + return hmac.new(secret.digest(), msg.encode('utf-8'), digestmod=hashlib.sha256).hexdigest() + + def check_token(data, token): """ Validate auth token @@ -14,12 +29,5 @@ def check_token(data, token): :param token: :return: """ - secret = hashlib.sha256() - secret.update(token.encode('utf-8')) - sorted_params = collections.OrderedDict(sorted(data.items())) - param_hash = sorted_params.pop('hash', '') or '' - msg = "\n".join(["{}={}".format(k, v) for k, v in sorted_params.items()]) - - if param_hash == hmac.new(secret.digest(), msg.encode('utf-8'), digestmod=hashlib.sha256).hexdigest(): - return True - return False + param_hash = data.get('hash', '') or '' + return param_hash == generate_hash(data, token) diff --git a/tests/test_token.py b/tests/test_token.py new file mode 100644 index 00000000..b8a6087f --- /dev/null +++ b/tests/test_token.py @@ -0,0 +1,41 @@ +import pytest + +from aiogram.bot import api +from aiogram.utils import auth_widget, exceptions + +VALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff-1234567890' +INVALID_TOKEN = '123456789:AABBCCDDEEFFaabbccddeeff 123456789' # Space in token and wrong length + +VALID_DATA = { + 'date': 1525385236, + 'first_name': 'Test', + 'last_name': 'User', + 'id': 123456789, + 'username': 'username', + 'hash': '69a9871558fbbe4cd0dbaba52fa1cc4f38315d3245b7504381a64139fb024b5b' +} +INVALID_DATA = { + 'date': 1525385237, + 'first_name': 'Test', + 'last_name': 'User', + 'id': 123456789, + 'username': 'username', + 'hash': '69a9871558fbbe4cd0dbaba52fa1cc4f38315d3245b7504381a64139fb024b5b' +} + + +def test_valid_token(): + assert api.check_token(VALID_TOKEN) + + +def test_invalid_token(): + with pytest.raises(exceptions.ValidationError): + api.check_token(INVALID_TOKEN) + + +def test_widget(): + assert auth_widget.check_token(VALID_DATA, VALID_TOKEN) + + +def test_invalid_widget_data(): + assert not auth_widget.check_token(INVALID_DATA, VALID_TOKEN) From 25a328793b32d4ce428d4fabf6a2214690a75d05 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 May 2018 13:45:17 +0300 Subject: [PATCH 83/94] Cleaning the `auth_widget` utility --- aiogram/utils/auth_widget.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/aiogram/utils/auth_widget.py b/aiogram/utils/auth_widget.py index 800dfc12..4ea3adc0 100644 --- a/aiogram/utils/auth_widget.py +++ b/aiogram/utils/auth_widget.py @@ -1,9 +1,16 @@ -import collections +""" +Implementation of Telegram site authorization checking mechanism +for more information https://core.telegram.org/widgets/login#checking-authorization + +Source: https://gist.github.com/JrooTJunior/887791de7273c9df5277d2b1ecadc839 +""" import hashlib import hmac +import collections -def generate_hash(data, token): + +def generate_hash(data: dict, token: str) -> str: """ Generate secret hash @@ -14,16 +21,13 @@ def generate_hash(data, token): secret = hashlib.sha256() secret.update(token.encode('utf-8')) sorted_params = collections.OrderedDict(sorted(data.items())) - msg = "\n".join(["{}={}".format(k, v) for k, v in sorted_params.items() if k != 'hash']) + msg = '\n'.join(["{}={}".format(k, v) for k, v in sorted_params.items() if k != 'hash']) return hmac.new(secret.digest(), msg.encode('utf-8'), digestmod=hashlib.sha256).hexdigest() -def check_token(data, token): +def check_token(data: dict, token: str) -> bool: """ Validate auth token - https://core.telegram.org/widgets/login#checking-authorization - - Source: https://gist.github.com/xen/e4bea72487d34caa28c762776cf655a3 :param data: :param token: From cd320b3cf55458f022241721d6ef07b8cf3f9023 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 May 2018 13:47:02 +0300 Subject: [PATCH 84/94] Move ToMuchMessages to MessageError group. --- aiogram/utils/exceptions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 015720c7..7aed8e54 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -9,6 +9,7 @@ TelegramAPIError MessageToDeleteNotFound MessageIdentifierNotSpecified MessageTextIsEmpty + ToMuchMessages ChatNotFound InvalidQueryID InvalidPeerID @@ -22,7 +23,6 @@ TelegramAPIError NotFound MethodNotKnown PhotoAsInputFileRequired - ToMuchMessages InvalidStickersSet ChatAdminRequired PhotoDimensions @@ -155,6 +155,13 @@ class MessageTextIsEmpty(MessageError): match = 'Message text is empty' +class ToMuchMessages(MessageError): + """ + Will be raised when you try to send media group with more than 10 items. + """ + match = 'Too much messages to send as an album' + + class ChatNotFound(BadRequest): match = 'chat not found' @@ -188,13 +195,6 @@ class PhotoAsInputFileRequired(BadRequest): match = 'Photo should be uploaded as an InputFile' -class ToMuchMessages(BadRequest): - """ - Will be raised when you try to send media group with more than 10 items. - """ - match = 'Too much messages to send as an album' - - class InvalidStickersSet(BadRequest): match = 'STICKERSET_INVALID' text = 'Stickers set is invalid' From 20109228091c9ee3fa84f0510b8a43647207066c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 4 May 2018 13:55:53 +0300 Subject: [PATCH 85/94] More truthful description of the method (`Executor.start(...)`) --- aiogram/utils/executor.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index a3b19844..dc5b60ad 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -59,14 +59,14 @@ def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None, executor.start_webhook(webhook_path, **kwargs) -def start(dispatcher, func, *, loop=None, skip_updates=None, +def start(dispatcher, future, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None): """ - Execute function + Execute Future. - :param dispatcher: - :param func: - :param loop: + :param dispatcher: instance of Dispatcher + :param future: future + :param loop: instance of AbstractEventLoop :param skip_updates: :param on_startup: :param on_shutdown: @@ -75,7 +75,7 @@ def start(dispatcher, func, *, loop=None, skip_updates=None, executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop) _setup_callbacks(executor, on_startup, on_shutdown) - return executor.start(func) + return executor.start(future) class Executor: @@ -237,11 +237,13 @@ class Executor: loop.run_until_complete(self._shutdown_polling()) log.warning("Goodbye!") - def start(self, func): + def start(self, future): """ - Execute function + Execute Future. - :param func: + Return the Future's result, or raise its exception. + + :param future: :return: """ self._check_frozen() @@ -250,7 +252,7 @@ class Executor: try: loop.run_until_complete(self._startup_polling()) - result = loop.run_until_complete(func) + result = loop.run_until_complete(future) except (KeyboardInterrupt, SystemExit): result = None loop.stop() From f5f3555ae04217ed26b1b7152cf5164c2154f3df Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 5 May 2018 14:37:54 +0300 Subject: [PATCH 86/94] Bump license: Year. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index bbb46736..12244562 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 Alex Root Junior +Copyright (c) 2017-2018 Alex Root Junior Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software From b946caabce78b29414200874442cb9ad34dce428 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 11 May 2018 00:27:18 +0300 Subject: [PATCH 87/94] Fix classifiers list. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6b24e39c..9b583400 100755 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( description='Is a pretty simple and fully asynchronous library for Telegram Bot API', long_description=get_description(), classifiers=[ - 'Development Status :: 5 - Production/Stable' + 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Framework :: AsyncIO', 'Intended Audience :: Developers', From 1f8b2b464e2561821fa8e7604882a1b962a3883b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 11 May 2018 01:51:50 +0300 Subject: [PATCH 88/94] Use global parse_mode in InputTextMessageContent --- aiogram/types/input_message_content.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/aiogram/types/input_message_content.py b/aiogram/types/input_message_content.py index 6aa99231..88f8a74f 100644 --- a/aiogram/types/input_message_content.py +++ b/aiogram/types/input_message_content.py @@ -62,9 +62,18 @@ class InputTextMessageContent(InputMessageContent): parse_mode: base.String = fields.Field() disable_web_page_preview: base.Boolean = fields.Field() + def safe_get_parse_mode(self): + try: + return self.bot.parse_mode + except RuntimeError: + pass + def __init__(self, message_text: typing.Optional[base.String] = None, parse_mode: typing.Optional[base.String] = None, disable_web_page_preview: typing.Optional[base.Boolean] = None): + if parse_mode is None: + parse_mode = self.safe_get_parse_mode() + super(InputTextMessageContent, self).__init__(message_text=message_text, parse_mode=parse_mode, disable_web_page_preview=disable_web_page_preview) From 30761733846aaf5654756900e3bf584fd45d7d49 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 11 May 2018 02:00:32 +0300 Subject: [PATCH 89/94] New error types. --- aiogram/bot/api.py | 2 ++ aiogram/utils/exceptions.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 342ec62c..48904446 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -79,6 +79,8 @@ async def _check_result(method_name, response): raise exceptions.NetworkError('File too large for uploading. ' 'Check telegram api limits https://core.telegram.org/bots/api#senddocument') elif response.status >= HTTPStatus.INTERNAL_SERVER_ERROR: + if 'restart' in description: + raise exceptions.RestartingTelegram() raise exceptions.TelegramAPIError(description) raise exceptions.TelegramAPIError(f"{description} [{response.status}]") diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 7aed8e54..d305b9b5 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -19,6 +19,7 @@ TelegramAPIError BadWebhook WebhookRequireHTTPS BadWebhookPort + BadWebhookAddrInfo CantParseUrl NotFound MethodNotKnown @@ -39,6 +40,7 @@ TelegramAPIError NetworkError RetryAfter MigrateToChat + RestartingTelegram AIOGramWarning TimeoutWarning @@ -232,6 +234,11 @@ class BadWebhookPort(BadWebhook): text = 'bad webhook: ' + match +class BadWebhookAddrInfo(BadWebhook): + match = 'getaddrinfo: Temporary failure in name resolution' + text = 'bad webhook: ' + match + + class CantParseUrl(BadRequest): match = 'can\'t parse URL' @@ -282,6 +289,11 @@ class NetworkError(TelegramAPIError): pass +class RestartingTelegram(TelegramAPIError): + def __init__(self): + super(RestartingTelegram, self).__init__('The Telegram Bot API service is restarting. Wait few second.') + + class RetryAfter(TelegramAPIError): def __init__(self, retry_after): super(RetryAfter, self).__init__(f"Flood control exceeded. Retry in {retry_after} seconds.") From 272322ab382f16a64bca38b098a825d89cff70b2 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 11 May 2018 02:24:57 +0300 Subject: [PATCH 90/94] Fixed slow-responses into webhook (via simple request) --- aiogram/dispatcher/webhook.py | 7 +++++-- aiogram/utils/context.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 1711bffe..ca717202 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -87,8 +87,11 @@ class WebhookRequestHandler(web.View): :return: :class:`aiogram.Dispatcher` """ dp = self.request.app[BOT_DISPATCHER_KEY] - context.set_value('dispatcher', dp) - context.set_value('bot', dp.bot) + try: + context.set_value('dispatcher', dp) + context.set_value('bot', dp.bot) + except RuntimeError: + pass return dp async def parse_update(self, bot): diff --git a/aiogram/utils/context.py b/aiogram/utils/context.py index ea922b48..376d9aa9 100644 --- a/aiogram/utils/context.py +++ b/aiogram/utils/context.py @@ -46,6 +46,8 @@ def get_current_state() -> typing.Dict: :rtype: :obj:`dict` """ task = asyncio.Task.current_task() + if task is None: + raise RuntimeError('Can be used only in Task context.') context_ = getattr(task, 'context', None) if context_ is None: context_ = task.context = {} From 5ed2b35eed1fb1aeaaabfebe40c5f219f0a6bb7f Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 18 May 2018 21:13:19 +0300 Subject: [PATCH 91/94] Fixed RethinkDBStorage. --- aiogram/contrib/fsm_storage/rethinkdb.py | 4 ++-- aiogram/utils/executor.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index 8c6d24ae..9aeef13f 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -101,7 +101,7 @@ class RethinkDBStorage(BaseStorage): except r.ReqlError: raise ConnectionNotClosed('Exception was caught while closing connection') - def wait_closed(self): + async def wait_closed(self): """ Checks if connection is closed. """ @@ -115,7 +115,7 @@ class RethinkDBStorage(BaseStorage): conn = await self.get_connection() result = await r.table(self._table).get(chat)[user]['state'].default(default or '').run(conn) await self.put_connection(conn) - return result + return result or None async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Dict: diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index dc5b60ad..39e10333 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -232,7 +232,8 @@ class Executor: loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook)) loop.run_forever() except (KeyboardInterrupt, SystemExit): - loop.stop() + # loop.stop() + pass finally: loop.run_until_complete(self._shutdown_polling()) log.warning("Goodbye!") From b767c3e8f6808018abbecf464281d3901e2a425c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 18 May 2018 22:13:12 +0300 Subject: [PATCH 92/94] Fix webhook executor. I think. --- aiogram/utils/executor.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index 39e10333..201af7cc 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -185,7 +185,7 @@ class Executor: if app is None: self._web_app = app = web.Application() - if self._identity in self._identity: + if self._identity == app.get(self._identity): # App is already configured return @@ -197,9 +197,13 @@ class Executor: for callback in self._on_startup_webhook: app.on_startup.append(functools.partial(_wrap_callback, callback)) - for callback in self._on_shutdown_webhook: - app.on_shutdown.append(functools.partial(_wrap_callback, callback)) + # for callback in self._on_shutdown_webhook: + # app.on_shutdown.append(functools.partial(_wrap_callback, callback)) + async def _on_shutdown(_): + await self._shutdown_webhook() + + app.on_shutdown.append(_on_shutdown) app[APP_EXECUTOR_KEY] = self app[BOT_DISPATCHER_KEY] = self.dispatcher app[self._identity] = datetime.datetime.now() @@ -216,6 +220,7 @@ class Executor: """ self._prepare_webhook(webhook_path, request_handler) self.loop.run_until_complete(self._startup_webhook()) + web.run_app(self._web_app, **kwargs) def start_polling(self, reset_webhook=None): @@ -296,6 +301,15 @@ class Executor: if wait_closed: await self.dispatcher.wait_closed() + async def _shutdown_webhook(self, wait_closed=False): + for callback in self._on_shutdown_webhook: + await callback(self.dispatcher) + + await self._shutdown() + + if wait_closed: + await self.dispatcher.wait_closed() + async def _startup_webhook(self): await self._welcome() if self.skip_updates: From 116719553b4636e6f73aa58095153642e8437022 Mon Sep 17 00:00:00 2001 From: Arslan Sakhapov Date: Sat, 19 May 2018 14:03:29 +0500 Subject: [PATCH 93/94] Repair RethinkDBStorage --- aiogram/contrib/fsm_storage/rethinkdb.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index 8c6d24ae..9a065bae 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -101,19 +101,17 @@ class RethinkDBStorage(BaseStorage): except r.ReqlError: raise ConnectionNotClosed('Exception was caught while closing connection') - def wait_closed(self): + async def wait_closed(self): """ - Checks if connection is closed. + Does nothing """ - if len(self._outstanding_connections) != 0 and self._queue.qsize() != 0: - raise ConnectionNotClosed - return True + await asyncio.sleep(0) async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Optional[str]: chat, user = map(str, self.check_address(chat=chat, user=user)) conn = await self.get_connection() - result = await r.table(self._table).get(chat)[user]['state'].default(default or '').run(conn) + result = await r.table(self._table).get(chat)[user]['state'].default(default or None).run(conn) await self.put_connection(conn) return result From e1eed6ccb7defdb862c1fc45f3fc753bdc0bbfce Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sat, 19 May 2018 19:55:55 +0300 Subject: [PATCH 94/94] partial rollback. add TODO. --- aiogram/contrib/fsm_storage/rethinkdb.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aiogram/contrib/fsm_storage/rethinkdb.py b/aiogram/contrib/fsm_storage/rethinkdb.py index f49eab12..cfa71663 100644 --- a/aiogram/contrib/fsm_storage/rethinkdb.py +++ b/aiogram/contrib/fsm_storage/rethinkdb.py @@ -11,6 +11,9 @@ __all__ = ['RethinkDBStorage', 'ConnectionNotClosed'] r.set_loop_type('asyncio') +# TODO: rewrite connections pool + + class ConnectionNotClosed(Exception): """ Indicates that DB connection wasn't closed. @@ -105,7 +108,7 @@ class RethinkDBStorage(BaseStorage): """ Does nothing """ - await asyncio.sleep(0) + pass async def get_state(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Optional[str]: @@ -113,7 +116,7 @@ class RethinkDBStorage(BaseStorage): conn = await self.get_connection() result = await r.table(self._table).get(chat)[user]['state'].default(default or None).run(conn) await self.put_connection(conn) - return result or None + return result async def get_data(self, *, chat: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None, default: typing.Optional[str] = None) -> typing.Dict: