diff --git a/aiogram/bot/base.py b/aiogram/bot/base.py index 0a3f3ca2..25c1d568 100644 --- a/aiogram/bot/base.py +++ b/aiogram/bot/base.py @@ -118,7 +118,7 @@ class BaseBot: url = api.Methods.file_url(token=self.__token, path=file_path) dest = destination if isinstance(destination, io.IOBase) else open(destination, 'wb') - async with self.session.get(url, timeout=timeout, proxy=self.proxy, proxy_auth=self.proxy_auth) as response: + async with self.connector.session.get(url, timeout=timeout) as response: while True: chunk = await response.content.read(chunk_size) if not chunk: diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 1c77e6dd..d15088ad 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -6,7 +6,7 @@ from contextvars import ContextVar from .base import BaseBot, api from .. import types from ..types import base -from ..utils.payload import generate_payload, prepare_arg +from ..utils.payload import generate_payload, prepare_arg, prepare_attachment, prepare_file class Bot(BaseBot): @@ -91,8 +91,8 @@ class Bot(BaseBot): """ allowed_updates = prepare_arg(allowed_updates) payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_UPDATES, payload) + result = await self.request(api.Methods.GET_UPDATES, payload) return [types.Update(**update) for update in result] async def set_webhook(self, url: base.String, @@ -121,8 +121,11 @@ class Bot(BaseBot): """ allowed_updates = prepare_arg(allowed_updates) payload = generate_payload(**locals(), exclude=['certificate']) - result = await self.send_file('certificate', api.Methods.SET_WEBHOOK, certificate, payload) + files = {} + prepare_file(payload, files, 'certificate', certificate) + + result = await self.request(api.Methods.SET_WEBHOOK, payload, files) return result async def delete_webhook(self) -> base.Boolean: @@ -136,8 +139,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_WEBHOOK, payload) + result = await self.request(api.Methods.DELETE_WEBHOOK, payload) return result async def get_webhook_info(self) -> types.WebhookInfo: @@ -152,8 +155,8 @@ class Bot(BaseBot): :rtype: :obj:`types.WebhookInfo` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_WEBHOOK_INFO, payload) + result = await self.request(api.Methods.GET_WEBHOOK_INFO, payload) return types.WebhookInfo(**result) # === Base methods === @@ -169,8 +172,8 @@ class Bot(BaseBot): :rtype: :obj:`types.User` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_ME, payload) + result = await self.request(api.Methods.GET_ME, payload) return types.User(**result) async def send_message(self, chat_id: typing.Union[base.Integer, base.String], text: base.String, @@ -212,7 +215,6 @@ class Bot(BaseBot): payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.SEND_MESSAGE, payload) - return types.Message(**result) async def forward_message(self, chat_id: typing.Union[base.Integer, base.String], @@ -235,8 +237,8 @@ class Bot(BaseBot): :rtype: :obj:`types.Message` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.FORWARD_MESSAGE, payload) + result = await self.request(api.Methods.FORWARD_MESSAGE, payload) return types.Message(**result) async def send_photo(self, chat_id: typing.Union[base.Integer, base.String], @@ -278,8 +280,10 @@ class Bot(BaseBot): if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) - result = await self.send_file('photo', api.Methods.SEND_PHOTO, photo, payload) + files = {} + prepare_file(payload, files, 'photo', photo) + result = await self.request(api.Methods.SEND_PHOTO, payload, files) return types.Message(**result) async def send_audio(self, chat_id: typing.Union[base.Integer, base.String], @@ -336,8 +340,10 @@ class Bot(BaseBot): if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) - result = await self.send_file('audio', api.Methods.SEND_AUDIO, audio, payload) + files = {} + prepare_file(payload, files, 'audio', audio) + result = await self.request(api.Methods.SEND_AUDIO, payload, files) return types.Message(**result) async def send_document(self, chat_id: typing.Union[base.Integer, base.String], @@ -384,8 +390,10 @@ class Bot(BaseBot): if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) - result = await self.send_file('document', api.Methods.SEND_DOCUMENT, document, payload) + files = {} + prepare_file(payload, files, 'document', document) + result = await self.request(api.Methods.SEND_DOCUMENT, payload, document) return types.Message(**result) async def send_video(self, chat_id: typing.Union[base.Integer, base.String], @@ -439,29 +447,33 @@ class Bot(BaseBot): :rtype: :obj:`types.Message` """ reply_markup = prepare_arg(reply_markup) - payload = generate_payload(**locals(), exclude=['video']) + payload = generate_payload(**locals(), exclude=['video', 'thumb']) if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) - result = await self.send_file('video', api.Methods.SEND_VIDEO, video, payload) + files = {} + prepare_file(payload, files, 'video', video) + prepare_attachment(payload, files, 'thumb', thumb) + result = await self.request(api.Methods.SEND_VIDEO, payload, files) 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: + 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). @@ -503,9 +515,13 @@ class Bot(BaseBot): :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) + payload = generate_payload(**locals(), exclude=["animation", "thumb"]) + + files = {} + prepare_file(payload, files, 'animation', animation) + prepare_attachment(payload, files, 'thumb', thumb) + result = await self.request(api.Methods.SEND_ANIMATION, payload, files) return types.Message(**result) async def send_voice(self, chat_id: typing.Union[base.Integer, base.String], @@ -554,8 +570,10 @@ class Bot(BaseBot): if self.parse_mode: payload.setdefault('parse_mode', self.parse_mode) - result = await self.send_file('voice', api.Methods.SEND_VOICE, voice, payload) + files = {} + prepare_file(payload, files, 'voice', voice) + result = await self.request(api.Methods.SEND_VOICE, payload, files) return types.Message(**result) async def send_video_note(self, chat_id: typing.Union[base.Integer, base.String], @@ -597,8 +615,11 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals(), exclude=['video_note']) - result = await self.send_file('video_note', api.Methods.SEND_VIDEO_NOTE, video_note, payload) + files = {} + prepare_file(payload, files, 'video_note', video_note) + + result = await self.request(api.Methods.SEND_VIDEO_NOTE, payload, files) return types.Message(**result) async def send_media_group(self, chat_id: typing.Union[base.Integer, base.String], @@ -626,13 +647,12 @@ class Bot(BaseBot): if isinstance(media, list): media = types.MediaGroup(media) - # Extract files - files = media.get_files() + files = dict(media.get_files()) media = prepare_arg(media) payload = generate_payload(**locals(), exclude=['files']) - result = await self.request(api.Methods.SEND_MEDIA_GROUP, payload, files) + result = await self.request(api.Methods.SEND_MEDIA_GROUP, payload, files) return [types.Message(**message) for message in result] async def send_location(self, chat_id: typing.Union[base.Integer, base.String], @@ -669,8 +689,8 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SEND_LOCATION, payload) + result = await self.request(api.Methods.SEND_LOCATION, payload) return types.Message(**result) async def edit_message_live_location(self, latitude: base.Float, longitude: base.Float, @@ -704,11 +724,10 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) - result = await self.request(api.Methods.EDIT_MESSAGE_LIVE_LOCATION, payload) + result = await self.request(api.Methods.EDIT_MESSAGE_LIVE_LOCATION, payload) if isinstance(result, bool): return result - return types.Message(**result) async def stop_message_live_location(self, @@ -737,11 +756,10 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) - result = await self.request(api.Methods.STOP_MESSAGE_LIVE_LOCATION, payload) + result = await self.request(api.Methods.STOP_MESSAGE_LIVE_LOCATION, payload) if isinstance(result, bool): return result - return types.Message(**result) async def send_venue(self, chat_id: typing.Union[base.Integer, base.String], @@ -786,8 +804,8 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SEND_VENUE, payload) + result = await self.request(api.Methods.SEND_VENUE, payload) return types.Message(**result) async def send_contact(self, chat_id: typing.Union[base.Integer, base.String], @@ -827,8 +845,8 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SEND_CONTACT, payload) + result = await self.request(api.Methods.SEND_CONTACT, payload) return types.Message(**result) async def send_chat_action(self, chat_id: typing.Union[base.Integer, base.String], @@ -851,8 +869,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SEND_CHAT_ACTION, payload) + result = await self.request(api.Methods.SEND_CHAT_ACTION, payload) return result async def get_user_profile_photos(self, user_id: base.Integer, offset: typing.Union[base.Integer, None] = None, @@ -872,8 +890,8 @@ class Bot(BaseBot): :rtype: :obj:`types.UserProfilePhotos` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_USER_PROFILE_PHOTOS, payload) + result = await self.request(api.Methods.GET_USER_PROFILE_PHOTOS, payload) return types.UserProfilePhotos(**result) async def get_file(self, file_id: base.String) -> types.File: @@ -892,8 +910,8 @@ class Bot(BaseBot): :rtype: :obj:`types.File` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_FILE, payload) + result = await self.request(api.Methods.GET_FILE, payload) return types.File(**result) async def kick_chat_member(self, chat_id: typing.Union[base.Integer, base.String], user_id: base.Integer, @@ -922,8 +940,8 @@ class Bot(BaseBot): """ until_date = prepare_arg(until_date) payload = generate_payload(**locals()) - result = await self.request(api.Methods.KICK_CHAT_MEMBER, payload) + result = await self.request(api.Methods.KICK_CHAT_MEMBER, payload) return result async def unban_chat_member(self, chat_id: typing.Union[base.Integer, base.String], @@ -944,8 +962,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.UNBAN_CHAT_MEMBER, payload) + result = await self.request(api.Methods.UNBAN_CHAT_MEMBER, payload) return result async def restrict_chat_member(self, chat_id: typing.Union[base.Integer, base.String], @@ -984,8 +1002,8 @@ class Bot(BaseBot): """ until_date = prepare_arg(until_date) payload = generate_payload(**locals()) - result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload) + result = await self.request(api.Methods.RESTRICT_CHAT_MEMBER, payload) return result async def promote_chat_member(self, chat_id: typing.Union[base.Integer, base.String], @@ -1031,8 +1049,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload) + result = await self.request(api.Methods.PROMOTE_CHAT_MEMBER, payload) return result async def export_chat_invite_link(self, chat_id: typing.Union[base.Integer, base.String]) -> base.String: @@ -1048,8 +1066,8 @@ class Bot(BaseBot): :rtype: :obj:`base.String` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.EXPORT_CHAT_INVITE_LINK, payload) + result = await self.request(api.Methods.EXPORT_CHAT_INVITE_LINK, payload) return result async def set_chat_photo(self, chat_id: typing.Union[base.Integer, base.String], @@ -1071,8 +1089,11 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals(), exclude=['photo']) - result = await self.send_file('photo', api.Methods.SET_CHAT_PHOTO, photo, payload) + files = {} + prepare_file(payload, files, 'photo', photo) + + result = await self.request(api.Methods.SET_CHAT_PHOTO, payload, files) return result async def delete_chat_photo(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: @@ -1091,8 +1112,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_CHAT_PHOTO, payload) + result = await self.request(api.Methods.DELETE_CHAT_PHOTO, payload) return result async def set_chat_title(self, chat_id: typing.Union[base.Integer, base.String], @@ -1114,8 +1135,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_TITLE, payload) + result = await self.request(api.Methods.SET_CHAT_TITLE, payload) return result async def set_chat_description(self, chat_id: typing.Union[base.Integer, base.String], @@ -1134,8 +1155,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_DESCRIPTION, payload) + result = await self.request(api.Methods.SET_CHAT_DESCRIPTION, payload) return result async def pin_chat_message(self, chat_id: typing.Union[base.Integer, base.String], message_id: base.Integer, @@ -1157,8 +1178,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.PIN_CHAT_MESSAGE, payload) + result = await self.request(api.Methods.PIN_CHAT_MESSAGE, payload) return result async def unpin_chat_message(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: @@ -1174,8 +1195,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.UNPIN_CHAT_MESSAGE, payload) + result = await self.request(api.Methods.UNPIN_CHAT_MESSAGE, payload) return result async def leave_chat(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: @@ -1190,8 +1211,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.LEAVE_CHAT, payload) + result = await self.request(api.Methods.LEAVE_CHAT, payload) return result async def get_chat(self, chat_id: typing.Union[base.Integer, base.String]) -> types.Chat: @@ -1207,8 +1228,8 @@ class Bot(BaseBot): :rtype: :obj:`types.Chat` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_CHAT, payload) + result = await self.request(api.Methods.GET_CHAT, payload) return types.Chat(**result) async def get_chat_administrators(self, chat_id: typing.Union[base.Integer, base.String] @@ -1227,8 +1248,8 @@ class Bot(BaseBot): :rtype: :obj:`typing.List[types.ChatMember]` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_CHAT_ADMINISTRATORS, payload) + result = await self.request(api.Methods.GET_CHAT_ADMINISTRATORS, payload) return [types.ChatMember(**chatmember) for chatmember in result] async def get_chat_members_count(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Integer: @@ -1243,8 +1264,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Integer` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_CHAT_MEMBERS_COUNT, payload) + result = await self.request(api.Methods.GET_CHAT_MEMBERS_COUNT, payload) return result async def get_chat_member(self, chat_id: typing.Union[base.Integer, base.String], @@ -1262,8 +1283,8 @@ class Bot(BaseBot): :rtype: :obj:`types.ChatMember` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_CHAT_MEMBER, payload) + result = await self.request(api.Methods.GET_CHAT_MEMBER, payload) return types.ChatMember(**result) async def set_chat_sticker_set(self, chat_id: typing.Union[base.Integer, base.String], @@ -1285,8 +1306,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_CHAT_STICKER_SET, payload) + result = await self.request(api.Methods.SET_CHAT_STICKER_SET, payload) return result async def delete_chat_sticker_set(self, chat_id: typing.Union[base.Integer, base.String]) -> base.Boolean: @@ -1305,8 +1326,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_CHAT_STICKER_SET, payload) + result = await self.request(api.Methods.DELETE_CHAT_STICKER_SET, payload) return result async def answer_callback_query(self, callback_query_id: base.String, @@ -1340,8 +1361,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload) + result = await self.request(api.Methods.ANSWER_CALLBACK_QUERY, payload) return result async def edit_message_text(self, text: base.String, @@ -1383,10 +1404,8 @@ class Bot(BaseBot): payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.EDIT_MESSAGE_TEXT, payload) - if isinstance(result, bool): return result - return types.Message(**result) async def edit_message_caption(self, chat_id: typing.Union[base.Integer, base.String, None] = None, @@ -1425,19 +1444,17 @@ class Bot(BaseBot): payload.setdefault('parse_mode', self.parse_mode) result = await self.request(api.Methods.EDIT_MESSAGE_CAPTION, payload) - if isinstance(result, bool): return result - 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]: + 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. @@ -1464,20 +1481,17 @@ class Bot(BaseBot): 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(media, types.InputMedia): + files = dict(media.get_files()) + else: + files = None + 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, @@ -1506,11 +1520,10 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) - result = await self.request(api.Methods.EDIT_MESSAGE_REPLY_MARKUP, payload) + result = await self.request(api.Methods.EDIT_MESSAGE_REPLY_MARKUP, payload) if isinstance(result, bool): return result - return types.Message(**result) async def delete_message(self, chat_id: typing.Union[base.Integer, base.String], @@ -1535,8 +1548,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_MESSAGE, payload) + result = await self.request(api.Methods.DELETE_MESSAGE, payload) return result # === Stickers === @@ -1571,8 +1584,11 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals(), exclude=['sticker']) - result = await self.send_file('sticker', api.Methods.SEND_STICKER, sticker, payload) + files = {} + prepare_file(payload, files, 'sticker', sticker) + + result = await self.request(api.Methods.SEND_STICKER, payload, files) return types.Message(**result) async def get_sticker_set(self, name: base.String) -> types.StickerSet: @@ -1587,8 +1603,8 @@ class Bot(BaseBot): :rtype: :obj:`types.StickerSet` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.GET_STICKER_SET, payload) + result = await self.request(api.Methods.GET_STICKER_SET, payload) return types.StickerSet(**result) async def upload_sticker_file(self, user_id: base.Integer, png_sticker: base.InputFile) -> types.File: @@ -1607,8 +1623,11 @@ class Bot(BaseBot): :rtype: :obj:`types.File` """ payload = generate_payload(**locals(), exclude=['png_sticker']) - result = await self.send_file('png_sticker', api.Methods.UPLOAD_STICKER_FILE, png_sticker, payload) + files = {} + prepare_file(payload, files, 'png_sticker', png_sticker) + + result = await self.request(api.Methods.UPLOAD_STICKER_FILE, payload, files) return types.File(**result) async def create_new_sticker_set(self, user_id: base.Integer, name: base.String, title: base.String, @@ -1640,8 +1659,11 @@ class Bot(BaseBot): """ mask_position = prepare_arg(mask_position) payload = generate_payload(**locals(), exclude=['png_sticker']) - result = await self.send_file('png_sticker', api.Methods.CREATE_NEW_STICKER_SET, png_sticker, payload) + files = {} + prepare_file(payload, files, 'png_sticker', png_sticker) + + result = await self.request(api.Methods.CREATE_NEW_STICKER_SET, payload, files) return result async def add_sticker_to_set(self, user_id: base.Integer, name: base.String, @@ -1668,8 +1690,11 @@ class Bot(BaseBot): """ mask_position = prepare_arg(mask_position) payload = generate_payload(**locals(), exclude=['png_sticker']) - result = await self.send_file('png_sticker', api.Methods.ADD_STICKER_TO_SET, png_sticker, payload) + files = {} + prepare_file(payload, files, 'png_sticker', png_sticker) + + result = await self.request(api.Methods.ADD_STICKER_TO_SET, payload, files) return result async def set_sticker_position_in_set(self, sticker: base.String, position: base.Integer) -> base.Boolean: @@ -1704,8 +1729,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.DELETE_STICKER_FROM_SET, payload) + result = await self.request(api.Methods.DELETE_STICKER_FROM_SET, payload) return result async def answer_inline_query(self, inline_query_id: base.String, @@ -1748,8 +1773,8 @@ class Bot(BaseBot): """ results = prepare_arg(results) payload = generate_payload(**locals()) - result = await self.request(api.Methods.ANSWER_INLINE_QUERY, payload) + result = await self.request(api.Methods.ANSWER_INLINE_QUERY, payload) return result # === Payments === @@ -1829,8 +1854,8 @@ class Bot(BaseBot): prices = prepare_arg([price.to_python() if hasattr(price, 'to_python') else price for price in prices]) reply_markup = prepare_arg(reply_markup) payload_ = generate_payload(**locals()) - result = await self.request(api.Methods.SEND_INVOICE, payload_) + result = await self.request(api.Methods.SEND_INVOICE, payload_) return types.Message(**result) async def answer_shipping_query(self, shipping_query_id: base.String, ok: base.Boolean, @@ -1863,8 +1888,8 @@ class Bot(BaseBot): else shipping_option for shipping_option in shipping_options]) payload = generate_payload(**locals()) - result = await self.request(api.Methods.ANSWER_SHIPPING_QUERY, payload) + result = await self.request(api.Methods.ANSWER_SHIPPING_QUERY, payload) return result async def answer_pre_checkout_query(self, pre_checkout_query_id: base.String, ok: base.Boolean, @@ -1891,8 +1916,8 @@ class Bot(BaseBot): :rtype: :obj:`base.Boolean` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.ANSWER_PRE_CHECKOUT_QUERY, payload) + result = await self.request(api.Methods.ANSWER_PRE_CHECKOUT_QUERY, payload) return result # === Games === @@ -1923,8 +1948,8 @@ class Bot(BaseBot): """ errors = prepare_arg(errors) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_PASSPORT_DATA_ERRORS, payload) + result = await self.request(api.Methods.SET_PASSPORT_DATA_ERRORS, payload) return result # === Games === @@ -1956,8 +1981,8 @@ class Bot(BaseBot): """ reply_markup = prepare_arg(reply_markup) payload = generate_payload(**locals()) - result = await self.request(api.Methods.SEND_GAME, payload) + result = await self.request(api.Methods.SEND_GAME, payload) return types.Message(**result) async def set_game_score(self, user_id: base.Integer, score: base.Integer, @@ -1994,11 +2019,10 @@ class Bot(BaseBot): :rtype: :obj:`typing.Union[types.Message, base.Boolean]` """ payload = generate_payload(**locals()) - result = await self.request(api.Methods.SET_GAME_SCORE, payload) + result = await self.request(api.Methods.SET_GAME_SCORE, payload) if isinstance(result, bool): return result - return types.Message(**result) async def get_game_high_scores(self, user_id: base.Integer, diff --git a/aiogram/dispatcher/webhook.py b/aiogram/dispatcher/webhook.py index c350206e..bc2a0e60 100644 --- a/aiogram/dispatcher/webhook.py +++ b/aiogram/dispatcher/webhook.py @@ -955,7 +955,7 @@ class SendMediaGroup(BaseResponse, ReplyToMixin, DisableNotificationMixin): self.reply_to_message_id = reply_to_message_id def prepare(self): - files = self.media.get_files() + files = dict(self.media.get_files()) if files: raise TypeError('Allowed only file ID or URL\'s') diff --git a/aiogram/types/fields.py b/aiogram/types/fields.py index fc12dd2e..81156d5d 100644 --- a/aiogram/types/fields.py +++ b/aiogram/types/fields.py @@ -9,7 +9,7 @@ class BaseField(metaclass=abc.ABCMeta): Base field (prop) """ - def __init__(self, *, base=None, default=None, alias=None): + def __init__(self, *, base=None, default=None, alias=None, on_change=None): """ Init prop @@ -17,10 +17,12 @@ class BaseField(metaclass=abc.ABCMeta): :param default: default value :param alias: alias name (for e.g. field 'from' has to be named 'from_user' as 'from' is a builtin Python keyword + :param on_change: callback will be called when value is changed """ self.base_object = base self.default = default self.alias = alias + self.on_change = on_change def __set_name__(self, owner, name): if self.alias is None: @@ -53,6 +55,13 @@ class BaseField(metaclass=abc.ABCMeta): self.resolve_base(instance) value = self.deserialize(value, parent) instance.values[self.alias] = value + self._trigger_changed(instance, value) + + def _trigger_changed(self, instance, value): + if not self.on_change and instance is not None: + return + callback = getattr(instance, self.on_change) + callback(value) def __get__(self, instance, owner): return self.get_value(instance) @@ -154,7 +163,7 @@ class ListOfLists(Field): return result -class DateTimeField(BaseField): +class DateTimeField(Field): """ In this field st_ored datetime @@ -167,3 +176,24 @@ class DateTimeField(BaseField): def deserialize(self, value, parent=None): return datetime.datetime.fromtimestamp(value) + + +class TextField(Field): + def __init__(self, *, prefix=None, suffix=None, default=None, alias=None): + super(TextField, self).__init__(default=default, alias=alias) + self.prefix = prefix + self.suffix = suffix + + def serialize(self, value): + if value is None: + return value + if self.prefix: + value = self.prefix + value + if self.suffix: + value += self.suffix + return value + + def deserialize(self, value, parent=None): + if value is not None and not isinstance(value, str): + raise TypeError(f"Field '{self.alias}' should be str not {type(value).__name__}") + return value diff --git a/aiogram/types/input_file.py b/aiogram/types/input_file.py index 59c30c63..5c271701 100644 --- a/aiogram/types/input_file.py +++ b/aiogram/types/input_file.py @@ -1,6 +1,7 @@ import io import logging import os +import secrets import time import aiohttp @@ -45,6 +46,8 @@ class InputFile(base.TelegramObject): self._filename = filename + self.attachment_key = secrets.token_urlsafe(16) + def __del__(self): """ Close file descriptor @@ -61,6 +64,10 @@ class InputFile(base.TelegramObject): def filename(self, value): self._filename = value + @property + def attach(self): + return f"attach://{self.attachment_key}" + def get_filename(self) -> str: """ Get file name @@ -159,6 +166,9 @@ class InputFile(base.TelegramObject): return writer + def __str__(self): + return f"" + def to_python(self): raise TypeError('Object of this type is not exportable!') diff --git a/aiogram/types/input_media.py b/aiogram/types/input_media.py index 1f68e632..7bb58a7a 100644 --- a/aiogram/types/input_media.py +++ b/aiogram/types/input_media.py @@ -12,6 +12,9 @@ ATTACHMENT_PREFIX = 'attach://' class InputMedia(base.TelegramObject): """ This object represents the content of a media message to be sent. It should be one of + - InputMediaAnimation + - InputMediaDocument + - InputMediaAudio - InputMediaPhoto - InputMediaVideo @@ -20,36 +23,76 @@ class InputMedia(base.TelegramObject): https://core.telegram.org/bots/api#inputmedia """ type: base.String = fields.Field(default='photo') - media: base.String = fields.Field() - thumb: typing.Union[base.InputFile, base.String] = fields.Field() + media: base.String = fields.Field(alias='media', on_change='_media_changed') + thumb: typing.Union[base.InputFile, base.String] = fields.Field(alias='thumb', on_change='_thumb_changed') caption: base.String = fields.Field() parse_mode: base.Boolean = fields.Field() def __init__(self, *args, **kwargs): + self._thumb_file = None + self._media_file = None + + media = kwargs.pop('media', None) + if isinstance(media, (io.IOBase, InputFile)): + self.file = media + elif media is not None: + self.media = media + + thumb = kwargs.pop('thumb', None) + if isinstance(thumb, (io.IOBase, InputFile)): + self.thumb_file = thumb + elif thumb is not None: + self.thumb = thumb + super(InputMedia, self).__init__(*args, **kwargs) + try: - if self.parse_mode is None and self.bot.parse_mode: + if self.parse_mode is None and self.bot and self.bot.parse_mode: self.parse_mode = self.bot.parse_mode except RuntimeError: pass @property def file(self): - return getattr(self, '_file', None) + return self._media_file @file.setter def file(self, file: io.IOBase): - setattr(self, '_file', file) - attachment_key = self.attachment_key = secrets.token_urlsafe(16) - self.media = ATTACHMENT_PREFIX + attachment_key + self.media = 'attach://' + secrets.token_urlsafe(16) + self._media_file = file + + @file.deleter + def file(self): + self.media = None + self._media_file = None + + def _media_changed(self, value): + if value is None or isinstance(value, str) and not value.startswith('attach://'): + self._media_file = None @property - def attachment_key(self): - return self.conf.get('attachment_key', None) + def thumb_file(self): + return self._thumb_file - @attachment_key.setter - def attachment_key(self, value): - self.conf['attachment_key'] = value + @thumb_file.setter + def thumb_file(self, file: io.IOBase): + self.thumb = 'attach://' + secrets.token_urlsafe(16) + self._thumb_file = file + + @thumb_file.deleter + def thumb_file(self): + self.thumb = None + self._thumb_file = None + + def _thumb_changed(self, value): + if value is None or isinstance(value, str) and not value.startswith('attach://'): + self._thumb_file = None + + def get_files(self): + if self._media_file: + yield self.media[9:], self._media_file + if self._thumb_file: + yield self.thumb[9:], self._thumb_file class InputMediaAnimation(InputMedia): @@ -72,9 +115,6 @@ class InputMediaAnimation(InputMedia): width=width, height=height, duration=duration, parse_mode=parse_mode, conf=kwargs) - if isinstance(media, (io.IOBase, InputFile)): - self.file = media - class InputMediaDocument(InputMedia): """ @@ -89,9 +129,6 @@ class InputMediaDocument(InputMedia): caption=caption, parse_mode=parse_mode, conf=kwargs) - if isinstance(media, (io.IOBase, InputFile)): - self.file = media - class InputMediaAudio(InputMedia): """ @@ -119,9 +156,6 @@ class InputMediaAudio(InputMedia): performer=performer, title=title, parse_mode=parse_mode, conf=kwargs) - if isinstance(media, (io.IOBase, InputFile)): - self.file = media - class InputMediaPhoto(InputMedia): """ @@ -136,9 +170,6 @@ class InputMediaPhoto(InputMedia): caption=caption, parse_mode=parse_mode, conf=kwargs) - if isinstance(media, (io.IOBase, InputFile)): - self.file = media - class InputMediaVideo(InputMedia): """ @@ -151,18 +182,17 @@ class InputMediaVideo(InputMedia): duration: base.Integer = fields.Field() supports_streaming: base.Boolean = fields.Field() - def __init__(self, media: base.InputFile, caption: base.String = None, + 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, supports_streaming: base.Boolean = None, **kwargs): - super(InputMediaVideo, self).__init__(type='video', media=media, caption=caption, + super(InputMediaVideo, self).__init__(type='video', media=media, thumb=thumb, caption=caption, width=width, height=height, duration=duration, parse_mode=parse_mode, supports_streaming=supports_streaming, conf=kwargs) - if isinstance(media, (io.IOBase, InputFile)): - self.file = media - class MediaGroup(base.TelegramObject): """ @@ -296,6 +326,7 @@ class MediaGroup(base.TelegramObject): self.attach(photo) def attach_video(self, video: typing.Union[InputMediaVideo, 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): """ @@ -308,7 +339,7 @@ class MediaGroup(base.TelegramObject): :param duration: """ if not isinstance(video, InputMedia): - video = InputMediaVideo(media=video, caption=caption, + video = InputMediaVideo(media=video, thumb=thumb, caption=caption, width=width, height=height, duration=duration) self.attach(video) @@ -327,6 +358,7 @@ class MediaGroup(base.TelegramObject): return result def get_files(self): - return {inputmedia.attachment_key: inputmedia.file - for inputmedia in self.media - if isinstance(inputmedia, InputMedia) and inputmedia.file} + for inputmedia in self.media: + if not isinstance(inputmedia, InputMedia) or not inputmedia.file: + continue + yield from inputmedia.get_files() diff --git a/aiogram/utils/payload.py b/aiogram/utils/payload.py index dac43492..bbed1967 100644 --- a/aiogram/utils/payload.py +++ b/aiogram/utils/payload.py @@ -1,5 +1,7 @@ import datetime +import secrets +from aiogram import types from . import json DEFAULT_FILTER = ['self', 'cls'] @@ -56,3 +58,22 @@ def prepare_arg(value): elif isinstance(value, datetime.datetime): return round(value.timestamp()) return value + + +def prepare_file(payload, files, key, file): + if isinstance(file, str): + payload[key] = file + elif file is not None: + files[key] = file + + +def prepare_attachment(payload, files, key, file): + if isinstance(file, str): + payload[key] = file + elif isinstance(file, types.InputFile): + payload[key] = file.attach + files[file.attachment_key] = file.file + elif file is not None: + file_attach_name = secrets.token_urlsafe(16) + payload[key] = "attach://" + file_attach_name + files[file_attach_name] = file