Привет, Хабр! Продолжаю серию статей про разработку telegram-ботов на библиотеке aiogram и языке программирования Python. Хочется отметить, что статья не является документацией или учебником. Я просто рассказываю пошагово как разработать полнофункционального бота, стараясь затронуть как можно больше тем. Если вы не увидели в статье чего-то очень важного по вашему мнению — предложите рассмотреть тему в следующей статье в комментариях.
Всем приятного чтения, жду вашего мнения, критики и вопросов в комментариях.
В предыдущей части мы настроили окружение и среду разработки и теперь готовы начать писать бота. В этой статье мы создадим меню и базовую логику взаимодействия с пользователем, а также подключим API OpenAI.
Создание меню
Меню нашего бота будет реализовано с помощью inline кнопок. Для начала надо создать клавиатуру главного меню. Все клавиатуры будут храниться в файле kb.py, поэтому открываем его и пишем в него такой код:
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove menu = [ [InlineKeyboardButton(text="📝 Генерировать текст", callback_data="generate_text"), InlineKeyboardButton(text="🖼 Генерировать изображение", callback_data="generate_image")], [InlineKeyboardButton(text="💳 Купить токены", callback_data="buy_tokens"), InlineKeyboardButton(text="💰 Баланс", callback_data="balance")], [InlineKeyboardButton(text="💎 Партнёрская программа", callback_data="ref"), InlineKeyboardButton(text="🎁 Бесплатные токены", callback_data="free_tokens")], [InlineKeyboardButton(text="🔎 Помощь", callback_data="help")] ] menu = InlineKeyboardMarkup(inline_keyboard=menu) exit_kb = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text="◀️ Выйти в меню")]], resize_keyboard=True) iexit_kb = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="◀️ Выйти в меню", callback_data="menu")]])
Здесь мы создаём основную клавиатуру menu, сразу же добавляя все кнопки в два столбца. Есть несколько способов создания клавиатур в aiogram 3:
Передать двумерный список кнопок как аргумент при создании клавиатуры. Данный способ используется в нашем проекте для создания меню. Удобен когда клавиатура статичная и все данные для неё заранее известны.
Использовать Keyboard Builder. Этот способ тоже будет использоваться для создания динамических клавиатур в дальнейшем. Чтобы создать клавиатуру через Keyboard Builder можно использовать следующий код:
builder = InlineKeyboardBuilder() for i in range(15): builder.button(text=f”Кнопка {i}”, callback_data=f”button_{i}”) builder.adjust(2) await msg.answer(“Текст сообщения”, reply_markup=builder.as_markup())
Здесь мы создаём Keyboard Builder и в цикле добавляем в него кнопки. builder.adjust(2) группирует кнопки в 2 столбца. Далее отправляется сообщение с созданной клавиатурой, которая из Keyboard Builder преобразовывается в Keyboard Markup функцией builder.as_markup() .
Клавиатура создана, теперь надо написать функции для вывода приветственного сообщения и меню. Тексты сообщений, используемые в боте, мы вынесем в отдельный файл text.py, чтобы можно было менять тексты, используемые в нескольких местах в боте, через изменение одной переменной в text.py.
greet = "Привет, {name}, я бот, использующий нейросети от OpenAI, такие как ChatGPT и Dall-e. Задавай мне вопросы и я постараюсь ответить ☺️" menu = "📍 Главное меню"
Теперь пишем обработчики в файле handlers.py.
from aiogram import F, Router, types from aiogram.filters import Command from aiogram.types import Message import kb import text router = Router() @router.message(Command("start")) async def start_handler(msg: Message): await msg.answer(text.greet.format(name=msg.from_user.full_name), reply_markup=kb.menu) @router.message(F.text == "Меню") @router.message(F.text == "Выйти в меню") @router.message(F.text == "◀️ Выйти в меню") async def menu(msg: Message): await msg.answer(text.menu, reply_markup=kb.menu)
Если вы всё сделали правильно, то после запуска бота (через файл main.py) вы увидите, что бот отвечает на команду /start и на слово «Меню».

Давайте подробнее разберём код, который мы написали. Первая часть файла похожа идентична предыдущей статье, а вот код обработчиков изменился. В функции start вместо отправки строки мы отправляем текст из переменной greet модуля text, причём форматируем его, подставляя имя пользователя (msg.from_user.full_name), а также прикрепляем к сообщению inline-клавиатуру, которую создали до этого. Далее добавился обработчик menu. Как вы могли заметить, перед объявлением функции стоят целых три декоратора. Это означает, что функция запустится, если сработает любой из трёх фильтров. В нашем примере функция будет реагировать на сообщения с текстом «Меню», «Выйти в меню» и «◀️ Выйти в меню». В самой функции нет ничего особенного, она просто отправит текст text.menu с клавиатурой kb.menu. Пока что ни один из наших пунктов меню не работает, давайте исправим это.
Подключение API OpenAI
Сейчас мы реализуем основной полезный функционал нашего бота — работа с нейросетями. Прежде всего стоит отметить, что API не бесплатное, однако при регистрации даётся бесплатно 5$ на счёт. Для тестов нам этого вполне хватит.
Сначала надо по��учить токен API, для этого регистрируемся на сайте OpenAI, используя VPN и иностранный номер телефона, можно использовать виртуальный, однако не на все виртуальные номера получается зарегистрироваться. После регистрации заходим в аккаунт, в раздел View API Keys и создаём ключ API. Обязательно скопируйте и сохраните ключ в надёжном месте, так как его нельзя будет посмотреть позже, только создать новый.
После того как вы получили API Key его надо сохранить в конфиге, например в переменной OPENAI_TOKEN. Также установите библиотеку openai для работы с API
pip install openai
Теперь когда все приготовления закончены, приступим к функциям для использования API. У нас будет две функции: для генерации текста и для генерации изображений, мы поместим их в файл utils.py, так как они не привязаны к aiogram и могут рассматриваться как внешний модуль.
import openai import logging import config openai.api_key = config.OPENAI_TOKEN async def generate_text(prompt) -> dict: try: response = await openai.ChatCompletion.acreate( model="gpt-3.5-turbo", messages=[ {"role": "user", "content": prompt} ] ) return response['choices'][0]['message']['content'], response['usage']['total_tokens'] except Exception as e: logging.error(e) async def generate_image(prompt, n=1, size="1024x1024") -> list[str]: try: response = await openai.Image.acreate( prompt=prompt, n=n, size=size ) urls = [] for i in response['data']: urls.append(i['url']) except Exception as e: logging.error(e) return [] else: return urls
Рассмотрим код подробнее. После импортов мы настраиваем библиотеку openai, давая ей наш ключ от API. Затем объявляем две функции:
generate_text— для генерации текстаgenerate_image— для генерации изображений
В обеих функциях используется конструкция try-catch для обработки исключений, в этом примере мы ничего не делаем, а лишь выводим ошибку в логи и возвращаем пустое значение.
Функция openai.ChatCompletion.acreate генерирует текст с помощью моделей завершения текста. В качестве параметров передаём используемую модель, в нашем случае gpt-3.5-turbo — самая дешёвая и быстрая на данный момент, и сообщения — список словарей с ключами system, user, assistant. Все переданные сообщения будут учтены при создании ответа, подробнее можете почитать в документации.
Мы передаём только сообщение от пользователя и используем поведение модели по умолчанию, но можно также передавать системные сообщения (role: system) для реализации режимов работы, например отдельные функции для написания кода и ответов на теоретические вопрос��. Также можно усовершенствовать функцию, чтобы сохранять контекст общения — нейросеть будет «помнить» предыдущие сообщения от пользователя и учитывать их при создании ответа. Всё это дополнительные усовершенствования, которые вы можете попробовать реализовать самостоятельно. Если эта тема будет интересна, то напишу отдельную статью, посвящённую реализации сохранения контекста и переключения режимов работы, поэтому пишите в комментариях, хотите ли видеть такую статью.
Вернёмся к нашим баранам. Последней строкой в функции мы возвращаем кортеж, состоящий из текста ответа от нейросети и количества израсходованных на запрос токенов. Если с текстом ответа всё понятно, то про токены стоит поговорить подробнее. Токен — базовая единица текста, воспринимаемая нейросетью. При отправке запроса специальный инструмент делит промпт (отправленное нейросети сообщение) на токены и в таком виде отдаёт модели. Для модели gpt-3.5-turbo один токен — примерно 3-4 английских буквы, либо 1 русская буква или любой другой символ, не входящий в английский алфавит. Количество использованных токенов определяет сколько вы заплатите за использование API. Для модели gpt-3.5-turbo на момент написания статьи 1 тысяча токенов стоит 0.002 доллара. Согласитесь, не очень много?
Функция генерации изображений очень похожа на предыдущую, но имеет немного другие параметры. Через API можно генерировать как одно, так и сразу несколько изображений, а также можно задать разрешение изображения на выходе. В нашей функции всегда генерируется одно изображение в максимальном разре��ении — 1024x1024. Однако мы всё равно перебираем полученные ссылки на изображения в цикле - так будет проще расширять функционал бота в будущем, например дать пользователям возможность генерировать несколько вариантов одного изображения и менять разрешение. Возвращается из функции список, состоящий из ссылок на полученные изображения.
Теперь, когда у нас есть база в виде функций генерации текста и изображений мы можем реализовать их работу с нашим ботом.
Подключение нейросетей к боту
Здесь нам нужно познакомиться с одной из самых удобных и мощных на мой взгляд функций aiogram, которой нет во многих других библиотеках — машиной состояний (её также называют машиной конечных автоматов или просто FSM).
FSM мы будем использовать чтобы бот принимал промпты для генерации текста и изображений только после нажатия соответствующей кнопки в меню, а также для того, чтобы различать сообщения из разных пунктов меню, так как бот не знает, в каком разделе меню находится пользователь.
Все состояния будем создавать в файле states.py
from aiogram.fsm.state import StatesGroup, State class Gen(StatesGroup): text_prompt = State() img_prompt = State()
Как видите всё очень просто, мы создали класс Gen и создали в нём два состояния:
text_prompt— бот будет воспринимать сообщения как промпты для ChatGPTimg_prompt— бот будет воспринимать сообщения как промпты для Dall-e
Последнее приготовление, которое надо сделать — подключить middleware для отображения статуса «Печатает…» во время когда бот ждёт ответа от API. Для этого мы подключим встроенную в aiogram middleware ChatActionMiddleware. В общих чертах middleware это некоторый код (функция, класс), выполняемый до передачи сообщения обработчику. В этом коде можно проверять какие-то данные, сохранять статистику, выполнять какие-либо действия или передавать дополнительные данные в обработчик. Подробнее мы рассмотрим middleware, когда будем реализовывать бан пользователей, а сейчас просто подключим уже готовую middleware. Для этого в файле main.py надо добавить импорт from aiogram.utils.chat_action import ChatActionMiddleware и перед запуском бота вставить строку dp.message.middleware(ChatActionMiddleware()). Так мы подключили middleware для отправки состояний бота пользователям.
Теперь приступаем к основному коду обработчиков. handlers.py. Код, который мы писали ранее дублировать не буду, обращу лишь внимание, что требуется 4 новых импорта:
from aiogram import flags from aiogram.fsm.context import FSMContext import utils from states import Gen
Остальной код:
@router.callback_query(F.data == "generate_text") async def input_text_prompt(clbck: CallbackQuery, state: FSMContext): await state.set_state(Gen.text_prompt) await clbck.message.edit_text(text.gen_text) await clbck.message.answer(text.gen_exit, reply_markup=kb.exit_kb) @router.message(Gen.text_prompt) @flags.chat_action("typing") async def generate_text(msg: Message, state: FSMContext): prompt = msg.text mesg = await msg.answer(text.gen_wait) res = await utils.generate_text(prompt) if not res: return await mesg.edit_text(text.gen_error, reply_markup=kb.iexit_kb) await mesg.edit_text(res[0] + text.text_watermark, disable_web_page_preview=True) @router.callback_query(F.data == "generate_image") async def input_image_prompt(clbck: CallbackQuery, state: FSMContext): await state.set_state(Gen.img_prompt) await clbck.message.edit_text(text.gen_image) await clbck.message.answer(text.gen_exit, reply_markup=kb.exit_kb) @router.message(Gen.img_prompt) @flags.chat_action("upload_photo") async def generate_image(msg: Message, state: FSMContext): prompt = msg.text mesg = await msg.answer(text.gen_wait) img_res = await utils.generate_image(prompt) if len(img_res) == 0: return await mesg.edit_text(text.gen_error, reply_markup=kb.iexit_kb) await mesg.delete() await mesg.answer_photo(photo=img_res[0], caption=text.img_watermark)
Обратите внимание, что сообщения отправляются с текстами из модуля text, поэтому надо там их создать
gen_text = "📝 Отправьте текст запроса к нейросети для генерации текста" gen_image = "🖼 Отправьте текст запроса к нейросети для генерации изображения" gen_exit = "Чтобы выйти из диалога с нейросетью нажмите на кнопку ниже" gen_error = f'🚫 Ошибка генерации. Возможные причины:\n1. Перегружены сервера OpenAI\n2. Ваш запрос нарушил правила OpenAI\n3. Ошибка в работе бота\nЕсли вы считаете, что проблема вызвана неисправностью бота, сообщите админу' text_watermark = '\n_______________________________________\nСоздано при помощи @dalle_chatgpt_bot' img_watermark = "Создано при помощи @dalle_chatgpt_bot" gen_wait = "⏳Пожалуйста, подождите немного, пока нейросеть обрабатывает ваш запрос..." err = "🚫 К сожалению произошла ошибка, попробуйте позже"
Теперь проанализируем код обработчиков. В нашем коде впервые встречается такой декоратор как callback_query. Он означает, что функция будет реагировать на нажатия inline-кнопок с определённым фильтром. Если при обработке сообщений надо было использовать выражение F.text, то теперь мы используем F.data.
Также появились новые строки установки состояний через set_state. Данная функция устанавливает для пользователя переданное ей состояние (предварительно созданное как класс). Потом мы обрабатываем сообщения с фильтром состояния @router.message(Gen.text_prompt). Это означает что функция будет реагировать только на те входящие сообщения, которые были отправлены после установки состояния в предыдущей функции.
Ещё появился интересный декоратор @flags.chat_action(«typing»). Именно для его использования мы подключали ChatActionMiddleware. Его функция очень проста — пока выполняется функция, к которой он прикреплён, у пользователя будет отображаться, что бот «печатает…», также можно задать любой другой статус, далее в коде этим же способом устанавливается статус «отправляет фото…». Думаю что весь код с функциями answer и edit_text интуитивно понятен — мы либо отвечаем текстом и клавиатурой на сообщение, либо редактируем то сообщение, от которого нам пришёл Callback Query.
В функциях где работа идёт с функциями из utils выполняется проверка на ошибки — если API вернёт ошибку или она произойдёт в самой функции, то она вернёт нам None. Поэтому перед отправкой пользователю сообщения с ответом мы проверяем, успешно ли функция отработала. К каждому ответу от бота будет прикреплён специальный текст, у меня помещённый в переменную text.text_watermark. Это обеспечит нам некоторую рекламу, если пользователь решит переслать ответ нейросети другому пользователю.
Параметр disable_web_page_preview отвечает за отображение превью ссылок. Нам это ни к чему, поэтому устанавливаем параметр в True.
Теперь наш бот умеет отвечать на текстовые запросы по API и генерировать изображения:

Заметьте, что если отправлять боту несколько запросов подряд, не выходя в меню, то он будет работать в том режиме, который вы выбрали через меню до тех пор пока вы не решите выйти в меню. В дальнейшем опять же можно реализовать запоминание контекста общения и воспользоваться этой особенностью нашего кода, однако пока мы просто скажем, что это такая фича.
Заключение
Что ж, на этом вторая часть заканчивается. Мы сделали немало работы — подключили API OpenAI к Python, создали клавиатуру меню и даже связали это всё вместе! Сейчас бот уже полноценно функционирует и может отвечать на запросы. Однако работа на этом не заканчивается, так как функционал нашего бота намного обширнее.
В следующей части мы подключим базу данных PostgreSQL к нашему боту, реализуем партнёрскую программу, вывод баланса пользователя и помощи по работе с ботом и обязательную подписку на канал для работы с ботом.
Жду вашего мнения, критики, советов и вопросов в комментариях!
