diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index 903616b7..76f322f8 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -236,6 +236,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 @@ -270,6 +271,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 @@ -290,6 +292,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 diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index f58e5842..f191b1ec 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -289,6 +289,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, @@ -317,6 +318,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]` @@ -339,6 +342,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, @@ -358,6 +362,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, @@ -387,6 +393,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, @@ -412,6 +419,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, @@ -438,6 +447,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 thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + :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, @@ -492,6 +555,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, @@ -512,6 +576,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 @@ -675,6 +741,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, @@ -698,6 +765,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 @@ -717,6 +786,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, @@ -736,6 +806,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 @@ -1352,6 +1424,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, @@ -1770,6 +1890,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, diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index 73af7f8f..c350206e 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -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 @@ -129,8 +130,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() @@ -247,6 +254,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. diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index cc7abb11..943efb4b 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -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', diff --git a/aiogram/types/audio.py b/aiogram/types/audio.py index ed323f81..9423d02c 100644 --- a/aiogram/types/audio.py +++ b/aiogram/types/audio.py @@ -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) diff --git a/aiogram/types/contact.py b/aiogram/types/contact.py index 842d6044..b70045b9 100644 --- a/aiogram/types/contact.py +++ b/aiogram/types/contact.py @@ -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): diff --git a/aiogram/types/encrypted_credentials.py b/aiogram/types/encrypted_credentials.py new file mode 100644 index 00000000..d649c8d9 --- /dev/null +++ b/aiogram/types/encrypted_credentials.py @@ -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() diff --git a/aiogram/types/encrypted_passport_element.py b/aiogram/types/encrypted_passport_element.py new file mode 100644 index 00000000..bc7b212b --- /dev/null +++ b/aiogram/types/encrypted_passport_element.py @@ -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) diff --git a/aiogram/types/inline_query_result.py b/aiogram/types/inline_query_result.py index 62936961..a80352d7 100644 --- a/aiogram/types/inline_query_result.py +++ b/aiogram/types/inline_query_result.py @@ -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): diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 2ae4da4f..1f68e632 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -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): """ diff --git a/aiogram/types/input_message_content.py b/aiogram/types/input_message_content.py index 88f8a74f..736a4454 100644 --- a/aiogram/types/input_message_content.py +++ b/aiogram/types/input_message_content.py @@ -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, diff --git a/aiogram/types/message.py b/aiogram/types/message.py index a282a180..b593fdb0 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -7,6 +7,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 @@ -15,6 +16,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 @@ -51,6 +53,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) @@ -75,6 +78,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() @@ -83,6 +87,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: @@ -127,6 +133,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] @@ -750,6 +758,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 @@ -771,6 +780,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 diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index 6ca81518..abb4f060 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -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 diff --git a/aiogram/types/passport_data.py b/aiogram/types/passport_data.py new file mode 100644 index 00000000..06cbad1c --- /dev/null +++ b/aiogram/types/passport_data.py @@ -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) diff --git a/aiogram/types/passport_element_error.py b/aiogram/types/passport_element_error.py new file mode 100644 index 00000000..f673ba16 --- /dev/null +++ b/aiogram/types/passport_element_error.py @@ -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) diff --git a/aiogram/types/passport_file.py b/aiogram/types/passport_file.py new file mode 100644 index 00000000..f00e80c7 --- /dev/null +++ b/aiogram/types/passport_file.py @@ -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() diff --git a/aiogram/types/venue.py b/aiogram/types/venue.py index 38f28cf9..1b420d57 100644 --- a/aiogram/types/venue.py +++ b/aiogram/types/venue.py @@ -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() + diff --git a/aiogram/utils/exceptions.py b/aiogram/utils/exceptions.py index 880b35ab..cddd0c74 100644 --- a/aiogram/utils/exceptions.py +++ b/aiogram/utils/exceptions.py @@ -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): diff --git a/aiogram/utils/executor.py b/aiogram/utils/executor.py index ee231f95..ac1a9657 100644 --- a/aiogram/utils/executor.py +++ b/aiogram/utils/executor.py @@ -39,7 +39,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 @@ -53,7 +53,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) @@ -83,12 +84,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) @@ -190,6 +192,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 diff --git a/docs/Makefile b/docs/Makefile index d4bd90d4..4e50ed99 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -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) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/examples/webhook_example_2.py b/examples/webhook_example_2.py new file mode 100644 index 00000000..1a6fc2a1 --- /dev/null +++ b/examples/webhook_example_2.py @@ -0,0 +1,45 @@ +import asyncio +import logging + +from aiogram import Bot, types +from aiogram.dispatcher import Dispatcher +from aiogram.utils.executor import start_webhook + +API_TOKEN = 'BOT TOKEN HERE' + +# webhook settings +WEBHOOK_HOST = 'https://your.domain' +WEBHOOK_PATH = '/path/to/api' +WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}" + +# webserver settings +WEBAPP_HOST = 'localhost' # or ip +WEBAPP_PORT = 3001 + +logging.basicConfig(level=logging.INFO) + +loop = asyncio.get_event_loop() +bot = Bot(token=API_TOKEN, loop=loop) +dp = Dispatcher(bot) + + +@dp.message_handler() +async def echo(message: types.Message): + await bot.send_message(message.chat.id, message.text) + + +async def on_startup(dp): + await bot.set_webhook(WEBHOOK_URL) + # insert code here to run it after start + # + + +async def on_shutdown(dp): + # insert code here to run it before shutdown + # + await bot.close() + + +if __name__ == '__main__': + start_webhook(dispatcher=dp, webhook_path=WEBHOOK_PATH, on_startup=on_startup, on_shutdown=on_shutdown, + skip_updates=True, host=WEBAPP_HOST, port=WEBAPP_PORT)