Телеграм-бот для поиска фотографий через API-ключ фотостока

Материал из Поле цифровой дидактики



Определение функциональных требований к боту

Перед работой над созданием бота, я решил создать реестр требований, чтобы точно отслеживать какие моменты я хочу увидеть в работе бота.

В моем представлении, бот, работающий с API фотостока, должен уметь работать с медиа-файлами. Помимо всего прочего, бот - есть бот, он должен уметь первоначально взаимодействовать с пользователем и отзываться на его команды. Соответственно, необходимо эти команды придумать и понять, что пользователь захочет сделать.

Размышляя таким образом, я пришел к следующему реестру функциональных и нефункциональных требований:

  • Бот должен уметь начинать работу с пользователем через команду /start
  • Бот должен уметь рассказывать о себе и о том зачем он нужен
  • В случае, если пользователь забыл команды - напомнить пользователю о них через команду /help
  • Иметь возможность работать как с открытыми запросами (на случай, если нужна случайная фотография), так и с закрытыми (при конкретных запросах)
  • Выдавать ссылку на источник с фотографией, при выборе пользователем нужной фотографии (из 4-х)



Получение API и создание внутреннего конфигурационного API файла

Далее также важный момент, для работы бота потребуется ДВА API ключа. Первый - Telegram Bot Key, второй - как раз Unsplash API.

Для того, чтобы получить ключ для бота Telegram, необходимо его создать. Сделать это можно через бота @BotFather.

При заходе в бота, необходимо открыть внутреннее интегрированное приложение через кнопку "Open" и создать своего бота:

После получения доступа к API бота - его нужно будет в дальнейшем сохранить в файл .env в той же папке, где и сам файл Python, но об этом чуть позже.


Теперь нужно получить API ключ фотостока, я выбрал Unsplash, так как получение API там не затруднено лишними тарифными планами и дополнительными регистрациями.

Чтобы получить доступ к API Unsplash необходимо зайти на их сайт, зарегистрироваться и в личном кабинете открыть новое приложение (проект), указать его вид и название. Важно, что Access и Secret ключи должны быть конфиденциальны и не могут передаваться третьим лицам, поэтому в этом проекте и статье, ключи будут скрыты.

После получения всех требуемых API ключей - сохраняем их в файл окружения .env в переменные, которые в дальнейшем будем использовать в Python-коде.


Разработка бота

Начало работы и создание заглушки

Для начала был создан файлик с начальными зависимостями в Python

"requirements.txt"

После создания этого файлика необходимо запустить команду `pip install -r [путь к файлу]` и указать путь к созданному requirements.txt

pip install -r requirements.txt

Начало разработки кода

Так как разработка в Python позволяет работать с разными существующими библиотеками, имеющие свои документации, будем пользоваться предоставленными правами.

Для работы бота я подключил несколько библиотек, такие как:

  • python-telegram-bot - основная библиотека для создания Telegram ботов
  • requests - библиотека для отправки HTTP-запросов к API Unsplash
  • python-dotenv - загрузка переменных окружения из файла .env
  • logging - логирование событий и ошибок для отладки
  • os - взаимодействие с операционной системой
  • uuid - генерация уникальных идентификаторов

В соответствии с ранее описанными функциональными требованиями диаграмма UML ниже

Непосредственная работа с кодом приложения

Код перед созданием класса =

Импорт вышеописанных библиотек и подгрузка переменных из файла .env:

import logging
import os
import uuid
from dotenv import load_dotenv
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes, CallbackQueryHandler
import requests

load_dotenv()

Настройка логирования

logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO
)
logger = logging.getLogger(__name__)
Работа с классом

Далее, так как пользователей у бота может быть несколько - крайне важно создать класс, который поможет работать нескольким пользователям одновременно в соответствие с объектно-ориентированным программированием.

Для этого я создал класс, в котором указал основные метаданные, а также функции:

class UnsplashTelegramBot:
    def __init__(self):
        self.unsplash_key = os.getenv('UNSPLASH_ACCESS_KEY')
        self.telegram_token = os.getenv('TELEGRAM_BOT_TOKEN')
        self.unsplash_url = "https://api.unsplash.com"
        self.headers = {
            'Authorization': f'Client-ID {self.unsplash_key}'
        }

        self.search_results = {}
Программирование функций класса бота
  • /start - основная команда, которая будет запускать бота и создавать класс в нем. После отправки этой команды пользователь должен получать информацию о боте.
    async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Команда /start"""
        welcome_message = (
            "Привет! Я бот для поиска фотографий на Unsplash.\n\n"
            "Команды:\n"
            "/search <запрос> - поиск и выбор из 4 фотографий\n"
            "/random - случайное фото\n"
            "/popular - популярные фото\n"
            "/help - помощь"
        )
        await update.message.reply_text(welcome_message)


  • /help - дополнительная команда, которая будет отчасти повторять функционал команды /start, но описывать функционал более подробно.
    async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Команда /help"""
        help_text = (
            "? Как пользоваться ботом:\n\n"
            "1. Для поиска фото напиши: /search природа\n"
            "   Бот покажет 4 фото на выбор\n"
            "2. Нажми на кнопку под фото, чтобы получить ссылку на источник\n"
            "3. Для случайного фото: /random\n"
            "4. Для популярных фото: /popular\n\n"
            "Также можно просто отправить текстовый запрос для поиска!"
        )
        await update.message.reply_text(help_text)


  • /search или сообщение без команды - основа всего бота, поиск фотографий и показ 4-х фотографий.
    async def search_photos(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Поиск фотографий и показ 4 вариантов"""

        if context.args:
            query = ' '.join(context.args)
        else:
            # Команда без арг-тов, просто сообщение
            if update.message and update.message.text and not update.message.text.startswith('/'):
                query = update.message.text
            else:
                await update.message.reply_text("Пожалуйста, укажи запрос. Например: /search природа")
                return
        
        # Отправляем сообщение о поиске
        status_message = await update.message.reply_text(f"Ищу фото по запросу: {query}...")
        
        try:
            # Получаем 4 фотографии
            response = requests.get(
                f"{self.unsplash_url}/search/photos",
                headers=self.headers,
                params={'query': query, 'per_page': 4, 'page': 1}
            )
            
            if response.status_code == 200:
                data = response.json()
                if data['results']:
                    # Удаляем сообщение о поиске
                    await status_message.delete()
                    
                    # Уникальный ID
                    search_id = str(uuid.uuid4())[:8]
                    self.search_results[search_id] = data['results']
                    
                    for i, photo in enumerate(data['results'], 1):
                        caption = f"Вариант {i} из 4\n👤 Автор: {photo['user']['name']}"
                        
                        keyboard = [
                            [InlineKeyboardButton(
                                "Получить ссылку на источник", 
                                callback_data=f"get_link_{search_id}_{i-1}"
                            )]
                        ]
                        reply_markup = InlineKeyboardMarkup(keyboard)
                        
                        await update.message.reply_photo(
                            photo=photo['urls']['regular'],
                            caption=caption,
                            reply_markup=reply_markup
                        )
                    
                    # Отправляем сообщение с инструкцией
                    await update.message.reply_text(
                        f"✅ Найдено {data['total']} фото. Выбери понравившийся вариант, чтобы получить ссылку на источник!"
                    )
                else:
                    await status_message.edit_text("Ничего не найдено. Попробуй другой запрос.")
            else:
                await status_message.edit_text("❌ Ошибка при поиске фото. Попробуй позже.")
                
        except Exception as e:
            logger.error(f"Ошибка: {e}")
            await status_message.edit_text("❌ Произошла ошибка. Попробуй еще раз.")
  • /random - случайное фото из Unsplash.
    async def random_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Случайное фото"""
        await self.send_random_photo(update, context)
    
    async def send_random_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Отправка случайного фото"""
        # Определяем, откуда пришел запрос
        if update.callback_query:
            message = update.callback_query.message
            await update.callback_query.answer()
        else:
            message = update.message
        
        query = ' '.join(context.args) if context.args and hasattr(context, 'args') else None
        
        status_msg = await message.reply_text("🎲 Получаю случайное фото...")
        
        try:
            params = {}
            if query:
                params['query'] = query
            
            response = requests.get(
                f"{self.unsplash_url}/photos/random",
                headers=self.headers,
                params=params
            )
            
            if response.status_code == 200:
                photo = response.json()
                await status_msg.delete()
                
                caption = (
                    f"**Случайное фото**\n\n"
                    f"👤 **Автор:** {photo['user']['name']}\n"
                    f"📝 **Описание:** {photo.get('description', 'Нет описания')}\n\n"
                    f"[🔗 Открыть на Unsplash]({photo['links']['html']})"
                )
                
                # Создаем кнопки для действий с фото
                keyboard = [
                    [InlineKeyboardButton("🔗 Получить все ссылки", callback_data=f"get_link_random_{photo['id']}")],
                    [InlineKeyboardButton("🔄 Еще случайное", callback_data="random_photo")]
                ]
                reply_markup = InlineKeyboardMarkup(keyboard)
                
                await message.reply_photo(
                    photo=photo['urls']['regular'],
                    caption=caption,
                    parse_mode='Markdown',
                    reply_markup=reply_markup
                )
                
                # Сохраняем фото для возможного запроса ссылок
                if not hasattr(self, 'random_photos'):
                    self.random_photos = {}
                self.random_photos[photo['id']] = photo
                
            else:
                await status_msg.edit_text("❌ Ошибка при получении случайного фото.")
                
        except Exception as e:
            logger.error(f"Ошибка: {e}")
            await status_msg.edit_text("❌ Произошла ошибка. Попробуй еще раз.")


  • /popular - случайное фото из подборки "Популярное" из Unsplash. делается наподобие команды /random.
    async def popular_photos(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
        """Популярные фото"""
        status_msg = await update.message.reply_text("🔥 Загружаю популярные фото...")
        
        try:
            response = requests.get(
                f"{self.unsplash_url}/photos",
                headers=self.headers,
                params={'per_page': 5, 'order_by': 'popular'}
            )
            
            if response.status_code == 200:
                photos = response.json()
                await status_msg.delete()
                
                for i, photo in enumerate(photos, 1):
                    caption = (
                        f"🔥 **Популярное фото #{i}**\n\n"
                        f"👤 **Автор:** {photo['user']['name']}\n"
                        f"❤️ **Лайков:** {photo['likes']}\n\n"
                        f"[🔗 Открыть на Unsplash]({photo['links']['html']})"
                    )
                    
                    # Кнопка для получения ссылок
                    keyboard = [[InlineKeyboardButton("🔗 Получить ссылки", callback_data=f"get_link_popular_{photo['id']}")]]
                    reply_markup = InlineKeyboardMarkup(keyboard)
                    
                    await update.message.reply_photo(
                        photo=photo['urls']['regular'],
                        caption=caption,
                        parse_mode='Markdown',
                        reply_markup=reply_markup
                    )
                    
                    # Сохраняем фото
                    if not hasattr(self, 'popular_photos_cache'):
                        self.popular_photos_cache = {}
                    self.popular_photos_cache[photo['id']] = photo
            else:
                await status_msg.edit_text("❌ Ошибка при загрузке популярных фото.")
                
        except Exception as e:
            logger.error(f"Ошибка: {e}")
            await status_msg.edit_text("❌ Произошла ошибка.")
Написание обработчика на запуск бота

Функция на запуск бота, включает в себя создание приложения, обработчики команд и логирование всех команд в терминале.

    def run(self):
        """Запуск бота"""
        application = Application.builder().token(self.telegram_token).build()
        
        application.add_handler(CommandHandler("start", self.start))
        application.add_handler(CommandHandler("help", self.help_command))
        application.add_handler(CommandHandler("search", self.search_photos))
        application.add_handler(CommandHandler("random", self.random_photo))
        application.add_handler(CommandHandler("popular", self.popular_photos))
        application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message))
        application.add_handler(CallbackQueryHandler(self.handle_callback))
        
        print("/start")
        application.run_polling(allowed_updates=Update.ALL_TYPES)


Вне класса также создаем класс и запускаем бота.

if __name__ == "__main__":
    bot = UnsplashTelegramBot()
    bot.run()

Запуск бота и его тестирование

Производим запуск бота через запуск исполняемого python файла:

После этого действия можем начинать работать и тестировать бота.

Переходим в телеграм, пишем команду /start

Как видим, запрос прошел успешно, бот ответил то, что я зашил в код программы.

Попробуем получить случайное фото командой /random:


Теперь попробуем найти что-то конкретное и посмотреть как это будет выглядеть. Для этого сформируем команду /search. Искать будем собаку с породой самоед:

Как видим, бот отправил нам 4 варианта фотографий, к каждой из которой добавил ссылку на оригинал.

Притом каждый раз в терминале (консоли) мы можем заметить, как бот поддерживает подключение с Unsplash и логирует отправку сообщений.

Методические материалы

 Description
PythonPython в русском языке распространено название пито́н) — высокоуровневый язык программирования общего назначения, ориентированный на повышение производительности разработчика и читаемости кода. Синтаксис ядра Python минималистичен. В то же время стандартная библиотека включает большой объём полезных функций. Язык является полностью объектно-ориентированным в том плане, что всё является объектами
 Description
UMLUML (англ. Unified Modeling Language — унифицированный язык моделирования) — язык графического описания для объектного моделирования в области разработки программного обеспечения, для моделирования бизнес-процессов, системного проектирования и отображения организационных структур.
 Description
Telegram
 Description
APIИнтерфейс прикладного программирования application programming interface (API) - — описание способов взаимодействия одной компьютерной программы с другими. API (интерфейс прикладного программирования) упрощает процесс программирования при создании приложений, абстрагируя базовую реализацию и предоставляя только объекты или действия, необходимые разработчику. Если графический интерфейс для почтового клиента может предоставить пользователю кнопку, которая выполнит все шаги для выборки и выделения новых писем, то API для ввода/вывода файлов может дать разработчику функцию, которая копирует файл из одного места в другое, не требуя от разработчика понимания операций файловой системы.


 Description
APIИнтерфейс прикладного программирования application programming interface (API) - — описание способов взаимодействия одной компьютерной программы с другими. API (интерфейс прикладного программирования) упрощает процесс программирования при создании приложений, абстрагируя базовую реализацию и предоставляя только объекты или действия, необходимые разработчику. Если графический интерфейс для почтового клиента может предоставить пользователю кнопку, которая выполнит все шаги для выборки и выделения новых писем, то API для ввода/вывода файлов может дать разработчику функцию, которая копирует файл из одного места в другое, не требуя от разработчика понимания операций файловой системы.
PythonPython в русском языке распространено название пито́н) — высокоуровневый язык программирования общего назначения, ориентированный на повышение производительности разработчика и читаемости кода. Синтаксис ядра Python минималистичен. В то же время стандартная библиотека включает большой объём полезных функций. Язык является полностью объектно-ориентированным в том плане, что всё является объектами
UMLUML (англ. Unified Modeling Language — унифицированный язык моделирования) — язык графического описания для объектного моделирования в области разработки программного обеспечения, для моделирования бизнес-процессов, системного проектирования и отображения организационных структур.

CherenkovIR (обсуждение)