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-Tokenfor/bot*paths
- Validates
- Processing
- Emits
Quizitor.UpdateorQuizitor.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
TelegramBotClientWrapperthat publishesSendChatActionto 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.QuestionTimingNotifyandQuizitor.QuestionTimingStopevents - Redis integration for rating caches (short/full, stage/final)
- Runs EF Core migrations and applies seeds (roles, bot commands, test data)
Game→Round→QuestionQuestionOptions: available choices withCostRules: scoring modifiers, e.g. AnyAnswer, FirstAcceptedAnswerTime, optionalNotificationTime,Attempts,SubmissionNotificationTypeTimings:QuestionTimingwith generated notify/stop events
Session: concrete instance of a game runSubmission: participant answer with computedScoreandTimeTeam,TeamMember,TeamLeaderUser,Role,RolePermission,UserPermission,UserRole,UserPromptBot(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.UpdateQuizitor.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.QuestionTimingNotifyQuizitor.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/Migrationsand are applied byQuizitor.Migrator ApplicationDbContextregisters 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:
/metricsin each service (histograms for webhooks handling, update processing, sender methods, overall E2E) - Health probes:
/health - Sentry: enabled when
SENTRY_DSNis 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/ruresources)
PORT: default8080DOMAIN: public base domain used to set Telegram webhooks, e.g.,your-domain.exampleor ngrok domainPATH_BASE: optional path baseDB_CONNECTION_STRINGTELEGRAM_BOT_TOKEN: backoffice bot tokenTELEGRAM_WEBHOOK_SECRET: shared secret for webhook validation
PORT: requiredDB_CONNECTION_STRINGTELEGRAM_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/quizitorCRYPTO_PASSWORD: symmetric key to decrypt QR payloadsQR_CODE_EXPIRATION_SECONDS: default0(no expiration)KAFKA_CONSUMER_GROUP_ID: defaultQuizitor.BotsREDIS_CONNECTION_STRING,REDIS_KEY_PREFIX
PORT: requiredDB_CONNECTION_STRINGTELEGRAM_BOT_TOKEN: backoffice token for default channelWORKING_DIRECTORY: default/var/quizitorKAFKA_CONSUMER_GROUP_ID: e.g.,Quizitor.Sender
PORT: requiredDB_CONNECTION_STRINGREDIS_CONNECTION_STRING,REDIS_KEY_PREFIX
DB_CONNECTION_STRINGLOCALE
- 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.localfor 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 runEnsure Kafka, PostgreSQL, and Redis are reachable.
- Telegram posts Update to
POST /botorPOST /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
Optionplus contributions fromRuleappliers - Admin notification policy per
QuestionviaSubmissionNotificationType
- Tests reside under
tests/* - Dockerfile runs
dotnet testduring 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.jsonvalues are for local development only