Merge branch 'dev-1.x'

# Conflicts:
#	aiogram/__init__.py
This commit is contained in:
Alex Root Junior 2018-08-03 00:18:27 +03:00
commit 5f7a495b10
22 changed files with 581 additions and 18 deletions

View file

@ -10,5 +10,5 @@ except ImportError:
else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
__version__ = '1.3.3'
__version__ = '1.4'
__api_version__ = '3.6'

View file

@ -184,6 +184,7 @@ class Methods(Helper):
SEND_AUDIO = Item() # sendAudio
SEND_DOCUMENT = Item() # sendDocument
SEND_VIDEO = Item() # sendVideo
SEND_ANIMATION = Item() # sendAnimation
SEND_VOICE = Item() # sendVoice
SEND_VIDEO_NOTE = Item() # sendVideoNote
SEND_MEDIA_GROUP = Item() # sendMediaGroup
@ -218,6 +219,7 @@ class Methods(Helper):
# Updating messages
EDIT_MESSAGE_TEXT = Item() # editMessageText
EDIT_MESSAGE_CAPTION = Item() # editMessageCaption
EDIT_MESSAGE_MEDIA = Item() # editMessageMedia
EDIT_MESSAGE_REPLY_MARKUP = Item() # editMessageReplyMarkup
DELETE_MESSAGE = Item() # deleteMessage
@ -238,6 +240,9 @@ class Methods(Helper):
ANSWER_SHIPPING_QUERY = Item() # answerShippingQuery
ANSWER_PRE_CHECKOUT_QUERY = Item() # answerPreCheckoutQuery
# Telegram Passport
SET_PASSPORT_DATA_ERRORS = Item() # setPassportDataErrors
# Games
SEND_GAME = Item() # sendGame
SET_GAME_SCORE = Item() # setGameScore

View file

@ -278,6 +278,7 @@ class Bot(BaseBot):
duration: typing.Union[base.Integer, None] = None,
performer: typing.Union[base.String, None] = None,
title: typing.Union[base.String, None] = None,
thumb: typing.Union[base.InputFile, base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,
reply_markup: typing.Union[types.InlineKeyboardMarkup,
@ -306,6 +307,8 @@ class Bot(BaseBot):
:param performer: Performer
:type performer: :obj:`typing.Union[base.String, None]`
:param title: Track name
:param thumb: Thumbnail of the file sent.
:param :obj:`typing.Union[base.InputFile, base.String, None]`
:type title: :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]`
@ -328,6 +331,7 @@ class Bot(BaseBot):
async def send_document(self, chat_id: typing.Union[base.Integer, base.String],
document: typing.Union[base.InputFile, base.String],
thumb: typing.Union[base.InputFile, base.String, None] = None,
caption: typing.Union[base.String, None] = None,
parse_mode: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
@ -347,6 +351,8 @@ class Bot(BaseBot):
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
:param document: File to send.
:type document: :obj:`typing.Union[base.InputFile, base.String]`
:param thumb: Thumbnail of the file sent.
:param :obj:`typing.Union[base.InputFile, base.String, None]`
: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,
@ -376,6 +382,7 @@ class Bot(BaseBot):
duration: typing.Union[base.Integer, None] = None,
width: typing.Union[base.Integer, None] = None,
height: typing.Union[base.Integer, None] = None,
thumb: typing.Union[base.InputFile, base.String, None] = None,
caption: typing.Union[base.String, None] = None,
parse_mode: typing.Union[base.String, None] = None,
supports_streaming: typing.Union[base.Boolean, None] = None,
@ -401,6 +408,8 @@ class Bot(BaseBot):
:type width: :obj:`typing.Union[base.Integer, None]`
:param height: Video height
:type height: :obj:`typing.Union[base.Integer, None]`
:param thumb: Thumbnail of the file sent.
:param :obj:`typing.Union[base.InputFile, base.String, 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,
@ -427,6 +436,60 @@ class Bot(BaseBot):
return types.Message(**result)
async def send_animation(self,
chat_id: typing.Union[base.Integer, base.String],
animation: typing.Union[base.InputFile, base.String],
duration: typing.Union[base.Integer, None] = None,
width: typing.Union[base.Integer, None] = None,
height: typing.Union[base.Integer, None] = None,
thumb: typing.Union[typing.Union[base.InputFile, base.String], None] = None,
caption: typing.Union[base.String, None] = None,
parse_mode: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,
reply_markup: typing.Union[typing.Union[types.InlineKeyboardMarkup,
types.ReplyKeyboardMarkup,
types.ReplyKeyboardRemove,
types.ForceReply], None] = None,) -> types.Message:
"""
Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
On success, the sent Message is returned.
Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future.
Source https://core.telegram.org/bots/api#sendanimation
:param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername)
:type chat_id: :obj:`typing.Union[base.Integer, base.String]`
:param animation: Animation to send. Pass a file_id as String to send an animation that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get an animation from the Internet, or upload a new animation using multipart/form-data.
:type animation: :obj:`typing.Union[base.InputFile, base.String]`
:param duration: Duration of sent animation in seconds
:type duration: :obj:`typing.Union[base.Integer, None]`
:param width: Animation width
:type width: :obj:`typing.Union[base.Integer, None]`
:param height: Animation height
:type height: :obj:`typing.Union[base.Integer, None]`
:param thumb: Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnails width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails cant be reused and can be only uploaded as a new file, so you can pass attach://<file_attach_name> if the thumbnail was uploaded using multipart/form-data under <file_attach_name>.
:type thumb: :obj:`typing.Union[typing.Union[base.InputFile, base.String], None]`
:param caption: Animation caption (may also be used when resending animation 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 the media caption.
: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
:type reply_to_message_id: :obj:`typing.Union[base.Integer, None]`
:param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user.
:type reply_markup: :obj:`typing.Union[typing.Union[types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.ReplyKeyboardRemove, types.ForceReply], None]`
:return: On success, the sent Message is returned.
:rtype: :obj:`types.Message`
"""
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals(), exclude=["animation"])
result = await self.send_file("animation", api.Methods.SEND_ANIMATION, thumb, payload)
return types.Message(**result)
async def send_voice(self, chat_id: typing.Union[base.Integer, base.String],
voice: typing.Union[base.InputFile, base.String],
caption: typing.Union[base.String, None] = None,
@ -481,6 +544,7 @@ class Bot(BaseBot):
video_note: typing.Union[base.InputFile, base.String],
duration: typing.Union[base.Integer, None] = None,
length: typing.Union[base.Integer, None] = None,
thumb: typing.Union[base.InputFile, base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,
reply_markup: typing.Union[types.InlineKeyboardMarkup,
@ -501,6 +565,8 @@ class Bot(BaseBot):
:type duration: :obj:`typing.Union[base.Integer, None]`
:param length: Video width and height
:type length: :obj:`typing.Union[base.Integer, None]`
:param thumb: Thumbnail of the file sent.
:param :obj:`typing.Union[base.InputFile, 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
@ -664,6 +730,7 @@ class Bot(BaseBot):
latitude: base.Float, longitude: base.Float,
title: base.String, address: base.String,
foursquare_id: typing.Union[base.String, None] = None,
foursquare_type: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,
reply_markup: typing.Union[types.InlineKeyboardMarkup,
@ -687,6 +754,8 @@ class Bot(BaseBot):
:type address: :obj:`base.String`
:param foursquare_id: Foursquare identifier of the venue
:type foursquare_id: :obj:`typing.Union[base.String, None]`
:param foursquare_type: Foursquare type of the venue, if known.
:type foursquare_type: :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
@ -706,6 +775,7 @@ class Bot(BaseBot):
async def send_contact(self, chat_id: typing.Union[base.Integer, base.String],
phone_number: base.String, first_name: base.String,
last_name: typing.Union[base.String, None] = None,
vcard: typing.Union[base.String, None] = None,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,
reply_markup: typing.Union[types.InlineKeyboardMarkup,
@ -725,6 +795,8 @@ class Bot(BaseBot):
:type first_name: :obj:`base.String`
:param last_name: Contact's last name
:type last_name: :obj:`typing.Union[base.String, None]`
:param vcard: vcard
:type vcard: :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
@ -1341,6 +1413,54 @@ class Bot(BaseBot):
return types.Message(**result)
async def edit_message_media(self,
media: types.InputMedia,
chat_id: typing.Union[typing.Union[base.Integer, base.String], None] = None,
message_id: typing.Union[base.Integer, None] = None,
inline_message_id: typing.Union[base.String, None] = None,
reply_markup: typing.Union[types.InlineKeyboardMarkup, None] = None,
) -> typing.Union[types.Message, base.Boolean]:
"""
Use this method to edit audio, document, photo, or video messages.
If a message is a part of a message album, then it can be edited only to a photo or a video.
Otherwise, message type can be changed arbitrarily.
When inline message is edited, new file can't be uploaded.
Use previously uploaded file via its file_id or specify a URL.
On success, if the edited message was sent by the bot,
the edited Message is returned, otherwise True is returned.
Source https://core.telegram.org/bots/api#editmessagemedia
:param chat_id: Required if inline_message_id is not specified.
:type chat_id: :obj:`typing.Union[typing.Union[base.Integer, base.String], None]`
:param message_id: Required if inline_message_id is not specified. Identifier of the sent message
:type message_id: :obj:`typing.Union[base.Integer, None]`
:param inline_message_id: Required if chat_id and message_id are not specified. Identifier of the inline message
:type inline_message_id: :obj:`typing.Union[base.String, None]`
:param media: A JSON-serialized object for a new media content of the message
:type media: :obj:`types.InputMedia`
:param reply_markup: A JSON-serialized object for a new inline keyboard.
:type reply_markup: :obj:`typing.Union[types.InlineKeyboardMarkup, None]`
:return: On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned.
:rtype: :obj:`typing.Union[types.Message, base.Boolean]`
"""
if isinstance(media, types.InputMedia) and media.file:
files = {media.attachment_key: media.file}
else:
files = None
reply_markup = prepare_arg(reply_markup)
payload = generate_payload(**locals())
result = await self.request(api.Methods.EDIT_MESSAGE_MEDIA, payload, files)
if isinstance(result, bool):
return result
return types.Message(**result)
async def edit_message_reply_markup(self,
chat_id: typing.Union[base.Integer, base.String, None] = None,
message_id: typing.Union[base.Integer, None] = None,
@ -1759,6 +1879,38 @@ class Bot(BaseBot):
# === Games ===
# https://core.telegram.org/bots/api#games
async def set_passport_data_errors(self,
user_id: base.Integer,
errors: typing.List[types.PassportElementError]) -> base.Boolean:
"""
Informs a user that some of the Telegram Passport elements they provided contains errors.
The user will not be able to re-submit their Passport to you until the errors are fixed
(the contents of the field for which you returned the error must change).
Returns True on success.
Use this if the data submitted by the user doesn't satisfy the standards your service
requires for any reason. For example, if a birthday date seems invalid, a submitted document
is blurry, a scan shows evidence of tampering, etc. Supply some details in the error message
to make sure the user knows how to correct the issues.
Source https://core.telegram.org/bots/api#setpassportdataerrors
:param user_id: User identifier
:type user_id: :obj:`base.Integer`
:param errors: A JSON-serialized array describing the errors
:type errors: :obj:`typing.List[types.PassportElementError]`
:return: Returns True on success.
:rtype: :obj:`base.Boolean`
"""
errors = prepare_arg(errors)
payload = generate_payload(**locals())
result = await self.request(api.Methods.SET_PASSPORT_DATA_ERRORS, payload)
return result
# === Games ===
# https://core.telegram.org/bots/api#games
async def send_game(self, chat_id: base.Integer, game_short_name: base.String,
disable_notification: typing.Union[base.Boolean, None] = None,
reply_to_message_id: typing.Union[base.Integer, None] = None,

View file

@ -7,6 +7,7 @@ import typing
from typing import Dict, List, Optional, Union
from aiohttp import web
from aiohttp.web_exceptions import HTTPGone
from .. import types
from ..bot import api
@ -127,8 +128,14 @@ class WebhookRequestHandler(web.View):
response = self.get_response(results)
if response:
return response.get_web_response()
return web.Response(text='ok')
web_response = response.get_web_response()
else:
web_response = web.Response(text='ok')
if self.request.app.get('RETRY_AFTER', None):
web_response.headers['Retry-After'] = self.request.app['RETRY_AFTER']
return web_response
async def get(self):
self.validate_ip()
@ -245,6 +252,19 @@ class WebhookRequestHandler(web.View):
context.set_value('TELEGRAM_IP', ip_address)
class GoneRequestHandler(web.View):
"""
If a webhook returns the HTTP error 410 Gone for all requests for more than 23 hours successively,
it can be automatically removed.
"""
async def get(self):
raise HTTPGone()
async def post(self):
raise HTTPGone()
def configure_app(dispatcher, app: web.Application, path=DEFAULT_WEB_PATH):
"""
You can prepare web.Application for working with webhook handler.

View file

@ -11,6 +11,8 @@ from .chat_photo import ChatPhoto
from .chosen_inline_result import ChosenInlineResult
from .contact import Contact
from .document import Document
from .encrypted_credentials import EncryptedCredentials
from .encrypted_passport_element import EncryptedPassportElement
from .file import File
from .force_reply import ForceReply
from .game import Game
@ -24,7 +26,8 @@ from .inline_query_result import InlineQueryResult, InlineQueryResultArticle, In
InlineQueryResultGame, InlineQueryResultGif, InlineQueryResultLocation, InlineQueryResultMpeg4Gif, \
InlineQueryResultPhoto, InlineQueryResultVenue, InlineQueryResultVideo, InlineQueryResultVoice
from .input_file import InputFile
from .input_media import InputMediaPhoto, InputMediaVideo, MediaGroup
from .input_media import InputMedia, InputMediaAnimation, InputMediaAudio, InputMediaDocument, InputMediaPhoto, \
InputMediaVideo, MediaGroup
from .input_message_content import InputContactMessageContent, InputLocationMessageContent, InputMessageContent, \
InputTextMessageContent, InputVenueMessageContent
from .invoice import Invoice
@ -34,6 +37,11 @@ from .mask_position import MaskPosition
from .message import ContentType, Message, ParseMode
from .message_entity import MessageEntity, MessageEntityType
from .order_info import OrderInfo
from .passport_data import PassportData
from .passport_element_error import PassportElementError, PassportElementErrorDataField, PassportElementErrorFile, \
PassportElementErrorFiles, PassportElementErrorFrontSide, PassportElementErrorReverseSide, \
PassportElementErrorSelfie
from .passport_file import PassportFile
from .photo_size import PhotoSize
from .pre_checkout_query import PreCheckoutQuery
from .reply_keyboard import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
@ -70,6 +78,8 @@ __all__ = (
'Contact',
'ContentType',
'Document',
'EncryptedCredentials',
'EncryptedPassportElement',
'File',
'ForceReply',
'Game',
@ -100,6 +110,10 @@ __all__ = (
'InlineQueryResultVoice',
'InputContactMessageContent',
'InputFile',
'InputMedia',
'InputMediaAnimation',
'InputMediaAudio',
'InputMediaDocument',
'InputMediaPhoto',
'InputMediaVideo',
'InputLocationMessageContent',
@ -116,6 +130,15 @@ __all__ = (
'MessageEntity',
'MessageEntityType',
'OrderInfo',
'PassportData',
'PassportElementError',
'PassportElementErrorDataField',
'PassportElementErrorFile',
'PassportElementErrorFiles',
'PassportElementErrorFrontSide',
'PassportElementErrorReverseSide',
'PassportElementErrorSelfie',
'PassportFile',
'ParseMode',
'PhotoSize',
'PreCheckoutQuery',

View file

@ -1,6 +1,7 @@
from . import base
from . import fields
from . import mixins
from .photo_size import PhotoSize
class Audio(base.TelegramObject, mixins.Downloadable):
@ -15,3 +16,4 @@ class Audio(base.TelegramObject, mixins.Downloadable):
title: base.String = fields.Field()
mime_type: base.String = fields.Field()
file_size: base.Integer = fields.Field()
thumb: PhotoSize = fields.Field(base=PhotoSize)

View file

@ -12,6 +12,7 @@ class Contact(base.TelegramObject):
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
user_id: base.Integer = fields.Field()
vcard: base.String = fields.Field()
@property
def full_name(self):

View file

@ -0,0 +1,16 @@
from . import base
from . import fields
class EncryptedCredentials(base.TelegramObject):
"""
Contains data required for decrypting and authenticating EncryptedPassportElement.
See the Telegram Passport Documentation for a complete description of the data decryption
and authentication processes.
https://core.telegram.org/bots/api#encryptedcredentials
"""
data: base.String = fields.Field()
hash: base.String = fields.Field()
secret: base.String = fields.Field()

View file

@ -0,0 +1,21 @@
from . import base
from . import fields
import typing
from .passport_file import PassportFile
class EncryptedPassportElement(base.TelegramObject):
"""
Contains information about documents or other Telegram Passport elements shared with the bot by the user.
https://core.telegram.org/bots/api#encryptedpassportelement
"""
type: base.String = fields.Field()
data: base.String = fields.Field()
phone_number: base.String = fields.Field()
email: base.String = fields.Field()
files: typing.List[PassportFile] = fields.ListField(base=PassportFile)
front_side: PassportFile = fields.Field(base=PassportFile)
reverse_side: PassportFile = fields.Field(base=PassportFile)
selfie: PassportFile = fields.Field(base=PassportFile)

View file

@ -405,6 +405,7 @@ class InlineQueryResultVenue(InlineQueryResult):
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
foursquare_type: base.String = fields.Field()
def __init__(self, *,
id: base.String,
@ -417,12 +418,14 @@ class InlineQueryResultVenue(InlineQueryResult):
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None):
thumb_height: typing.Optional[base.Integer] = None,
foursquare_type: typing.Optional[base.String] = None):
super(InlineQueryResultVenue, self).__init__(id=id, latitude=latitude, longitude=longitude,
title=title, address=address, foursquare_id=foursquare_id,
reply_markup=reply_markup,
input_message_content=input_message_content, thumb_url=thumb_url,
thumb_width=thumb_width, thumb_height=thumb_height)
thumb_width=thumb_width, thumb_height=thumb_height,
foursquare_type=foursquare_type)
class InlineQueryResultContact(InlineQueryResult):
@ -441,10 +444,12 @@ class InlineQueryResultContact(InlineQueryResult):
phone_number: base.String = fields.Field()
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
vcard: base.String = fields.Field()
input_message_content: InputMessageContent = fields.Field(base=InputMessageContent)
thumb_url: base.String = fields.Field()
thumb_width: base.Integer = fields.Field()
thumb_height: base.Integer = fields.Field()
foursquare_type: base.String = fields.Field()
def __init__(self, *,
id: base.String,
@ -455,12 +460,14 @@ class InlineQueryResultContact(InlineQueryResult):
input_message_content: typing.Optional[InputMessageContent] = None,
thumb_url: typing.Optional[base.String] = None,
thumb_width: typing.Optional[base.Integer] = None,
thumb_height: typing.Optional[base.Integer] = None):
thumb_height: typing.Optional[base.Integer] = None,
foursquare_type: typing.Optional[base.String] = None):
super(InlineQueryResultContact, self).__init__(id=id, phone_number=phone_number,
first_name=first_name, last_name=last_name,
reply_markup=reply_markup,
input_message_content=input_message_content, thumb_url=thumb_url,
thumb_width=thumb_width, thumb_height=thumb_height)
thumb_width=thumb_width, thumb_height=thumb_height,
foursquare_type=foursquare_type)
class InlineQueryResultGame(InlineQueryResult):

View file

@ -21,6 +21,7 @@ class InputMedia(base.TelegramObject):
"""
type: base.String = fields.Field(default='photo')
media: base.String = fields.Field()
thumb: typing.Union[base.InputFile, base.String] = fields.Field()
caption: base.String = fields.Field()
parse_mode: base.Boolean = fields.Field()
@ -51,6 +52,77 @@ class InputMedia(base.TelegramObject):
self.conf['attachment_key'] = value
class InputMediaAnimation(InputMedia):
"""
Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
https://core.telegram.org/bots/api#inputmediaanimation
"""
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
def __init__(self, media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
parse_mode: base.Boolean = None, **kwargs):
super(InputMediaAnimation, self).__init__(type='animation', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode, conf=kwargs)
if isinstance(media, (io.IOBase, InputFile)):
self.file = media
class InputMediaDocument(InputMedia):
"""
Represents a photo to be sent.
https://core.telegram.org/bots/api#inputmediadocument
"""
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
super(InputMediaDocument, self).__init__(type='document', media=media, thumb=thumb,
caption=caption, parse_mode=parse_mode,
conf=kwargs)
if isinstance(media, (io.IOBase, InputFile)):
self.file = media
class InputMediaAudio(InputMedia):
"""
Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
https://core.telegram.org/bots/api#inputmediaanimation
"""
width: base.Integer = fields.Field()
height: base.Integer = fields.Field()
duration: base.Integer = fields.Field()
performer: base.String = fields.Field()
title: base.String = fields.Field()
def __init__(self, media: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.Boolean = None, **kwargs):
super(InputMediaAudio, self).__init__(type='audio', media=media, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
performer=performer, title=title,
parse_mode=parse_mode, conf=kwargs)
if isinstance(media, (io.IOBase, InputFile)):
self.file = media
class InputMediaPhoto(InputMedia):
"""
Represents a photo to be sent.
@ -58,8 +130,10 @@ class InputMediaPhoto(InputMedia):
https://core.telegram.org/bots/api#inputmediaphoto
"""
def __init__(self, media: base.InputFile, caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
super(InputMediaPhoto, self).__init__(type='photo', media=media, caption=caption, parse_mode=parse_mode,
def __init__(self, media: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.Boolean = None, **kwargs):
super(InputMediaPhoto, self).__init__(type='photo', media=media, thumb=thumb,
caption=caption, parse_mode=parse_mode,
conf=kwargs)
if isinstance(media, (io.IOBase, InputFile)):
@ -126,14 +200,89 @@ class MediaGroup(base.TelegramObject):
media = InputMediaPhoto(**media)
elif media_type == 'video':
media = InputMediaVideo(**media)
# elif media_type == 'document':
# media = InputMediaDocument(**media)
# elif media_type == 'audio':
# media = InputMediaAudio(**media)
# elif media_type == 'animation':
# media = InputMediaAnimation(**media)
else:
raise TypeError(f"Invalid media type '{media_type}'!")
elif not isinstance(media, InputMedia):
raise TypeError(f"Media must be an instance of InputMedia or dict, not {type(media).__name__}")
elif media.type in ['document', 'audio', 'animation']:
raise ValueError(f"This type of media is not supported by media groups!")
self.media.append(media)
'''
def attach_animation(self, animation: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None, duration: base.Integer = None,
parse_mode: base.Boolean = None):
"""
Attach animation
:param animation:
:param thumb:
:param caption:
:param width:
:param height:
:param duration:
:param parse_mode:
"""
if not isinstance(animation, InputMedia):
animation = InputMediaAnimation(media=animation, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
parse_mode=parse_mode)
self.attach(animation)
def attach_audio(self, audio: base.InputFile,
thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None,
width: base.Integer = None, height: base.Integer = None,
duration: base.Integer = None,
performer: base.String = None,
title: base.String = None,
parse_mode: base.Boolean = None):
"""
Attach animation
:param audio:
:param thumb:
:param caption:
:param width:
:param height:
:param duration:
:param performer:
:param title:
:param parse_mode:
"""
if not isinstance(audio, InputMedia):
audio = InputMediaAudio(media=audio, thumb=thumb, caption=caption,
width=width, height=height, duration=duration,
performer=performer, title=title,
parse_mode=parse_mode)
self.attach(audio)
def attach_document(self, document: base.InputFile, thumb: typing.Union[base.InputFile, base.String] = None,
caption: base.String = None, parse_mode: base.Boolean = None):
"""
Attach document
:param parse_mode:
:param caption:
:param thumb:
:param document:
"""
if not isinstance(document, InputMedia):
document = InputMediaDocument(media=document, thumb=thumb, caption=caption, parse_mode=parse_mode)
self.attach(document)
'''
def attach_photo(self, photo: typing.Union[InputMediaPhoto, base.InputFile],
caption: base.String = None):
"""

View file

@ -27,6 +27,7 @@ class InputContactMessageContent(InputMessageContent):
phone_number: base.String = fields.Field()
first_name: base.String = fields.Field()
last_name: base.String = fields.Field()
vcard: base.String = fields.Field()
def __init__(self, phone_number: base.String,
first_name: typing.Optional[base.String] = None,

View file

@ -5,6 +5,7 @@ import typing
from . import base
from . import fields
from .animation import Animation
from .audio import Audio
from .chat import Chat
from .contact import Contact
@ -13,6 +14,7 @@ from .game import Game
from .invoice import Invoice
from .location import Location
from .message_entity import MessageEntity
from .passport_data import PassportData
from .photo_size import PhotoSize
from .sticker import Sticker
from .successful_payment import SuccessfulPayment
@ -49,6 +51,7 @@ class Message(base.TelegramObject):
caption_entities: typing.List[MessageEntity] = fields.ListField(base=MessageEntity)
audio: Audio = fields.Field(base=Audio)
document: Document = fields.Field(base=Document)
animation: Animation = fields.Field(base=Animation)
game: Game = fields.Field(base=Game)
photo: typing.List[PhotoSize] = fields.ListField(base=PhotoSize)
sticker: Sticker = fields.Field(base=Sticker)
@ -73,6 +76,7 @@ class Message(base.TelegramObject):
invoice: Invoice = fields.Field(base=Invoice)
successful_payment: SuccessfulPayment = fields.Field(base=SuccessfulPayment)
connected_website: base.String = fields.Field()
passport_data: PassportData = fields.Field(base=PassportData)
@property
@functools.lru_cache()
@ -81,6 +85,8 @@ class Message(base.TelegramObject):
return ContentType.TEXT[0]
elif self.audio:
return ContentType.AUDIO[0]
elif self.animation:
return ContentType.ANIMATION[0]
elif self.document:
return ContentType.DOCUMENT[0]
elif self.game:
@ -125,6 +131,8 @@ class Message(base.TelegramObject):
return ContentType.DELETE_CHAT_PHOTO[0]
elif self.group_chat_created:
return ContentType.GROUP_CHAT_CREATED[0]
elif self.passport_data:
return ContentType.PASSPORT_DATA[0]
else:
return ContentType.UNKNOWN[0]
@ -748,6 +756,7 @@ class ContentType(helper.Helper):
TEXT = helper.ListItem() # text
AUDIO = helper.ListItem() # audio
DOCUMENT = helper.ListItem() # document
ANIMATION = helper.ListItem() # animation
GAME = helper.ListItem() # game
PHOTO = helper.ListItem() # photo
STICKER = helper.ListItem() # sticker
@ -769,6 +778,7 @@ class ContentType(helper.Helper):
NEW_CHAT_PHOTO = helper.ListItem() # new_chat_photo
DELETE_CHAT_PHOTO = helper.ListItem() # delete_chat_photo
GROUP_CHAT_CREATED = helper.ListItem() # group_chat_created
PASSPORT_DATA = helper.ListItem() # passport_data
UNKNOWN = helper.ListItem() # unknown
ANY = helper.ListItem() # any

View file

@ -83,9 +83,11 @@ class MessageEntityType(helper.Helper):
:key: MENTION
:key: HASHTAG
:key: CASHTAG
:key: BOT_COMMAND
:key: URL
:key: EMAIL
:key: PHONE_NUMBER
:key: BOLD
:key: ITALIC
:key: CODE
@ -97,9 +99,11 @@ class MessageEntityType(helper.Helper):
MENTION = helper.Item() # mention - @username
HASHTAG = helper.Item() # hashtag
CASHTAG = helper.Item() # cashtag
BOT_COMMAND = helper.Item() # bot_command
URL = helper.Item() # url
EMAIL = helper.Item() # email
PHONE_NUMBER = helper.Item() # phone_number
BOLD = helper.Item() # bold - bold text
ITALIC = helper.Item() # italic - italic text
CODE = helper.Item() # code - monowidth string

View file

@ -0,0 +1,16 @@
from . import base
from . import fields
import typing
from .encrypted_passport_element import EncryptedPassportElement
from .encrypted_credentials import EncryptedCredentials
class PassportData(base.TelegramObject):
"""
Contains information about Telegram Passport data shared with the bot by the user.
https://core.telegram.org/bots/api#passportdata
"""
data: typing.List[EncryptedPassportElement] = fields.ListField(base=EncryptedPassportElement)
credentials: EncryptedCredentials = fields.Field(base=EncryptedCredentials)

View file

@ -0,0 +1,110 @@
import typing
from . import base
from . import fields
class PassportElementError(base.TelegramObject):
"""
This object represents an error in the Telegram Passport element which was submitted that
should be resolved by the user.
https://core.telegram.org/bots/api#passportelementerror
"""
source: base.String = fields.Field()
type: base.String = fields.Field()
message: base.String = fields.Field()
class PassportElementErrorDataField(PassportElementError):
"""
Represents an issue in one of the data fields that was provided by the user.
The error is considered resolved when the field's value changes.
https://core.telegram.org/bots/api#passportelementerrordatafield
"""
field_name: base.String = fields.Field()
data_hash: base.String = fields.Field()
def __init__(self, source: base.String, type: base.String, field_name: base.String,
data_hash: base.String, message: base.String):
super(PassportElementErrorDataField, self).__init__(source=source, type=type, field_name=field_name,
data_hash=data_hash, message=message)
class PassportElementErrorFile(PassportElementError):
"""
Represents an issue with a document scan.
The error is considered resolved when the file with the document scan changes.
https://core.telegram.org/bots/api#passportelementerrorfile
"""
file_hash: base.String = fields.Field()
def __init__(self, source: base.String, type: base.String, file_hash: base.String, message: base.String):
super(PassportElementErrorFile, self).__init__(source=source, type=type, file_hash=file_hash,
message=message)
class PassportElementErrorFiles(PassportElementError):
"""
Represents an issue with a list of scans.
The error is considered resolved when the list of files containing the scans changes.
https://core.telegram.org/bots/api#passportelementerrorfiles
"""
file_hashes: typing.List[base.String] = fields.ListField()
def __init__(self, source: base.String, type: base.String, file_hashes: typing.List[base.String],
message: base.String):
super(PassportElementErrorFiles, self).__init__(source=source, type=type, file_hashes=file_hashes,
message=message)
class PassportElementErrorFrontSide(PassportElementError):
"""
Represents an issue with the front side of a document.
The error is considered resolved when the file with the front side of the document changes.
https://core.telegram.org/bots/api#passportelementerrorfrontside
"""
file_hash: base.String = fields.Field()
def __init__(self, source: base.String, type: base.String, file_hash: base.String, message: base.String):
super(PassportElementErrorFrontSide, self).__init__(source=source, type=type, file_hash=file_hash,
message=message)
class PassportElementErrorReverseSide(PassportElementError):
"""
Represents an issue with the reverse side of a document.
The error is considered resolved when the file with reverse side of the document changes.
https://core.telegram.org/bots/api#passportelementerrorreverseside
"""
file_hash: base.String = fields.Field()
def __init__(self, source: base.String, type: base.String, file_hash: base.String, message: base.String):
super(PassportElementErrorReverseSide, self).__init__(source=source, type=type, file_hash=file_hash,
message=message)
class PassportElementErrorSelfie(PassportElementError):
"""
Represents an issue with the selfie with a document.
The error is considered resolved when the file with the selfie changes.
https://core.telegram.org/bots/api#passportelementerrorselfie
"""
file_hash: base.String = fields.Field()
def __init__(self, source: base.String, type: base.String, file_hash: base.String, message: base.String):
super(PassportElementErrorSelfie, self).__init__(source=source, type=type, file_hash=file_hash,
message=message)

View file

@ -0,0 +1,15 @@
from . import base
from . import fields
class PassportFile(base.TelegramObject):
"""
This object represents a file uploaded to Telegram Passport.
Currently all Telegram Passport files are in JPEG format when decrypted and don't exceed 10MB.
https://core.telegram.org/bots/api#passportfile
"""
file_id: base.String = fields.Field()
file_size: base.Integer = fields.Field()
file_date: base.Integer = fields.Field()

View file

@ -13,3 +13,5 @@ class Venue(base.TelegramObject):
title: base.String = fields.Field()
address: base.String = fields.Field()
foursquare_id: base.String = fields.Field()
foursquare_type: base.String = fields.Field()

View file

@ -10,6 +10,7 @@ TelegramAPIError
MessageIdentifierNotSpecified
MessageTextIsEmpty
MessageCantBeEdited
MessageCantBeDeleted
MessageToEditNotFound
ToMuchMessages
ObjectExpectedAsReplyMarkup
@ -171,6 +172,10 @@ class MessageTextIsEmpty(MessageError):
class MessageCantBeEdited(MessageError):
match = 'message can\'t be edited'
class MessageCantBeDeleted(MessageError):
match = 'message can\'t be deleted'
class MessageToEditNotFound(MessageError):

View file

@ -40,7 +40,7 @@ def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=Tr
def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None,
on_startup=None, on_shutdown=None, check_ip=False, **kwargs):
on_startup=None, on_shutdown=None, check_ip=False, retry_after=None, **kwargs):
"""
Start bot in webhook mode
@ -54,7 +54,8 @@ def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None,
:param kwargs:
:return:
"""
executor = Executor(dispatcher, skip_updates=skip_updates, check_ip=check_ip, loop=loop)
executor = Executor(dispatcher, skip_updates=skip_updates, check_ip=check_ip, retry_after=retry_after,
loop=loop)
_setup_callbacks(executor, on_startup, on_shutdown)
executor.start_webhook(webhook_path, **kwargs)
@ -84,12 +85,13 @@ class Executor:
Main executor class
"""
def __init__(self, dispatcher, skip_updates=None, check_ip=False, loop=None):
def __init__(self, dispatcher, skip_updates=None, check_ip=False, retry_after=None, loop=None):
if loop is None:
loop = dispatcher.loop
self.dispatcher = dispatcher
self.skip_updates = skip_updates
self.check_ip = check_ip
self.retry_after = retry_after
self.loop = loop
self._identity = secrets.token_urlsafe(16)
@ -186,6 +188,9 @@ class Executor:
if app is None:
self._web_app = app = web.Application()
if self.retry_after:
app['RETRY_AFTER'] = self.retry_after
if self._identity == app.get(self._identity):
# App is already configured
return

View file

@ -3,11 +3,10 @@ import json
try:
import ujson
_UJSON_IS_AVAILABLE = True
except ImportError:
_UJSON_IS_AVAILABLE = False
ujson = None
_use_ujson = _UJSON_IS_AVAILABLE
_use_ujson = True if ujson else False
def disable_ujson():

View file

@ -10,11 +10,11 @@ BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)