Усі роботи
Концепт
Bot Kit — шаблон Telegram-бота
Шаблон бота для заявок: меню, покрокова форма, сповіщення адміну — готовий до bebb Cloud.
- Роль
- Backend, DevOps
- Рік
- 2026
- Стек
- Pythonaiogram 3RedisGitbebb Cloud
Задача
Малому бізнесу часто потрібен простий бот для заявок, але більшість «конструкторів» дорогі й закриті. Потрібен чистий шаблон, який легко адаптувати.
Що зроблено
- Бот на aiogram 3 з машиною станів: меню, покрокова форма, підтвердження.
- Заявки надходять адміну в особисті повідомлення, стан зберігається в Redis.
- Токен і налаштування — лише в змінних середовища, не в коді.
- Готовий до bebb Cloud: Git-деплой, змінні в кабінеті, Redis для стану.
Код
"""Bot Kit — шаблон Telegram-бота для заявок (aiogram 3).
Меню → покрокова форма (ім'я, контакт, задача) → підтвердження → заявка адміну в особисті.
Налаштування лише зі змінних середовища: BOT_TOKEN, ADMIN_ID, REDIS_URL (необов'язково).
"""
import asyncio
import html
import logging
import os
from aiogram import Bot, Dispatcher, F, Router
from aiogram.client.default import DefaultBotProperties
from aiogram.filters import Command, CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.base import BaseStorage
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
router = Router()
class Lead(StatesGroup):
name = State()
contact = State()
task = State()
confirm = State()
def kb(*rows: list[tuple[str, str]]) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t, callback_data=d) for t, d in row] for row in rows])
MENU = kb([("📝 Залишити заявку", "lead")], [("💼 Послуги", "services"), ("📞 Контакти", "contacts")])
@router.message(CommandStart())
async def start(message: Message, state: FSMContext) -> None:
await state.clear()
await message.answer("Привіт! Я допоможу залишити заявку — це займе хвилину.", reply_markup=MENU)
@router.callback_query(F.data == "services")
async def services(cb: CallbackQuery) -> None:
await cb.message.answer("Сайти, боти, сервери й супровід. Натисніть «Залишити заявку» — розповімо деталі.", reply_markup=MENU)
await cb.answer()
@router.callback_query(F.data == "contacts")
async def contacts(cb: CallbackQuery) -> None:
await cb.message.answer("Пишіть сюди, у бот, — ми відповідаємо протягом дня.", reply_markup=MENU)
await cb.answer()
@router.callback_query(F.data == "lead")
async def lead_start(cb: CallbackQuery, state: FSMContext) -> None:
await state.set_state(Lead.name)
await cb.message.answer("Як до вас звертатися?")
await cb.answer()
@router.message(Command("cancel"))
async def cancel(message: Message, state: FSMContext) -> None:
await state.clear()
await message.answer("Скасовано. Повернутися до меню — /start")
@router.message(Lead.name, F.text)
async def lead_name(message: Message, state: FSMContext) -> None:
await state.update_data(name=message.text[:100])
await state.set_state(Lead.contact)
await message.answer("Як з вами зв'язатися? Телефон, пошта або @нік.")
@router.message(Lead.contact, F.text)
async def lead_contact(message: Message, state: FSMContext) -> None:
await state.update_data(contact=message.text[:100])
await state.set_state(Lead.task)
await message.answer("Коротко опишіть задачу — кількох речень достатньо.")
@router.message(Lead.task, F.text)
async def lead_task(message: Message, state: FSMContext) -> None:
data = await state.update_data(task=message.text[:1500])
await state.set_state(Lead.confirm)
await message.answer(summary(data) + "\n\nНадіслати?", reply_markup=kb([("✅ Надіслати", "send"), ("✏️ Заново", "lead")]))
@router.callback_query(Lead.confirm, F.data == "send")
async def lead_send(cb: CallbackQuery, state: FSMContext, bot: Bot, admin_id: int) -> None:
data = await state.get_data()
user = cb.from_user
who = f"@{user.username}" if user.username else f"id {user.id}"
await bot.send_message(admin_id, f"🆕 <b>Нова заявка</b> від {html.escape(who)}\n\n{summary(data)}")
await state.clear()
await cb.message.edit_text("Дякуємо! Заявку отримано — відповімо найближчим часом. 🙌")
await cb.answer()
def summary(data: dict) -> str:
esc = lambda k: html.escape(str(data.get(k, "—"))) # noqa: E731 — ввід користувача не має стати розміткою
return f"<b>Ім'я:</b> {esc('name')}\n<b>Контакт:</b> {esc('contact')}\n<b>Задача:</b> {esc('task')}"
def storage() -> BaseStorage:
"""Redis зберігає незавершені форми між перезапусками; без REDIS_URL — пам'ять процесу."""
url = os.getenv("REDIS_URL")
if url:
from aiogram.fsm.storage.redis import RedisStorage
return RedisStorage.from_url(url)
return MemoryStorage()
def build() -> tuple[Bot, Dispatcher]:
bot = Bot(os.environ["BOT_TOKEN"], default=DefaultBotProperties(parse_mode="HTML"))
dp = Dispatcher(storage=storage(), admin_id=int(os.environ["ADMIN_ID"]))
dp.include_router(router)
return bot, dp
async def main() -> None:
logging.basicConfig(level=logging.INFO)
bot, dp = build()
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main()) Результат
- Повна розмова (старт → форма → заявка адміну) перевірена автотестом без мережі; введене користувачем екранується.
- Повний код — на сторінці кейсу: його можна взяти й адаптувати під свій бізнес.
Що далі
Оплати в боті й адмін-панель заявок.