diff --git a/aiogram/api.py b/aiogram/api.py index 1d4a2c14..baf92e47 100644 --- a/aiogram/api.py +++ b/aiogram/api.py @@ -1,3 +1,7 @@ +import os + +import aiohttp + from . import API_URL, log from .exceptions import ValidationError, TelegramAPIError @@ -43,10 +47,39 @@ async def _check_result(method_name, response): return result_json.get('result') -async def request(session, token, method, data=None): +def _guess_filename(obj): + name = getattr(obj, 'name', None) + if name and isinstance(name, str) and name[0] != '<' and name[-1] != '>': + return os.path.basename(name) + + +def _compose_data(params, files=None): + data = aiohttp.formdata.FormData() + + if params: + for key, value in params.items(): + data.add_field(key, str(value)) + + if files: + for key, f in files.items(): + if isinstance(f, tuple): + if len(f) == 2: + filename, fileobj = f + else: + raise ValueError('Tuple must have exactly 2 elements: filename, fileobj') + else: + filename, fileobj = _guess_filename(f) or key, f + + data.add_field(key, fileobj, filename=filename) + + return data + + +async def request(session, token, method, data=None, files=None): log.debug(f"Make request: '{method}' with data: {data or {}}") url = API_URL.format(token=token, method=method) - async with session.post(url, json=data) as response: + data = _compose_data(data, files) + async with session.post(url, data=data) as response: return await _check_result(method, response) diff --git a/aiogram/bot.py b/aiogram/bot.py index 6fa46a62..7d2893bd 100644 --- a/aiogram/bot.py +++ b/aiogram/bot.py @@ -43,8 +43,8 @@ class AIOGramBot: setattr(self, '_me', await self.get_me()) return getattr(self, '_me') - async def request(self, method, data=None): - return await api.request(self.session, self.__token, method, data) + async def request(self, method, data=None, files=None): + return await api.request(self.session, self.__token, method, data, files) async def get_me(self) -> User: raw = await self.request(ApiMethods.GET_ME) @@ -110,3 +110,34 @@ class AIOGramBot: payload = generate_payload(**locals()) message = await self.request(ApiMethods.FORWARD_MESSAGE, payload) return self.prepare_object(Message.de_json(message)) + + async def send_photo(self, chat_id, photo, caption=None, disable_notification=None, reply_to_message_id=None, + reply_markup=None) -> Message: + """ + chat_id Integer or String Yes Unique identifier for the target chat or username of the target channel (in the format @channelusername) + photo InputFile or String Yes Photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a photo from the Internet, or upload a new photo using multipart/form-data. More info on Sending Files ยป + caption String Optional Photo caption (may also be used when resending photos by file_id), 0-200 characters + disable_notification Boolean Optional Sends the message silently. iOS users will not receive a notification, Android users will receive a notification with no sound. + reply_to_message_id Integer Optional If the message is a reply, ID of the original message + reply_markup InlineKeyboardMarkup or ReplyKeyboardMarkup or ReplyKeyboardRemove or ForceReply Optional 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. + + :return: + """ + + if reply_markup and hasattr(reply_markup, 'to_json'): + reply_markup = json.dumps(reply_markup.to_json()) + + if hasattr(reply_to_message_id, 'message_id'): + reply_to_message_id = reply_to_message_id.message_id + + payload = generate_payload(**locals(), exclude=['photo']) + + if isinstance(photo, str): + payload['photo'] = photo + file = None + else: + file = photo + + message = await self.request(ApiMethods.SEND_PHOTO, payload, {'photo': file}) + + return self.prepare_object(Message.de_json(message))