Прямая ссылка на фото с telegraph
ForzendВ этой статье мы с вами напишем бота, который будет загружать картинку пользователя на telegra.ph (на котором вы, собственно, сейчас и читаете эту статью) и возвращать прямую ссылку на фото. Это может быть полезно, например, для создания скрытых c preview, ссылок на фото или добавления thumb_url к InlineQueryResultArticle.
Результат работы
Как мы видим, после того, как мы отправляем боту картинку, бот генерирует и возвращает нам ссылку на эту самую картинку на telegra.ph, которая имеет удобный предпросмотр.
В обработчике у нас будет всего пара строк.
Вся логика будет умещена в две функции (какую из двух использовать в своих "боевых" проектах вы решите сами), которые мы поместим в пакет app.utils
.
В итоге мы реализуем две утилиты: написанную лично, при помощи обычных post запросов aiohttp, и с использованием готового решения, представленного разработчиками aiogram, — aiograph. Обе функции будут возвращать нам строку — ссылку на уже загруженное изображение.
В итоге модуль будет выглядеть следующим образом:
Содержание статьи
┌── Результат работы ├─> Содержание статьи ├── Написание своего собственного модуля | └── Логика ├── Использование готового решания | └── Логика ├── Немножко красоты └── Заключение
В этой статье будет использоваться структура, описанная в одной из предыдущих статей (вы также можете найти её на github).
Написание собственного модуля
В первую очередь мы разберём собственную реализацию нужного нам функционала. В aiograph "под капотом" происходит практически то же, поэтому это позволит нам понимать, что же происходит за красивым названием функции и, при желании, полностью разобраться в устройстве библиотеки.
Я предлагаю расположить наш модуль в директории app/utils
, поэтому перейдём в него и создадим файл app/utils/photo_link.py
. Создадим в нём асинхронную функцию photo_link
которая будет принимать аргумент аиограмовского типа PhotoSize (именно список объектов данного типа лежит в message.photo) и возвращать саму ссылку, в виде строки, пока вместо ссылки пусть будет просто строка 'link'
.
app/utils/
photo_link.py
:
from aiogram import types async def photo_link(photo: types.photo_size.PhotoSize) -> str: return 'link'
И добавим эту функцию в наш app/utils/__init__.py
чтобы её было удобно импортировать (на месте многоточия, как вы понимаете, находится подключение других утилит).
app/utils/
__init__.py
:
. . . from .photo_link import photo_link __all__ = [ . . . "photo_link", ]
В функции photo_link
у нас и будет находиться основная логика, мы к ней вернёмся чуть позже, а сейчас давайте опишем небольшой обработчик, который будет в ответ на фото будет возвращать нам то, что вернёт функция, а именно ссылку на изображение.
Для этого создадим, в app/handlers/private
, допустим, модуль telegraph.py
.
app/handlers/private/
telegraph.py
:
from aiogram import types from app.loader import dp from app.utils import photo_link @dp.message_handler(content_types=types.ContentTypes.PHOTO) async def photo_handler(msg: types.Message): photo = msg.photo[-1] link = await photo_link(photo) await msg.answer(link)
Думаю, что никому не нужно объяснять, что здесь происходит.
Также пропишем наш модуль в __init__.py
файле обработчиков.
app/handlers/__init__.py
:
from loguru import logger from .errors import retry_after from .private import start, telegraph logger.info("Handlers are successfully configured")
Логика
Наконец вернёмся к нашей утилите и приступим к написанию её логики
В первую очередь мы откроем отправленный пользователем файл сразу в байтах, используя io.BytesIO с помощью оператора with.
app/utils/
photo_link.py
:
from io import BytesIO from aiogram import types async def photo_link(photo: types.photo_size.PhotoSize) -> str: with await photo.download(BytesIO()) as file: pass return 'link'
Немного подробнее объясню, что здесь происходит, потому что могут возникнуть вопросы. Если при вызове метода download передать в качестве аргумента destination экземпляр любого класса, который является реализацией абстрактого класса io.IOBase
, например io.BytesIO
, который мы и используем, функция нам вернёт объект именно этого типа, который мы который мы можем использовать в качестве уже скачанного в оперативную память файла, поэтому качать нам ничего не придётся. Именно этот объект мы и открываем при помощи оператора with, чтобы потом не закрывать его самостоятельно.
Далее обратимся к документации aiohttp. Мы будем использовать описанный ими синтаксис для отправки файла из формы.
Подробнее о FormData в можете прочесть в документации.
Реализуем отправку файла. Отправлять post запрос мы будем с помощью клиентской сессии aiohttp, которую мы можем просто достать из экземпляра нашего бота. Получаем ответ — экземпляр aiohttp.ClientResponse
, десериализируя который получаем список следующего вида:
[{'src': '/file/1234567890qwertyuiopa.jpg'}]
Здесь находится нужный нам url адрес на картинку. для удобства запишем этот список в переменную img_src. Добавим к адресу префикс сайта telegra.ph и вернём данное значение из функции.
app/utils/
photo_link.py
from io import BytesIO import aiohttp from aiogram import types from app.loader import bot async def photo_link(photo: types.photo_size.PhotoSize) -> str: with await photo.download(BytesIO()) as file: form = aiohttp.FormData() form.add_field( name='file', value=file, ) async with bot.session.post('https://telegra.ph/upload', data=form) as response: img_src = await response.json() link = 'http://telegra.ph/' + img_src[0]["src"] return link
Можем запустить бота и проверить, всё ли работает:
Использование готового решения
Конечно, это уже можно использовать, но я также предлагаю вам рассмотреть реализацию подобного функционала с помощью aiogram/aiograph.
Для этого, внесём две библиотеки в файл requirements.txt
.
requirements.txt
attrs==19.1.0 aiograph==0.2 . . .
Здесь мы устанавливаем одну из зависимостей aiograph — attrs. Важно, что она должна быть именно версии 19.1.0, ибо в одной из следующих обновлений, они сломали обратную совместимость. ну и собственно сам aiograph. На момент написания статьи, последней версией является 0.2, с ней мы и будем работать.
Установим все зависимости.
terminal:
pip install -r requirements.txt
После этого создадим экземпляр aiograph.Telegraph
в файле loader.py
.
app/
loader.py
:
. . . from aiograph import Telegraph . . . telegraph = Telegraph() __all__ = ( . . . "telegraph", )
Перейдём в файл __main__.py и пропишем его закрытие, при падении бота. Для этого определим функцию on_shutdown
и передадим её в один из аргументов executor.start_polling
.
app/
__main__.py
:
. . . from aiogram import Dispatcher from aiogram.utils import executor from app.loader import dp, telegraph async def on_shutdown(dispatcher: Dispatcher): await telegraph.close() if __name__ == '__main__': utils.setup_logger("INFO", ["sqlalchemy.engine", "aiogram.bot.api"]) executor.start_polling( dp, on_startup=on_startup, skip_updates=config.SKIP_UPDATES )
Теперь можем приступать к написанию самого функционала.
Логика
Перейдём во всё тот же photo_link.py
и определим там ещё одну функцию, назовём её, допустим, photo_link_telegraph
.
app/utils/
photo_link.py
:
from aiogram import types . . . async def photo_link_aiograph(photo: types.photo_size.PhotoSize) -> str: return 'link'
Как и прежде, читаем нужные нам байтики:
with await photo.download(BytesIO()) as file:
и закидываем их в telegraph.upload, получая список ссылок:
links = await telegraph.upload(file)
Забираем первую ссылку из списка и возвращаем её из функции. Функция написана.
app/utils/
photo_link.py
:
from io import BytesIO from aiogram import types from app.loader import telegraph . . . async def photo_link_aiograph(photo: types.photo_size.PhotoSize) -> str: with await photo.download(BytesIO()) as file: links = await telegraph.upload(file) return links[0]
Не забудем вписать её в наш __init__.py файл.
app/utils/
__init__.py
:
. . . from .photo_link import photo_link, photo_link_aiograph __all__ = [ . . . "photo_link", "photo_link_aiograph", ]
Теперь перейдём в наш обработчик. Закомментируем вызов предыдущей функции и вызовем новую.
app/handlers/private/
telegraph.py
:
from aiogram import types from app.loader import dp from app.utils import photo_link, photo_link_aiograph @dp.message_handler(content_types=types.ContentTypes.PHOTO) async def photo_handler(msg: types.Message): photo = msg.photo[-1] # link = await photo_link(photo) link = await photo_link_aiograph(photo) await msg.answer(link)
После запуска бот должен работать как прежде.
Немножко красоты
Так как мы делаем запрос на сервер, боту необходимо некоторое время, чтобы получить ответ. У меня с не самым быстрым интернетом обработка пришедшего обновления занимает около одной, двух секунд. Мы используем асинхронную библиотеку для отправки запросов, поэтому никаких проблем не будет и бот будет продолжать работать во время ожидания ответа. Однако было бы неплохо оповестить пользователя о том, что бот не умер, а просто обрабатывает его сообщение.
Поэтому перед отправкой запроса (вызовом нашей функции photo_handler или photo_handler_aiograph), с помощью bot.send_chat_action мы установим статус в чате на upload_photo
, который сбросится после отправки сообщения.
app/handlers/private/
telegraph.py
:
from aiogram import types from app.loader import dp from app.utils import photo_link, photo_link_aiograph @dp.message_handler(content_types=types.ContentTypes.PHOTO) async def photo_handler(msg: types.Message): photo = msg.photo[-1] await msg.bot.send_chat_action(msg.chat.id, 'upload_photo') # link = await photo_link(photo) link = await photo_link_aiograph(photo) await msg.answer(link)
Теперь после отправки сообщения пользователь увидит, что бот отправляет фото:
Заключение
Получение прямой ссылки на изображение, отправленное пользователем — частая задача, решение которой не кажется таким уж интуитивным. Такие ссылки могут использоваться для миниатюры в статье инлайн ответа, для того, чтобы спрятать невидимую ссылку с картинкой в сообщении или просто отправить картинку в чат без права на отправку фото 🌚.
В этой статье мы разобрались, как можно реализовать отправку изображения пользователя на telegra.ph, и получение прямой ссылки на него.
Код данной статьи вы можете найти в нашем github репозитории.
В статье за основу взята новая структура для написания телеграм ботов на базе aiogram.