Прямая ссылка на фото с telegraph

Прямая ссылка на фото с telegraph

Forzend
Прямая ссылка на фото с telegraph


В этой статье мы с вами напишем бота, который будет загружать картинку пользователя на telegra.ph (на котором вы, собственно, сейчас и читаете эту статью) и возвращать прямую ссылку на фото. Это может быть полезно, например, для создания скрытых c preview, ссылок на фото или добавления thumb_url к InlineQueryResultArticle.

Результат работы

Результат работы

Как мы видим, после того, как мы отправляем боту картинку, бот генерирует и возвращает нам ссылку на эту самую картинку на telegra.ph, которая имеет удобный предпросмотр.

В обработчике у нас будет всего пара строк.

message_handler

Вся логика будет умещена в две функции (какую из двух использовать в своих "боевых" проектах вы решите сами), которые мы поместим в пакет app.utils.

В итоге мы реализуем две утилиты: написанную лично, при помощи обычных post запросов aiohttp, и с использованием готового решения, представленного разработчиками aiogram, — aiograph. Обе функции будут возвращать нам строку — ссылку на уже загруженное изображение.

В итоге модуль будет выглядеть следующим образом:

photo_link module

Содержание статьи

┌── Результат работы
├─> Содержание статьи
├── Написание своего собственного модуля
|   └── Логика
├── Использование готового решания
|   └── Логика
├── Немножко красоты
└── Заключение
В этой статье будет использоваться структура, описанная в одной из предыдущих статей (вы также можете найти её на 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

Можем запустить бота и проверить, всё ли работает:

bot logging
Работа бота

Использование готового решения

Конечно, это уже можно использовать, но я также предлагаю вам рассмотреть реализацию подобного функционала с помощью aiogram/aiograph.

Для этого, внесём две библиотеки в файл requirements.txt.

requirements.txt
attrs==19.1.0
aiograph==0.2
. . .

Здесь мы устанавливаем одну из зависимостей aiographattrs. Важно, что она должна быть именно версии 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)  

Теперь после отправки сообщения пользователь увидит, что бот отправляет фото:

Cтатус отправки изоображения

Заключение

Получение прямой ссылки на изображение, отправленное пользователем — частая задача, решение которой не кажется таким уж интуитивным. Такие ссылки могут использоваться для миниатюры в статье инлайн ответа, для того, чтобы спрятать невидимую ссылку с картинкой в сообщении или просто отправить картинку в чат без права на отправку фото 🌚.

В этой статье мы разобрались, как можно реализовать отправку изображения пользователя на telegra.ph, и получение прямой ссылки на него.

Код данной статьи вы можете найти в нашем github репозитории.

В статье за основу взята новая структура для написания телеграм ботов на базе aiogram.

Report Page