Telegram-бот: user-сценарії (UML + sequence)
Повний набір сценаріїв Abitly Telegram-бота (@abitlybot, aiogram 3 · AWS ECS) очима користувача — на крос-сервісному рівні цього хабу. Кожен потік простежується до коду; під діаграмами — посилання на хендлер/сервіс у репозиторії abitly-tg-bot-v3.
Учасники діаграм
Section titled “Учасники діаграм”Однакові скорочення у всіх sequence-діаграмах нижче:
| Скор. | Учасник |
|---|---|
| U | Абітурієнт (користувач Telegram) |
| A | Адмін (id ∈ ADMIN_IDS) |
| TG | Telegram Bot API (long polling) |
| B | Bot — aiogram 3 на AWS ECS (хендлери → сервіси → репозиторії) |
| PG | Postgres: схема abitly (дані вступу) + telegram (логи апдейтів) |
| R | Redis (sidecar): FSM-storage + одноразові link-токени |
| API | abitly-api-v2 — власник схеми; мінтить link-токени |
| SND | MessageSender — черга масової відправки (rate-cap, 429-retry) |
| MINI | Mini App (Next.js · Railway), запускається кнопкою web_app |
Use-case: усі сценарії на одній мапі (UML use case)
Section titled “Use-case: усі сценарії на одній мапі (UML use case)”flowchart LR
U(["👤 Абітурієнт"])
A(["👤 Адмін"])
SCH(["⏰ Планувальник 07:00"])
BE(["🔧 abitly-api-v2"])
subgraph bot["@abitlybot"]
direction TB
subgraph uc_info["Інформація"]
uc1["Вставити URL офера → бали й рейтинг"]
uc2["Шанси на вступ (deep-link)"]
uc3["Дні відкритих дверей ЗВО (deep-link)"]
end
subgraph uc_track["Моніторинг"]
uc4["Додати / прибрати офер (ліміт 5)"]
uc5["Мої офери"]
uc6["Моя статистика"]
end
subgraph uc_me["Профіль і події"]
uc7["Мій профіль"]
uc8["Мої дні відкритих дверей"]
uc9["Фільтри сповіщень"]
uc10["Увімкнути / вимкнути сповіщення"]
end
subgraph uc_account["Акаунт"]
uc11["Прив'язати веб-акаунт (deep-link)"]
uc12["Відкрити веб-застосунок / Mini App"]
end
subgraph uc_admin["Адмін"]
uc13["Розсилка всім (broadcast)"]
uc14["Фан-аут оновлень днів відкритих дверей"]
end
uc15["Щоденні нагадування про дні відкритих дверей"]
end
U --> uc1 & uc2 & uc3
U --> uc4 & uc5 & uc6
U --> uc7 & uc8 & uc9 & uc10
U --> uc11 & uc12
A --> uc13 & uc14
SCH --> uc15
BE -. мінтить токен .-> uc11
uc15 -. нагадування .-> U
Індекс: команда / кнопка → сценарій
Section titled “Індекс: команда / кнопка → сценарій”| Тригер | Тип | Сценарій |
|---|---|---|
/start | команда | Привітання |
/start link_<token> | deep-link | Прив’язка акаунту |
/start chances_<spec>_<score> | deep-link | Шанси на вступ |
/start promo_blog | deep-link | Промокод −200 грн |
/start open_day_university_<id> | deep-link | Дні відкритих дверей ЗВО |
| (вставлений URL офера) | текст (catch-all) | Статистика офера |
add:<id> / remove:<id> | callback | Моніторинг офера |
/myoffers | команда | Мої офери |
/myprofile, «👤 Профіль» | команда / кнопка | Профіль |
/statistics | команда | Статистика |
/myopendays, «📅 Мої події» | команда / кнопка | Мої дні відкритих дверей |
/opendayfilters | команда | Фільтри сповіщень |
/notifications, 🔔/🔕 | команда / кнопка | Сповіщення |
/help | команда | Довідка (no-op) |
/broadcast, /notifyOpenDaysUpdate | admin | Адмін-розсилки |
Спільний шлях кожного апдейту (middleware)
Section titled “Спільний шлях кожного апдейту (middleware)”Цей конвеєр однаковий для всіх сценаріїв нижче; далі показано лише специфічну частину кожного.
sequenceDiagram
autonumber
actor U as Абітурієнт
participant TG as Telegram API
participant B as Bot (aiogram · ECS)
participant PG as Postgres
U->>TG: команда / текст / натискання кнопки
TG->>B: update (long polling)
Note over B: DbSessionMiddleware (outer) — 1 сесія = 1 транзакція
B->>PG: INSERT telegram.update_log (вхідний апдейт)
Note over B: ReposMiddleware — будує repos+services<br/>UserUpsertMiddleware — upsert telegram_users, last_interacted_at
B->>B: маршрутизація у хендлер (admin→start→…→offer_text LAST)
B-->>TG: відповідь (sendMessage / editMessageText)
B->>PG: INSERT telegram.sent_log (вихідний виклик)
Note over B: commit при успіху · rollback при винятку
Код: middlewares/ (db_session, repos, user_upsert, update_log, outgoing_log), handlers/__init__.py. Логи — ADR 0009/0010.
/start — привітання
Section titled “/start — привітання”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres (abitly)
U->>B: /start
alt новий користувач (tg_user_is_new)
B-->>U: привітання + reply-меню (📅 / 🔔 / 👤)
end
B->>PG: count_upcoming_events + count_tracked_open_days
PG-->>B: лічильники подій
B-->>U: динамічне привітання + inline «🌐 Відкрити веб-застосунок»
Якщо в /start є payload (link_…, chances_…, open_day_university_…) — спрацьовує відповідна гілка нижче замість дефолтного привітання. Код: handlers/start.py, services/start_service.py.
/start link_<token> — прив’язка веб-акаунту
Section titled “/start link_<token> — прив’язка веб-акаунту”sequenceDiagram
autonumber
participant API as abitly-api-v2
participant R as Redis (link-токени)
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
API->>R: SET abitly:link:<token> = {web_user_id} (TTL ~600с)
API-->>U: deep-link t.me/abitlybot?start=link_<token>
U->>B: /start link_<token>
B->>R: GETDEL abitly:link:<token>
alt відсутній / прострочений / повторний
R-->>B: nil
B-->>U: «посилання недійсне»
else валідний
R-->>B: {web_user_id}
B->>PG: перевірити users + UPDATE telegram_users.web_user_id
B-->>U: «акаунт під'єднано»
end
GETDEL робить токен строго одноразовим (повтор → nil). Бот — writer telegram_users.web_user_id у цьому напрямку.
Код: services/linking_service.py, handlers/start.py.
/start chances_… — шанси на вступ
Section titled “/start chances_… — шанси на вступ”sequenceDiagram
autonumber
participant Web as Веб-калькулятор (abitly.org)
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
Web-->>U: deep-link chances_<specialityId>_<score×100>
U->>B: /start chances_<spec>_<score>
B->>B: parse_chances_payload (бал у 0..200) + ремап спец-id на 2025
B->>PG: офери спеціальності з прохідними балами (ліміт 10)
PG-->>B: офери
B->>B: build_chance — порівняти бал із cutoff (бюджет / контракт)
alt є відповідні офери
B-->>U: список «куди вступиш» + розрив балів
else порожньо
B-->>U: «нічого не знайдено» + кнопка «🔍 Усі пропозиції»
end
Округлення розриву балів навмисно повторює JS Math.round (half-up) заради паритету з TS-ботом (ADR 0004). Код: services/admission_chances_service.py.
/start promo_blog — промокод −200 грн
Section titled “/start promo_blog — промокод −200 грн”Deep-link із блог-поста «Абітлі промокод». Єдиний сценарій, де бот викликає api-v2 по HTTP (а не через спільну БД): POST /admin/promo-codes/mint з x-admin-key.
sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant API as api-v2
U->>B: /start promo_blog
B->>API: POST /admin/promo-codes/mint {campaign:"blog", telegramUserId}
API-->>B: {code, discountUah:200, used, expiresAt}
alt новий/активний код
B-->>U: особистий код + кнопка «Застосувати знижку» (/uk/simulation?ref=<code>)
else used=true
B-->>U: «код уже використано»
else API недоступний
B-->>U: вибачення + посилання на симуляцію без коду
end
Мінт ідемпотентний per (campaign, telegram_user_id); env бота: ABITLY_API_URL, ABITLY_ADMIN_API_KEY, PROMO_ENABLED, PROMO_LANDING_URL. Деталі механіки знижки — картка фічі. Код: handlers/start.py, services/promo_codes.py.
/start open_day_university_<id> — дні відкритих дверей ЗВО
Section titled “/start open_day_university_<id> — дні відкритих дверей ЗВО”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
U->>B: /start open_day_university_<id>
B->>PG: university + майбутні open_days ЗВО
alt ЗВО не знайдено
B-->>U: «університет не знайдено»
else немає днів
B-->>U: повідомлення + «📬 Підписатися на події ЗВО»
else є дні
B-->>U: список (2/стор.) + пагінація + «📬 Підписатися»
end
U->>B: ⬅️ / ➡️ (uni_events_<id>_<page>)
B->>PG: інша сторінка днів
B-->>U: edit_text — нова сторінка
U->>B: 📬 Підписатися (subscribe_uni_<id>)
Note over B: фільтри ЗВО прибрано (ADR 0008) — без запису в БД
B-->>U: прибрати кнопку + пропозиція керувати у веб-застосунку
Код: handlers/start.py, services/start_service.py, keyboards/inline.py.
Вставлення URL офера — статистика, бал, рейтинг
Section titled “Вставлення URL офера — статистика, бал, рейтинг”Catch-all текстовий хендлер (реєструється останнім). Спершу — логіка маршрутизації тексту (UML activity):
flowchart TD
T["вхідний текст"] --> M{"містить посилання на офер?<br/>abitly.org/uk/offers/result/ID або vstup.edbo.gov.ua/offer/ID"}
M -- ні --> URL{"це взагалі URL?"}
URL -- ні --> IGN["проігнорувати (no-op)"]
URL -- так --> HINT["«посилання недійсне»"]
M -- так --> FOUND{"офер знайдено?"}
FOUND -- ні --> NF["«не знайдено»"]
FOUND -- так --> YEAR{"year == поточний?"}
YEAR -- ні --> IY["«неактуальний рік»"]
YEAR -- так --> OK["статистика + бал/рейтинг + кнопки моніторингу"]
sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres (abitly)
U->>B: вставляє URL офера
B->>B: regex → offer_id
B->>PG: офер (+ коефіцієнти) + аналітика + пул абітурієнтів
opt акаунт прив'язано (web_user_id)
B->>PG: оцінки користувача → бал + рейтинг (чисті функції)
end
B-->>U: статистика офера + «🔔 Додати в моніторинг»
Бал і рейтинг показуються лише для прив’язаного акаунту з оцінками; інакше — загальна статистика офера. Код: handlers/offer_text.py, services/offer_service.py, score_service.py, ranking_service.py.
Моніторинг офера: додати / прибрати
Section titled “Моніторинг офера: додати / прибрати”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
U->>B: 🔔 Додати в моніторинг (add:<offer_id>)
B->>PG: чи вже відстежується?
alt уже відстежується
B-->>U: «вже в моніторингу»
else
B->>PG: лічильник відстежуваних
alt count >= 5 (ліміт)
B-->>U: «досягнуто ліміт (5 оферів)»
else
B->>PG: INSERT telegram_user_monitoring_offers
B-->>U: «додано» + перемальована кнопка «🔕 Прибрати»
end
end
U->>B: 🔕 Прибрати (remove:<offer_id>)
B->>PG: DELETE telegram_user_monitoring_offers
B-->>U: «прибрано» + кнопка «🔔 Додати»
Стан картки офера (UML state)
Section titled “Стан картки офера (UML state)”stateDiagram-v2
state "Не відстежується" as NotTracked
state "Відстежується" as Tracked
[*] --> NotTracked
NotTracked --> Tracked: add (якщо < 5)
NotTracked --> NotTracked: add при ліміті 5 → відмова
Tracked --> NotTracked: remove
Tracked --> Tracked: add → «вже в моніторингу»
Стан зберігається в telegram_user_monitoring_offers (БД), а не у FSM. Код: handlers/offer_callbacks.py.
/myoffers — мої пропозиції
Section titled “/myoffers — мої пропозиції”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
U->>B: /myoffers
B->>PG: список відстежуваних оферів
alt порожньо
B-->>U: «немає відстежуваних оферів»
else
opt акаунт прив'язано
B->>PG: оцінки → рейтинг + прохідність бюджету по кожному оферу
end
B-->>U: список оферів (з рейтингом, якщо прив'язано)
end
Код: handlers/offers.py, services/offer_service.py (build_my_offers).
/myprofile — профіль і бали
Section titled “/myprofile — профіль і бали”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
U->>B: /myprofile (або «👤 Профіль»)
B->>PG: профіль + лічильники подій (майбутні / усього)
B-->>U: статус профілю (+ «🔍 Усі ЗВО», якщо список > 3)
U->>B: 🔍 Усі ЗВО (view_all_universities_profile)
B-->>U: edit_text — повний профіль
Список ЗВО завжди порожній (ADR 0008), тож кнопка «Усі ЗВО» фактично не з’являється; гілка лишена для паритету. Код: handlers/profile.py, services/profile_service.py.
/statistics — статистика позицій
Section titled “/statistics — статистика позицій”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
U->>B: /statistics
B->>PG: к-сть відстежуваних оферів
alt 0 оферів
B-->>U: «спершу додай офери в моніторинг»
else немає прив'язки / оцінок
B-->>U: «потрібні оцінки — прив'яжи акаунт»
else
loop по кожному відстежуваному оферу
B->>PG: бал + пул абітурієнтів → позиція + прохідність бюджету
end
B-->>U: середня / найкраща / найгірша позиція + бюджетні шанси
end
aggregate_positions (середня half-up) — чиста, unit-tested функція. Код: handlers/statistics.py, services/statistics_service.py.
/myopendays — мої дні відкритих дверей
Section titled “/myopendays — мої дні відкритих дверей”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
U->>B: /myopendays (або «📅 Мої події»)
B->>PG: майбутні відстежувані дні (monitoring_open_days)
alt порожньо
B-->>U: «немає майбутніх подій»
else
B-->>U: список (2/стор.) + пагінація
end
U->>B: ⬅️ / ➡️ (my_events_<page>)
B->>PG: інша сторінка
B-->>U: edit_text — нова сторінка
Код: handlers/open_days.py, services/open_day_service.py.
/opendayfilters — фільтри сповіщень
Section titled “/opendayfilters — фільтри сповіщень”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
U->>B: /opendayfilters
B->>PG: фільтри користувача (тільки регіони)
B-->>U: перелік фільтрів + «🌐 Відкрити веб-застосунок»
Керування фільтрами — у веб-застосунку; у боті лише перегляд. Фільтри ЗВО/спеціальностей прибрано (ADR 0008), лишилися регіони. Код: handlers/filters.py, services/filters_service.py.
/notifications — перемикач сповіщень
Section titled “/notifications — перемикач сповіщень”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
participant PG as Postgres
U->>B: /notifications (або 🔔 / 🔕)
B->>B: інвертувати telegram_notifications_enabled
B-->>U: новий статус + перемальоване reply-меню
Note over B,PG: зміна фіксується commit-ом DbSessionMiddleware
Код: handlers/notifications.py.
/help — відоме обмеження (no-op)
Section titled “/help — відоме обмеження (no-op)”sequenceDiagram
autonumber
actor U as Абітурієнт
participant B as Bot
U->>B: /help
Note over B: окремого хендлера немає —<br/>текст «/help» доходить до catch-all (offer_text)
B->>B: не URL → тихо ігнорується
Note over U: відповіді немає (no-op)
Команда є в меню setMyCommands, але без хендлера. Див. «Відомі обмеження» в картці бота.
Адмін: /broadcast + /notifyOpenDaysUpdate
Section titled “Адмін: /broadcast + /notifyOpenDaysUpdate”sequenceDiagram
autonumber
actor A as Адмін (ADMIN_IDS)
participant B as Bot
participant PG as Postgres
participant SND as MessageSender
participant TG as Telegram API
A->>B: /broadcast <текст>
Note over B: фільтр IsAdmin
B->>PG: усі chat_id
B->>SND: add_messages + start (у фоні)
B-->>A: «розсилку запущено для N користувачів»
loop обмежена конкурентність + rate-cap
SND->>TG: send_message
Note over SND: 429 → sleep + re-queue · заблокований → drop+log
end
sequenceDiagram
autonumber
actor A as Адмін
participant B as Bot
participant PG as Postgres
participant SND as MessageSender
A->>B: /notifyOpenDaysUpdate
B->>PG: дні, оновлені сьогодні + користувачі з фільтрами
B->>B: match за регіоном (user_matches_open_day)
B->>SND: повідомлення для кожного збігу → фан-аут
B-->>A: «надіслано: N повідомлень»
Матчинг — лише за регіоном (фільтри ЗВО/спеціальностей прибрано — ADR 0008). Деталі rate-cap/429 — data-flows та ADR 0005. Код: handlers/admin.py, services/notification_service.py, infra/sender.py.
Системний: щоденні нагадування (cron 07:00 Київ)
Section titled “Системний: щоденні нагадування (cron 07:00 Київ)”Не ініціюється користувачем, але саме його повідомлення отримує користувач.
sequenceDiagram
autonumber
participant SCH as APScheduler (07:00 Київ)
participant B as Bot
participant PG as Postgres
participant SND as MessageSender
actor U as Абітурієнт
SCH->>B: тригер щоденної задачі
B->>PG: monitoring-рядки подій у вікні [сьогодні, +3 дні]
B->>B: лишити лише ті, що за 1 або 3 дні (чиста функція)
B->>SND: enqueue нагадувань → фан-аут
SND-->>U: «нагадування про день відкритих дверей»
Код: infra/scheduler.py, services/notification_service.py.
Доменна модель user-сценаріїв (UML class)
Section titled “Доменна модель user-сценаріїв (UML class)”Сутності, яких торкаються сценарії вище. Бот пише лише власну підмножину; решта — read-only (власник схеми — abitly-api-v2).
classDiagram
class TelegramUser {
+bigint telegram_chat_id
+uuid web_user_id
+bool notifications_enabled
}
class Offer {
+int id
+int year
+int speciality_id
}
class OpenDay {
+int id
+datetime date
+int university_id
}
class MonitoringOffer
class MonitoringOpenDay
class BaseUser {
+uuid id
}
class UserGrade {
+int grade
}
TelegramUser "1" --> "0..5" MonitoringOffer : відстежує (write)
MonitoringOffer --> Offer
TelegramUser "1" --> "0..*" MonitoringOpenDay : відвідує (write)
MonitoringOpenDay --> OpenDay
TelegramUser ..> BaseUser : web_user_id (прив'язка, write)
BaseUser "1" --> "0..*" UserGrade : оцінки (read)
Повна ER-схема всіх таблиць — у data-model бота. Власність читання/запису — картка бота та ADR 0006.
Дотичне
Section titled “Дотичне”- Картка сервісу: Abitly Telegram bot
- Інтеграція: Telegram — bot + Mini App
- Крос-сервісні sequence: Потоки даних
- Контейнери: Containers (C4 рівень 2)