Merge branch 'dev-1.x'

# Conflicts:
#	aiogram/__init__.py
This commit is contained in:
Alex Root Junior 2018-04-08 18:21:19 +03:00
commit 35f671c4c9
20 changed files with 1278 additions and 86 deletions

26
.gitignore vendored
View file

@ -33,6 +33,7 @@ pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/ .tox/
.pytest_cache/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
@ -43,37 +44,14 @@ coverage.xml
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# pyenv
.python-version
# virtualenv # virtualenv
.venv .venv
venv/ venv/
ENV/ ENV/
### JetBrains template # JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/ .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 # Current project
experiment.py experiment.py

View file

@ -13,7 +13,7 @@ clean:
find . -name '*.pyo' -exec $(RM) {} + find . -name '*.pyo' -exec $(RM) {} +
find . -name '*~' -exec $(RM) {} + find . -name '*~' -exec $(RM) {} +
find . -name '__pycache__' -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: tag:
@echo "Add tag: '$(AIOGRAM_VERSION)'" @echo "Add tag: '$(AIOGRAM_VERSION)'"

View file

@ -20,7 +20,7 @@ else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 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) API_VERSION = Version(3, 6)
__version__ = VERSION.version __version__ = VERSION.version

View file

@ -1,12 +1,12 @@
import os
import logging import logging
import os
from http import HTTPStatus from http import HTTPStatus
import aiohttp import aiohttp
from .. import types from .. import types
from ..utils import json
from ..utils import exceptions from ..utils import exceptions
from ..utils import json
from ..utils.helper import Helper, HelperMode, Item from ..utils.helper import Helper, HelperMode, Item
# Main aiogram logger # Main aiogram logger
@ -67,10 +67,50 @@ async def _check_result(method_name, response):
elif 'migrate_to_chat_id' in result_json: elif 'migrate_to_chat_id' in result_json:
raise exceptions.MigrateToChat(result_json['migrate_to_chat_id']) raise exceptions.MigrateToChat(result_json['migrate_to_chat_id'])
elif response.status == HTTPStatus.BAD_REQUEST: 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) 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: 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) raise exceptions.ConflictError(description)
elif response.status in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN]: 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) raise exceptions.Unauthorized(description)
elif response.status == HTTPStatus.REQUEST_ENTITY_TOO_LARGE: elif response.status == HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
raise exceptions.NetworkError('File too large for uploading. ' raise exceptions.NetworkError('File too large for uploading. '

View file

@ -1,8 +1,10 @@
import asyncio import asyncio
import io import io
import ssl
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
import aiohttp import aiohttp
import certifi
from . import api from . import api
from ..types import ParseMode, base from ..types import ParseMode, base
@ -55,9 +57,11 @@ class BaseBot:
self.loop = loop self.loop = loop
# aiohttp main session # aiohttp main session
self.session = aiohttp.ClientSession( ssl_context = ssl.create_default_context(cafile=certifi.where())
connector=aiohttp.TCPConnector(limit=connections_limit), connector = aiohttp.TCPConnector(limit=connections_limit, ssl_context=ssl_context,
loop=self.loop, json_serialize=json.dumps) loop=self.loop)
self.session = aiohttp.ClientSession(connector=connector, loop=self.loop,
json_serialize=json.dumps)
# Temp sessions # Temp sessions
self._temp_sessions = [] self._temp_sessions = []
@ -68,7 +72,8 @@ class BaseBot:
self.parse_mode = parse_mode self.parse_mode = parse_mode
def __del__(self): def __del__(self):
asyncio.ensure_future(self.close()) # asyncio.ensure_future(self.close())
pass
async def close(self): async def close(self):
""" """

View file

@ -249,6 +249,9 @@ class Bot(BaseBot):
:type photo: :obj:`typing.Union[base.InputFile, base.String]` :type photo: :obj:`typing.Union[base.InputFile, base.String]`
:param caption: Photo caption (may also be used when resending photos by file_id), 0-200 characters :param caption: Photo caption (may also be used when resending photos by file_id), 0-200 characters
:type caption: :obj:`typing.Union[base.String, None]` :type caption: :obj:`typing.Union[base.String, None]`
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
fixed-width text or inline URLs in your bot's message.
:type parse_mode: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound. :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]` :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 :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]` :type audio: :obj:`typing.Union[base.InputFile, base.String]`
:param caption: Audio caption, 0-200 characters :param caption: Audio caption, 0-200 characters
:type caption: :obj:`typing.Union[base.String, None]` :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 :param duration: Duration of the audio in seconds
:type duration: :obj:`typing.Union[base.Integer, None]` :type duration: :obj:`typing.Union[base.Integer, None]`
:param performer: Performer :param performer: Performer
@ -343,6 +349,9 @@ class Bot(BaseBot):
:type document: :obj:`typing.Union[base.InputFile, base.String]` :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 :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]` :type caption: :obj:`typing.Union[base.String, None]`
:param parse_mode: Send Markdown or HTML, if you want Telegram apps to show bold, italic,
fixed-width text or inline URLs in your bot's message.
:type parse_mode: :obj:`typing.Union[base.String, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound. :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
:type disable_notification: :obj:`typing.Union[base.Boolean, None]` :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 :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]` :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 :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]` :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 :param supports_streaming: Pass True, if the uploaded video is suitable for streaming
:type supports_streaming: :obj:`typing.Union[base.Boolean, None]` :type supports_streaming: :obj:`typing.Union[base.Boolean, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound. :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
@ -441,6 +453,9 @@ class Bot(BaseBot):
:type voice: :obj:`typing.Union[base.InputFile, base.String]` :type voice: :obj:`typing.Union[base.InputFile, base.String]`
:param caption: Voice message caption, 0-200 characters :param caption: Voice message caption, 0-200 characters
:type caption: :obj:`typing.Union[base.String, None]` :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 :param duration: Duration of the voice message in seconds
:type duration: :obj:`typing.Union[base.Integer, None]` :type duration: :obj:`typing.Union[base.Integer, None]`
:param disable_notification: Sends the message silently. Users will receive a notification with no sound. :param disable_notification: Sends the message silently. Users will receive a notification with no sound.
@ -1305,6 +1320,9 @@ class Bot(BaseBot):
:type inline_message_id: :obj:`typing.Union[base.String, None]` :type inline_message_id: :obj:`typing.Union[base.String, None]`
:param caption: New caption of the message :param caption: New caption of the message
:type caption: :obj:`typing.Union[base.String, None]` :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. :param reply_markup: A JSON-serialized object for an inline keyboard.
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]` :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, :return: On success, if edited message is sent by the bot, the edited Message is returned,

View file

@ -1,5 +1,8 @@
import typing import typing
from ..utils.deprecated import warn_deprecated as warn
from ..utils.exceptions import FSMStorageWarning
# Leak bucket # Leak bucket
KEY = 'key' KEY = 'key'
LAST_CALL = 'called_at' LAST_CALL = 'called_at'
@ -324,22 +327,29 @@ class DisabledStorage(BaseStorage):
chat: typing.Union[str, int, None] = None, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
default: typing.Optional[str] = None) -> typing.Dict: default: typing.Optional[str] = None) -> typing.Dict:
self._warn()
return {} return {}
async def update_data(self, *, async def update_data(self, *,
chat: typing.Union[str, int, None] = None, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None, **kwargs): data: typing.Dict = None, **kwargs):
pass self._warn()
async def set_state(self, *, async def set_state(self, *,
chat: typing.Union[str, int, None] = None, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
state: typing.Optional[typing.AnyStr] = None): state: typing.Optional[typing.AnyStr] = None):
pass self._warn()
async def set_data(self, *, async def set_data(self, *,
chat: typing.Union[str, int, None] = None, chat: typing.Union[str, int, None] = None,
user: typing.Union[str, int, None] = None, user: typing.Union[str, int, None] = None,
data: typing.Dict = None): data: typing.Dict = None):
pass self._warn()
@staticmethod
def _warn():
warn(f"You havent 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)

View file

@ -521,7 +521,7 @@ class ChatActions(helper.Helper):
:param sleep: sleep timeout :param sleep: sleep timeout
:return: :return:
""" """
await cls._do(cls.UPLOAD_PHOTO, sleep) await cls._do(cls.RECORD_VIDEO, sleep)
@classmethod @classmethod
async def upload_video(cls, sleep=None): async def upload_video(cls, sleep=None):
@ -531,7 +531,7 @@ class ChatActions(helper.Helper):
:param sleep: sleep timeout :param sleep: sleep timeout
:return: :return:
""" """
await cls._do(cls.RECORD_VIDEO, sleep) await cls._do(cls.UPLOAD_VIDEO, sleep)
@classmethod @classmethod
async def record_audio(cls, sleep=None): async def record_audio(cls, sleep=None):
@ -541,7 +541,7 @@ class ChatActions(helper.Helper):
:param sleep: sleep timeout :param sleep: sleep timeout
:return: :return:
""" """
await cls._do(cls.UPLOAD_VIDEO, sleep) await cls._do(cls.RECORD_AUDIO, sleep)
@classmethod @classmethod
async def upload_audio(cls, sleep=None): async def upload_audio(cls, sleep=None):
@ -551,7 +551,7 @@ class ChatActions(helper.Helper):
:param sleep: sleep timeout :param sleep: sleep timeout
:return: :return:
""" """
await cls._do(cls.RECORD_AUDIO, sleep) await cls._do(cls.UPLOAD_AUDIO, sleep)
@classmethod @classmethod
async def upload_document(cls, sleep=None): async def upload_document(cls, sleep=None):
@ -561,7 +561,7 @@ class ChatActions(helper.Helper):
:param sleep: sleep timeout :param sleep: sleep timeout
:return: :return:
""" """
await cls._do(cls.UPLOAD_AUDIO, sleep) await cls._do(cls.UPLOAD_DOCUMENT, sleep)
@classmethod @classmethod
async def find_location(cls, sleep=None): async def find_location(cls, sleep=None):
@ -571,7 +571,7 @@ class ChatActions(helper.Helper):
:param sleep: sleep timeout :param sleep: sleep timeout
:return: :return:
""" """
await cls._do(cls.UPLOAD_DOCUMENT, sleep) await cls._do(cls.FIND_LOCATION, sleep)
@classmethod @classmethod
async def record_video_note(cls, sleep=None): async def record_video_note(cls, sleep=None):
@ -581,7 +581,7 @@ class ChatActions(helper.Helper):
:param sleep: sleep timeout :param sleep: sleep timeout
:return: :return:
""" """
await cls._do(cls.FIND_LOCATION, sleep) await cls._do(cls.RECORD_VIDEO_NOTE, sleep)
@classmethod @classmethod
async def upload_video_note(cls, sleep=None): async def upload_video_note(cls, sleep=None):
@ -591,4 +591,4 @@ class ChatActions(helper.Helper):
:param sleep: sleep timeout :param sleep: sleep timeout
:return: :return:
""" """
await cls._do(cls.RECORD_VIDEO_NOTE, sleep) await cls._do(cls.UPLOAD_VIDEO_NOTE, sleep)

View file

@ -69,7 +69,7 @@ class InlineKeyboardMarkup(base.TelegramObject):
:return: self :return: self
:rtype: :obj:`types.InlineKeyboardMarkup` :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) self.inline_keyboard[-1].append(button)
else: else:
self.add(button) self.add(button)

View file

@ -1,10 +1,15 @@
import io import io
import logging import logging
import os import os
import time
import aiohttp
from . import base from . import base
from ..bot import api from ..bot import api
CHUNK_SIZE = 65536
log = logging.getLogger('aiogram') log = logging.getLogger('aiogram')
@ -76,6 +81,84 @@ class InputFile(base.TelegramObject):
""" """
return self.file 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): def to_python(self):
raise TypeError('Object of this type is not exportable!') raise TypeError('Object of this type is not exportable!')

View file

@ -130,7 +130,7 @@ class Message(base.TelegramObject):
command, _, args = self.text.partition(' ') command, _, args = self.text.partition(' ')
return command, args return command, args
def get_command(self): def get_command(self, pure=False):
""" """
Get command from message Get command from message
@ -138,7 +138,10 @@ class Message(base.TelegramObject):
""" """
command = self.get_full_command() command = self.get_full_command()
if command: if command:
return command[0] command = command[0]
if pure:
command, _, _ = command[1:].partition('@')
return command
def get_args(self): def get_args(self):
""" """

View file

@ -41,7 +41,7 @@ class ReplyKeyboardMarkup(base.TelegramObject):
:rtype: :obj:`types.ReplyKeyboardMarkup` :rtype: :obj:`types.ReplyKeyboardMarkup`
""" """
row = [] row = []
for index, button in enumerate(args): for index, button in enumerate(args, start=1):
row.append(button) row.append(button)
if index % self.row_width == 0: if index % self.row_width == 0:
self.keyboard.append(row) self.keyboard.append(row)

View file

@ -69,7 +69,7 @@ def deprecated(reason):
raise TypeError(repr(type(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.simplefilter('always', warning)
warnings.warn(message, category=warning, stacklevel=2) warnings.warn(message, category=warning, stacklevel=stacklevel)
warnings.simplefilter('default', warning) warnings.simplefilter('default', warning)

View file

@ -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 import time
_PREFIXES = ['Error: ', '[Error]: ', 'Bad Request: ', 'Conflict: '] _PREFIXES = ['Error: ', '[Error]: ', 'Bad Request: ', 'Conflict: ', 'Not Found: ']
def _clean_message(text): def _clean_message(text):
@ -11,10 +48,23 @@ def _clean_message(text):
class TelegramAPIError(Exception): class TelegramAPIError(Exception):
def __init__(self, message): def __init__(self, message=None):
super(TelegramAPIError, self).__init__(_clean_message(message)) 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): class AIOGramWarning(Warning):
pass pass
@ -23,6 +73,10 @@ class TimeoutWarning(AIOGramWarning):
pass pass
class FSMStorageWarning(AIOGramWarning):
pass
class ValidationError(TelegramAPIError): class ValidationError(TelegramAPIError):
pass pass
@ -31,14 +85,124 @@ class BadRequest(TelegramAPIError):
pass 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): class ConflictError(TelegramAPIError):
pass 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): class Unauthorized(TelegramAPIError):
pass 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): class NetworkError(TelegramAPIError):
pass pass
@ -55,7 +219,7 @@ class MigrateToChat(TelegramAPIError):
self.migrate_to_chat_id = chat_id self.migrate_to_chat_id = chat_id
class Throttled(Exception): class Throttled(TelegramAPIError):
def __init__(self, **kwargs): def __init__(self, **kwargs):
from ..dispatcher.storage import DELTA, EXCEEDED_COUNT, KEY, LAST_CALL, RATE_LIMIT, RESULT from ..dispatcher.storage import DELTA, EXCEEDED_COUNT, KEY, LAST_CALL, RATE_LIMIT, RESULT
self.key = kwargs.pop(KEY, '<None>') self.key = kwargs.pop(KEY, '<None>')

View file

@ -11,15 +11,15 @@ async def _startup(dispatcher: Dispatcher, skip_updates=False, callback=None):
user = await dispatcher.bot.me user = await dispatcher.bot.me
log.info(f"Bot: {user.full_name} [@{user.username}]") log.info(f"Bot: {user.full_name} [@{user.username}]")
if callable(callback):
await callback(dispatcher)
if skip_updates: if skip_updates:
await dispatcher.reset_webhook(True) await dispatcher.reset_webhook(True)
count = await dispatcher.skip_updates() count = await dispatcher.skip_updates()
if count: if count:
log.warning(f"Skipped {count} updates.") log.warning(f"Skipped {count} updates.")
if callable(callback):
await callback(dispatcher)
async def _wh_startup(app): async def _wh_startup(app):
callback = app.get('_startup_callback', None) callback = app.get('_startup_callback', None)

View file

@ -185,3 +185,14 @@ def escape_md(*content, sep=' '):
:return: :return:
""" """
return _escape(_join(*content, sep=sep)) 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'<a href="{url}">&#8203;</a>'

View file

@ -10,3 +10,5 @@ wheel>=0.30.0
rethinkdb>=2.3.0 rethinkdb>=2.3.0
sphinx>=1.6.6 sphinx>=1.6.6
sphinx-rtd-theme>=0.2.4 sphinx-rtd-theme>=0.2.4
aresponses
tox

View file

@ -1,2 +1,3 @@
aiohttp>=2.3.5 aiohttp>=2.3.5
Babel>=2.5.1 Babel>=2.5.1
certifi>=2018.01.18

View file

@ -1,4 +1,526 @@
import aiogram import aresponses
import pytest
# bot = aiogram.Bot('123456789:AABBCCDDEEFFaabbccddeeff-1234567890') from aiogram import Bot, types
# TODO: mock for aiogram.bot.api.request and then test all AI methods.
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

View file

@ -1,10 +1,14 @@
""""
Dict data set for Telegram message types
"""
USER = { USER = {
"id": 12345678, "id": 12345678,
"is_bot": False, "is_bot": False,
"first_name": "FirstName", "first_name": "FirstName",
"last_name": "LastName", "last_name": "LastName",
"username": "username", "username": "username",
"language_code": "ru-RU" "language_code": "ru"
} }
CHAT = { CHAT = {
@ -15,12 +19,38 @@ CHAT = {
"type": "private" "type": "private"
} }
MESSAGE = { PHOTO = {
"message_id": 11223, "file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg",
"from": USER, "file_size": 1101,
"chat": CHAT, "width": 90,
"date": 1508709711, "height": 51
"text": "Hi, world!" }
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 = { DOCUMENT = {
@ -30,27 +60,6 @@ DOCUMENT = {
"file_size": 21331 "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 = { ANIMATION = {
"file_name": "a9b0e0ca537aa344338f80978f0896b7.gif.mp4", "file_name": "a9b0e0ca537aa344338f80978f0896b7.gif.mp4",
"mime_type": "video/mp4", "mime_type": "video/mp4",
@ -59,6 +68,43 @@ ANIMATION = {
"file_size": 65837 "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 = { GAME = {
"title": "Karate Kido", "title": "Karate Kido",
"description": "No trees were harmed in the making of this game :)", "description": "No trees were harmed in the making of this game :)",
@ -66,6 +112,162 @@ GAME = {
"animation": ANIMATION "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_WITH_GAME = {
"message_id": 12345, "message_id": 12345,
"from": USER, "from": USER,
@ -73,3 +275,156 @@ MESSAGE_WITH_GAME = {
"date": 1508824810, "date": 1508824810,
"game": GAME "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
}