Усі нотатки

Telegram-бот за вечір: що ламається на другий день

Бота для заявок справді можна зробити за вечір. А потім хтось пише HTML у поле імені. Сім речей, які я закладаю в Bot Kit з першого дня.

(`・ω・´)ゞ 28 вересня 2026 р. · 3 хв читання PythonaiogramTelegram

«Нам потрібен простенький бот: меню, кілька питань і заявка менеджеру. За вечір зробите?» Так, за вечір зроблю . Бот запрацює, клієнт буде щасливий… до другого дня.

Вечір першого дня
Бот питає ім’я, телефон і задачу. Заявка падає адміну. Красиво, швидко, 80 рядків.
Ранок другого
Хтось кинув форму посередині. Хтось написав ім’я <b>Олег</b> і зламав повідомлення адміну. Бот перезапустили — і всі недописані форми зникли.

Ось що я закладаю в Bot Kit одразу, щоб другого дня не було.

1. Форма — це машина станів, а не купа if-ів

Кожне питання — окремий стан: name → contact → task → confirm. Користувач може зникнути на будь-якому кроці, повернутися через годину чи натиснути /start посеред форми. З машиною станів (у aiogram це StatesGroup) бот завжди знає, що чекає, і не приймає номер телефону за ім’я.

2. Стан — у Redis, а не в пам’яті

Типове сховище — пам’ять процесу. Перезапуск, оновлення, падіння — і всі недописані форми зникли. Redis живе окремо від бота:

def storage() -> BaseStorage:
    url = os.getenv("REDIS_URL")
    return RedisStorage.from_url(url) if url else MemoryStorage()

3. Введене користувачем — екранувати завжди

Бот надсилає заявку адміну з розміткою HTML (жирні підписи — це ж гарно). Але якщо просто вставити текст користувача, то <b> у полі імені стане розміткою, а незакритий тег Telegram відхилить — і заявка не дійде взагалі.

def summary(data: dict) -> str:
    esc = lambda k: html.escape(str(data.get(k, "—")))  # ввід не стає розміткою
    return f"<b>Ім'я:</b> {esc('name')}\n<b>Контакт:</b> {esc('contact')}\n<b>Задача:</b> {esc('task')}"

4. Обмежити довжину

Хтось обов’язково вставить у поле «Задача» весь договір. Повідомлення в Telegram обмежене 4096 символами, тож я ріжу кожне поле заздалегідь: ім’я — до 100 символів, задачу — до 1500.

5. Токен — лише в змінних середовища

BOT_TOKEN і ADMIN_ID беруться зі змінних, у коді їх немає. Якщо токен засвітився (скинули в чат, закомітили), його перевипускають у @BotFather за хвилину, і код не треба чіпати. Так, я теж колись отримував токен у чаті .

6. Тест без мережі

Бота можна перевірити без Telegram: підміняєш мережевий шар і «проганяєш» розмову оновленнями.

class FakeSession(BaseSession):
    async def make_request(self, bot, method, timeout=None):
        sent.append((type(method).__name__, getattr(method, "chat_id", None), getattr(method, "text", None)))
        return Message.model_validate({...})   # відповідь «від Telegram»

for up in [msg("/start"), cb("lead"), msg("Оля"), msg("@olya"), msg("Потрібен сайт"), cb("send")]:
    await dp.feed_update(bot, up)
assert any(chat == ADMIN_ID for _, chat, _ in sent)   # заявка дійшла адміну

Десять секунд, і ти знаєш, що вся форма від /start до «Дякуємо!» працює.

7. Логи й автоперезапуск

Бот має жити на сервері, де його перезапустять після падіння, а логи можна почитати. Тоді на «чому не прийшла заявка?» у тебе є відповідь, а не здогадки. У bebb Cloud це «Логи» з пошуком і сповіщення в Telegram, якщо бот падає по колу.

Підсумок

Бот за вечір — реальність. Бот, який не підведе на другий день, — це ще кілька годин на стани, екранування й тести. Ці години в Bot Kit уже вкладено, тож беріть і адаптуйте .

Маєте ідею? Давайте поговоримо

Опишіть задачу кількома реченнями — відповім з питаннями й першою оцінкою.