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

