diff --git a/.gitignore b/.gitignore index 925f8dcd..6c2a9404 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 @@ -43,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 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)'" diff --git a/aiogram/__init__.py b/aiogram/__init__.py index d957be85..57884c9d 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.FINAL, build=0) +VERSION = Version(1, 2, 2, stage=Stage.FINAL, build=0) API_VERSION = Version(3, 6) __version__ = VERSION.version diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 29a53606..123c5359 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,50 @@ 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() 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/bot/base.py b/aiogram/bot/base.py index 57dd5895..56f56caa 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) # Temp sessions self._temp_sessions = [] @@ -68,7 +72,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): """ 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/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/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) diff --git a/aiogram/types/inline_keyboard.py b/aiogram/types/inline_keyboard.py index 1b5c8011..69049345 100644 --- a/aiogram/types/inline_keyboard.py +++ b/aiogram/types/inline_keyboard.py @@ -69,7 +69,7 @@ class InlineKeyboardMarkup(base.TelegramObject): :return: self :rtype: :obj:`types.InlineKeyboardMarkup` """ - 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) diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index ff5ab1f0..9d42c6b7 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=CHUNK_SIZE): + """ + 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=CHUNK_SIZE): + """ + 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=CHUNK_SIZE): + """ + 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!') diff --git a/aiogram/types/message.py b/aiogram/types/message.py index f64f1cdb..48ea1b29 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -130,7 +130,7 @@ class Message(base.TelegramObject): command, _, args = self.text.partition(' ') return command, args - def get_command(self): + def get_command(self, pure=False): """ Get command from message @@ -138,7 +138,10 @@ class Message(base.TelegramObject): """ command = self.get_full_command() if command: - return command[0] + command = command[0] + if pure: + command, _, _ = command[1:].partition('@') + return command def get_args(self): """ diff --git a/aiogram/types/reply_keyboard.py b/aiogram/types/reply_keyboard.py index e09ae4c4..8eda21f9 100644 --- a/aiogram/types/reply_keyboard.py +++ b/aiogram/types/reply_keyboard.py @@ -41,7 +41,7 @@ class ReplyKeyboardMarkup(base.TelegramObject): :rtype: :obj:`types.ReplyKeyboardMarkup` """ row = [] - for index, button in enumerate(args): + for index, button in enumerate(args, start=1): row.append(button) if index % self.row_width == 0: self.keyboard.append(row) 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..b7bacb1e 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -1,6 +1,43 @@ +""" +TelegramAPIError + ValidationError + Throttled + BadRequest + MessageError + MessageNotModified + MessageToForwardNotFound + MessageToDeleteNotFound + MessageIdentifierNotSpecified + ChatNotFound + InvalidQueryID + InvalidPeerID + InvalidHTTPUrlContent + WrongFileIdentifier + GroupDeactivated + BadWebhook + WebhookRequireHTTPS + BadWebhookPort + CantParseUrl + NotFound + MethodNotKnown + PhotoAsInputFileRequired + 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 +48,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 +73,10 @@ class TimeoutWarning(AIOGramWarning): pass +class FSMStorageWarning(AIOGramWarning): + pass + + class ValidationError(TelegramAPIError): pass @@ -31,14 +85,124 @@ class BadRequest(TelegramAPIError): pass +class MessageError(BadRequest): + pass + + +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' + + +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 PhotoAsInputFileRequired(BadRequest, _MatchErrorMixin): + """ + Will be raised when you try to set chat photo from file ID. + """ + match = 'Photo should be uploaded as an InputFile' + + +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 +219,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, '') 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) diff --git a/aiogram/utils/markdown.py b/aiogram/utils/markdown.py index 1e64c106..89a23d94 100644 --- a/aiogram/utils/markdown.py +++ b/aiogram/utils/markdown.py @@ -185,3 +185,14 @@ def escape_md(*content, sep=' '): :return: """ return _escape(_join(*content, sep=sep)) + + +def hide_link(url): + """ + Hide URL (HTML only) + Can be used for adding an image to a text message + + :param url: + :return: + """ + return f'' diff --git a/dev_requirements.txt b/dev_requirements.txt index 93a20597..d63ed3d1 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 +tox \ No newline at end of file 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 diff --git a/tests/test_bot.py b/tests/test_bot.py index 9c0f860d..c3a29687 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1,4 +1,526 @@ -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' + + +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): + """ Bot fixture """ + _bot = Bot(TOKEN, loop=event_loop, parse_mode=types.ParseMode.MARKDOWN) + yield _bot + await _bot.close() + + +@pytest.mark.asyncio +async def test_get_me(bot: Bot, event_loop): + """ getMe method test """ + from .types.dataset import USER + user = types.User(**USER) + + async with FakeTelegram(message_dict=USER, loop=event_loop): + result = await bot.me + assert result == user + + +@pytest.mark.asyncio +async def test_send_message(bot: Bot, event_loop): + """ sendMessage method test """ + from .types.dataset import MESSAGE + msg = types.Message(**MESSAGE) + + 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) + + async with FakeTelegram(message_dict=FORWARDED_MESSAGE, loop=event_loop): + 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_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) + 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 with file_id """ + 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 with file_id """ + 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 + + +@pytest.mark.asyncio +async def test_send_video(bot: Bot, event_loop): + """ sendVideo method test with file_id """ + from .types.dataset import MESSAGE_WITH_VIDEO, VIDEO + msg = types.Message(**MESSAGE_WITH_VIDEO) + video = types.Video(**VIDEO) + + async with FakeTelegram(message_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 + + +@pytest.mark.asyncio +async def test_send_voice(bot: Bot, event_loop): + """ sendVoice method test with file_id """ + from .types.dataset import MESSAGE_WITH_VOICE, VOICE + msg = types.Message(**MESSAGE_WITH_VOICE) + voice = types.Voice(**VOICE) + + async with FakeTelegram(message_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 + + +@pytest.mark.asyncio +async def test_send_video_note(bot: Bot, event_loop): + """ sendVideoNote method test with file_id """ + from .types.dataset import MESSAGE_WITH_VIDEO_NOTE, VIDEO_NOTE + msg = types.Message(**MESSAGE_WITH_VIDEO_NOTE) + video_note = types.VideoNote(**VIDEO_NOTE) + + async with FakeTelegram(message_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 + + +@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 + + +@pytest.mark.asyncio +async def test_send_location(bot: Bot, event_loop): + """ sendLocation method test """ + from .types.dataset import MESSAGE_WITH_LOCATION, LOCATION + msg = types.Message(**MESSAGE_WITH_LOCATION) + location = types.Location(**LOCATION) + + async with FakeTelegram(message_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 + + +@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): + """ stopMessageLiveLocation 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) + assert 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 + + +@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 + + +@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 + + +@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) + + +@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) + + +@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 + + +@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 + + +@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 + + +@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 + + +@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 + + +@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 + assert len(result) == 2 + + +@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 + + +@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 diff --git a/tests/types/dataset.py b/tests/types/dataset.py index 0a991a4a..1333f178 100644 --- a/tests/types/dataset.py +++ b/tests/types/dataset.py @@ -1,10 +1,14 @@ +"""" +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 = { @@ -15,12 +19,38 @@ 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": 236, + "mime_type": "audio/mpeg3", + "title": "The Best Song", + "performer": "The Best Singer", + "file_id": "CQADAgADbQEAAsnrIUpNoRRNsH7_hAI", + "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", + "last_name": "Smith", } DOCUMENT = { @@ -30,27 +60,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", "mime_type": "video/mp4", @@ -59,6 +68,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 :)", @@ -66,6 +112,162 @@ GAME = { "animation": ANIMATION } +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": 50.693416, + "longitude": 30.624605 +} + +VENUE = { + "location": LOCATION, + "title": "Venue Name", + "address": "Venue Address", + "foursquare_id": "4e6f2cec483bad563d150f98" +} + +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 +} + +VIDEO_NOTE = { + "duration": 4, + "length": 240, + "thumb": PHOTO, + "file_id": "AbCdEfGhIjKlMnOpQrStUvWxYz", + "file_size": 186562 +} + +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 = { + "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": 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 = {} + +MESSAGE = { + "message_id": 11223, + "from": USER, + "chat": CHAT, + "date": 1508709711, + "text": "Hi, world!" +} + +MESSAGE_WITH_AUDIO = { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508739776, + "audio": AUDIO, + "caption": "This is my favourite song" +} + +MESSAGE_WITH_AUTHOR_SIGNATURE = {} + +MESSAGE_WITH_CHANNEL_CHAT_CREATED = {} + +MESSAGE_WITH_CONTACT = { + "message_id": 56006, + "from": USER, + "chat": CHAT, + "date": 1522850298, + "contact": CONTACT +} + +MESSAGE_WITH_DELETE_CHAT_PHOTO = {} + +MESSAGE_WITH_DOCUMENT = { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508768012, + "document": DOCUMENT, + "caption": "Read my document" +} + +MESSAGE_WITH_EDIT_DATE = {} + +MESSAGE_WITH_ENTITIES = {} + MESSAGE_WITH_GAME = { "message_id": 12345, "from": USER, @@ -73,3 +275,156 @@ MESSAGE_WITH_GAME = { "date": 1508824810, "game": GAME } + +MESSAGE_WITH_GROUP_CHAT_CREATED = {} + +MESSAGE_WITH_INVOICE = { + "message_id": 9772, + "from": USER, + "chat": CHAT, + "date": 1508761719, + "invoice": INVOICE +} + +MESSAGE_WITH_LEFT_CHAT_MEMBER = {} + +MESSAGE_WITH_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 = { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508825154, + "photo": [PHOTO, PHOTO, PHOTO, 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 = {} + +MESSAGE_WITH_STICKER = { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508771450, + "sticker": STICKER +} + +MESSAGE_WITH_SUCCESSFUL_PAYMENT = { + "message_id": 9768, + "from": USER, + "chat": CHAT, + "date": 1508761169, + "successful_payment": SUCCESSFUL_PAYMENT +} + +MESSAGE_WITH_SUPERGROUP_CHAT_CREATED = {} + +MESSAGE_WITH_VENUE = { + "message_id": 56004, + "from": USER, + "chat": CHAT, + "date": 1522849819, + "location": LOCATION, + "venue": VENUE +} + +MESSAGE_WITH_VIDEO = { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508756494, + "video": VIDEO, + "caption": "description" +} + +MESSAGE_WITH_VIDEO_NOTE = { + "message_id": 55934, + "from": USER, + "chat": CHAT, + "date": 1522835890, + "video_note": VIDEO_NOTE +} + +MESSAGE_WITH_VOICE = { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508768403, + "voice": VOICE +} + +PRE_CHECKOUT_QUERY = { + "id": "262181558630368727", + "from": USER, + "currency": "USD", + "total_amount": 6250, + "invoice_payload": "HAPPY FRIDAYS COUPON" +} + +REPLY_MESSAGE = { + "message_id": 12345, + "from": USER, + "chat": CHAT, + "date": 1508751866, + "reply_to_message": MESSAGE, + "text": "Reply to quoted message" +} + +SHIPPING_QUERY = { + "id": "262181558684397422", + "from": USER, + "invoice_payload": "HAPPY FRIDAYS COUPON", + "shipping_address": SHIPPING_ADDRESS +} + +USER_PROFILE_PHOTOS = { + "total_count": 1, "photos": [ + [PHOTO, PHOTO, PHOTO] + ] +} + +FILE = { + "file_id": "XXXYYYZZZ", + "file_size": 5254, + "file_path": "voice\/file_8" +} + +INVITE_LINK = 'https://t.me/joinchat/AbCdEfjKILDADwdd123' + +UPDATE = { + "update_id": 123456789, + "message": MESSAGE +} + +WEBHOOK_INFO = { + "url": "", + "has_custom_certificate": False, + "pending_update_count": 0 +}