Telegram-first quiz/game orchestration platform. Quizitor ingests Telegram updates via webhooks, processes them through a behavior engine, persists state in PostgreSQL, schedules quiz events, and delivers outbound Telegram actions asynchronously.
- BackOffice bot to manage games, rounds, questions, sessions, users, roles, and mailings
- GameAdmin bot to conduct sessions: start/stop/time questions, notify participants, review submissions
- GameServer bot to interact with participants, collect answers, apply rules, and score
- Pluggable behavior engine with permissions and localization
- Reliable async I/O via Kafka for all inbound updates and outbound Telegram API calls
- Timed events (notify/auto-stop) and rating calculations
- Observability: Prometheus metrics (/metrics) and Sentry error reporting
Updates and actions are decoupled with Kafka to isolate Telegram API latency and improve reliability.
sequenceDiagram
participant TG as Telegram
participant API as Quizitor.Api
participant K as Kafka
participant B as Quizitor.Bots
participant S as Quizitor.Sender
TG->>API: POST /bot(/:botId) (Update)
API->>K: Produce Quizitor.Update(.{botId})
B->>K: Consume Update(.{botId})
B->>B: Identify + Route to Behavior + DB/Redis
B->>K: Produce Send* topics (SendMessage, EditMessage, ...)
S->>K: Consume Send* topics
S->>TG: Call Telegram HTTP API
Question timing flow (notify/auto-stop):
sequenceDiagram
participant GA as GameAdmin
participant B as Bots (GameAdmin behavior)
participant DB as PostgreSQL
participant EV as Events
participant K as Kafka
GA->>B: Start Question
B->>DB: Create QuestionTiming + Notify/Stop events
EV->>DB: Poll candidate events
EV->>K: Produce Quizitor.QuestionTimingNotify / Stop
B->>K: Consume timing events
B->>TG: Broadcast/Notify via Send* through Sender
src/Quizitor.Api
: Ingress API for Telegram webhooks and lightweight operationssrc/Quizitor.Bots
: Behavior engine and business logic (BackOffice, GameAdmin, GameServer, LoadBalancer)src/Quizitor.Sender
: Outbound Telegram HTTP sender (consumes Send* topics)src/Quizitor.Events
: Timed event processors and rating jobssrc/Quizitor.Migrator
: EF Core migrations/seeds executorlib/Quizitor.*
: Shared libraries (Common, Data, Kafka, Redis, Logging, Localization)tests/*
: Unit/integration tests
- Endpoints
POST /bot
: backoffice/default bot webhookPOST /bot/{botId:int}
: per-bot webhookGET /metrics
: PrometheusGET /health
: health probe
- Security
- Validates
X-Telegram-Bot-Api-Secret-Token
for/bot*
paths
- Validates
- Processing
- Emits
Quizitor.Update
orQuizitor.Update.{botId}
withUpdateContext
(includes optionalInitiatedAt
,IsTest
) - Configures webhooks for all active bots on startup/shutdown (sets bot commands, updates usernames)
- Emits
- Sends-only operations
- Provides a minimal
TelegramBotClientWrapper
that publishesSendChatAction
to Kafka (other send operations are handled by Bots/Sender)
- Provides a minimal
- Consumes
Quizitor.Update(.{botId})
and timing topics - Identifies users, decrypts QR if present, builds
IBehaviorContext
- Routes to behaviors using traits:
- Message text, bot commands, callback-query data (equals/prefix), QR data prefix
- Enforces permissions; sends localized unauthorized responses when needed
- Major behavior groups
- BackOffice: manage bots, users/roles, games, rounds/questions (options, rules), sessions, mailings
- GameAdmin: start/stop/time questions, team/session ops, rating views
- GameServer: participant interactions and submissions pipeline
- LoadBalancer: auxiliary routing (if configured)
- Outbound I/O
- Publishes Send* requests (SendMessage, EditMessage, DeleteMessage, AnswerCallbackQuery, SendPhoto, SendChatAction) to Kafka
- Consumes Send* topics
- Executes Telegram HTTP requests with per-bot named
HttpClient
- Records method-level histograms and E2E timing from
UpdateContext.InitiatedAt
- Background loops poll DB for candidate items
- Produces
Quizitor.QuestionTimingNotify
andQuizitor.QuestionTimingStop
events - Redis integration for rating caches (short/full, stage/final)
- Runs EF Core migrations and applies seeds (roles, bot commands, test data)
Game
→Round
→Question
Question
Options
: available choices withCost
Rules
: scoring modifiers, e.g. AnyAnswer, FirstAcceptedAnswerTime
, optionalNotificationTime
,Attempts
,SubmissionNotificationType
Timings
:QuestionTiming
with generated notify/stop events
Session
: concrete instance of a game runSubmission
: participant answer with computedScore
andTime
Team
,TeamMember
,TeamLeader
User
,Role
,RolePermission
,UserPermission
,UserRole
,UserPrompt
Bot
(type: BackOffice/GameAdmin/GameServer/LoadBalancer/Universal)Mailing
,MailingProfile
, and Filters (by Bot/BotType/Game/Session/Team/User)
Topic naming uses Quizitor.
prefix (see lib/Quizitor.Kafka/KafkaTopics.cs
).
- Updates
Quizitor.Update
Quizitor.Update.{botId}
- Sender (outbound Telegram)
Quizitor.SendChatAction(.{botId})
Quizitor.SendMessage(.{botId})
Quizitor.SendPhoto(.{botId})
Quizitor.EditMessage(.{botId})
Quizitor.AnswerCallbackQuery(.{botId})
Quizitor.DeleteMessage(.{botId})
- Timings
Quizitor.QuestionTimingNotify
Quizitor.QuestionTimingStop
Consumers ensure topics exist at startup and isolate per-bot streams with dedicated consumer groups.
- PostgreSQL via EF Core (Npgsql)
- Migrations are in
src/Quizitor.Migrator/Migrations
and are applied byQuizitor.Migrator
ApplicationDbContext
registers repositories underlib/Quizitor.Data
- Migrations are in
- Redis (StackExchange.Redis)
- Rating caches with serializers and typed storages under
lib/Quizitor.Redis
- Rating caches with serializers and typed storages under
- Prometheus:
/metrics
in each service (histograms for webhooks handling, update processing, sender methods, overall E2E) - Health probes:
/health
- Sentry: enabled when
SENTRY_DSN
is set; otherwise logs to console
All configuration is via environment variables. Below are commonly used settings per service.
DB_CONNECTION_STRING
: PostgreSQL connection string (required by all services)KAFKA_BOOTSTRAP_SERVERS
: Kafka bootstrap servers (e.g.,localhost:9092
)KAFKA_DEFAULT_NUM_PARTITIONS
: default1
(applied by all consumers when autocreating topics)KAFKA_DEFAULT_REPLICATION_FACTOR
: default1
(applied by all consumers when autocreating topics)SENTRY_DSN
: Optional Sentry DSNLOCALE
: Defaulten
(supportsen
/ru
resources)
PORT
: default8080
DOMAIN
: public base domain used to set Telegram webhooks, e.g.,your-domain.example
or ngrok domainPATH_BASE
: optional path baseDB_CONNECTION_STRING
TELEGRAM_BOT_TOKEN
: backoffice bot tokenTELEGRAM_WEBHOOK_SECRET
: shared secret for webhook validation
PORT
: requiredDB_CONNECTION_STRING
TELEGRAM_BOT_TOKEN
: backoffice token for outbound defaultsAUTHORIZED_USER_IDS
: list of allowed super-admin IDs (comma/space separated). Set*
to disable SA gatingWORKING_DIRECTORY
: default/var/quizitor
CRYPTO_PASSWORD
: symmetric key to decrypt QR payloadsQR_CODE_EXPIRATION_SECONDS
: default0
(no expiration)KAFKA_CONSUMER_GROUP_ID
: defaultQuizitor.Bots
REDIS_CONNECTION_STRING
,REDIS_KEY_PREFIX
PORT
: requiredDB_CONNECTION_STRING
TELEGRAM_BOT_TOKEN
: backoffice token for default channelWORKING_DIRECTORY
: default/var/quizitor
KAFKA_CONSUMER_GROUP_ID
: e.g.,Quizitor.Sender
PORT
: requiredDB_CONNECTION_STRING
REDIS_CONNECTION_STRING
,REDIS_KEY_PREFIX
DB_CONNECTION_STRING
LOCALE
- Docker (and Docker Compose)
- .NET SDK 9.0
- Start Kafka only (optional, if not using the full stack):
docker compose -f docker-compose.kafka.yaml up -d
- Prepare env files in
env/*/.env.local
for each service (see Configuration section for required variables). Example vars:
# env/api/.env.local
PORT=80
DOMAIN=<your-public-domain>
DB_CONNECTION_STRING=Host=postgres;Port=5432;Database=quizitor;Username=postgres;Password=postgres
TELEGRAM_BOT_TOKEN=<bot-token>
TELEGRAM_WEBHOOK_SECRET=<random-secret>
KAFKA_BOOTSTRAP_SERVERS=kafka:9092
- Bring up the full stack:
docker compose -f docker-compose.local.yaml up --build
- Access Kafka UI at
http://localhost:9090
.
Tip: For local webhook testing, expose Quizitor.Api
publicly (e.g., ngrok) and set DOMAIN
accordingly.
From each project directory, set env vars and run:
dotnet run
Ensure Kafka, PostgreSQL, and Redis are reachable.
- Telegram posts Update to
POST /bot
orPOST /bot/{botId}
- Api validates secret, enriches context with
InitiatedAt
, producesQuizitor.Update(.{botId})
- Bots consumer parses
UpdateContext
, resolves identity, picks behavior(s), applies DB/Redis operations - Bots publish Send* requests for any Telegram I/O
- Sender consumes and calls Telegram HTTP API
- Sender records E2E histogram from
InitiatedAt
- GameAdmin triggers "Start Question"
- Bots create
QuestionTiming
, enqueue Notify/Stop events - Events produce timing Kafka messages when due
- Bots consume timing messages, notify participants/admins or auto-close, then optionally prompt next steps
- Submissions receive base cost from chosen
Option
plus contributions fromRule
appliers - Admin notification policy per
Question
viaSubmissionNotificationType
- Tests reside under
tests/*
- Dockerfile runs
dotnet test
during image build
- Metrics prefix:
quizitor_
- All Kafka topics auto-created by consumers with configured partitions/replication
- Localization via
TR.LPlus
; resource files live undersrc/*/Localization
- Avoid committing real secrets;
launchSettings.json
values are for local development only