A Telegram bot for managing support tickets using forum threads. Each ticket is created as a separate topic in a Telegram forum (supergroup with topics enabled), allowing organized support conversations.
- Thread-based Tickets: Each ticket is a separate forum topic for organized conversations
- Admin-only Access: Only configured admins can manage and respond to tickets
- Ticket Management: Open, close, and reopen tickets with ease
- Priority System: Set ticket priorities (low, normal, high, urgent)
- User Ban System: Ban abusive users from creating tickets
- Internal Notes: Add private notes visible only to admins
- Media Support: Forward photos, documents, videos, voice messages, and more
- Canned Responses: Create and send predefined responses for common questions
- Language Selection: English and Russian user flows
- Ticket Stats: View ticket activity summaries from admin commands
- Automatic Ticket Welcome: Send a configurable first support reply on new tickets
- Business Hours Notice: Optionally warn users when a ticket is opened outside support hours
- Onboarding Captcha: Require a verification step on first bot launch, with generated or custom questions
- HWID Device Flow: Detect HWID/device-limit questions, auto-escalate the ticket, and let users remove panel devices with a cooldown
- Web Admin Panel: React + TypeScript admin site for ticket triage, replies, notes, canned responses, bans, and auth management
The repository includes a GitHub Actions workflow at .github/workflows/docker-image-tar.yml that builds two Docker image tarballs:
support-ticket-bot-<VERSION>-linux-amd64.tarsupport-ticket-bot-<VERSION>-linux-arm64.tar
- Create and push a Git tag such as
1.2.3:git tag 1.2.3 git push origin 1.2.3
- Wait for the
Build Docker Image Tarworkflow to finish. - Download the tarball that matches the target server architecture from the workflow artifacts or the GitHub Release assets.
- Load the image on the target server:
For ARM64 servers, use:
docker load -i support-ticket-bot-1.2.3-linux-amd64.tar
docker load -i support-ticket-bot-1.2.3-linux-arm64.tar
- Set
BOT_VERSION=1.2.3in.env. - Start or update the stack:
docker compose up -d
The Compose file bind-mounts local config.yaml into the container as /app/config.yaml, so config edits do not require rebuilding the image. After changing config.yaml, restart the bot container:
docker compose restart support-ticket-botThe workflow creates a GitHub Release named <VERSION>, attaches the .tar file and checksum, and uses GitHub's standard Generate release notes output for the release description.
The loaded image tag will be support-ticket-bot:<VERSION>, and docker-compose.yml uses BOT_VERSION to select that exact image version.
Minimum .env example for the image version:
BOT_VERSION=1.2.3The repository also includes .github/workflows/release-to-seaweedfs.yml. It runs on every published GitHub Release, downloads the release .tar assets, and uploads them to SeaweedFS through its S3-compatible endpoint.
Required repository secrets:
SEAWEEDFS_S3_ENDPOINTSEAWEEDFS_S3_BUCKETSEAWEEDFS_S3_ACCESS_KEY_IDSEAWEEDFS_S3_SECRET_ACCESS_KEY
Optional repository secrets:
SEAWEEDFS_S3_REGION(defaults tous-east-1when empty)SEAWEEDFS_S3_PREFIX(for exampletelegram-support-bot/releases)
Uploaded object path format:
s3://<bucket>/<prefix>/<version>/<filename>.tar
The repository now includes a built-in admin panel under web/admin-ui and a Go HTTP server under internal/web.
For local source-based runs outside Docker, build the frontend once before starting the bot:
cd web/admin-ui
npm install
npm run build- Dashboard summary for ticket activity
- Ticket list with open/all/closed filters
- Ticket detail view with replies, priority changes, close/reopen, notes, and HTML export
- Canned responses management
- Banned users management
- Authentication and passkey management
Only these methods are enabled in the codebase:
- Password
- Passkey
- Telegram Login Widget
- GitHub OAuth
No generic OAuth, Keycloak, Pocket ID, or Yandex auth is included.
WEB_ADMIN_ENABLED=trueWEB_ADMIN_LISTEN_ADDR=:8080WEB_ADMIN_PUBLIC_URL=https://support.example.com# Optional. Defaults to the path part of WEB_ADMIN_PUBLIC_URL, or / when there is none.# WEB_ADMIN_BASE_PATH=/WEB_ADMIN_SESSION_COOKIE_NAME=support_admin_sessionWEB_ADMIN_SESSION_TTL_HOURS=168WEB_AUTH_PASSWORD_ENABLED=trueWEB_AUTH_PASSWORD_USERS=admin:$2a$10$replace_with_bcrypt_hashWEB_AUTH_PASSKEY_ENABLED=trueWEB_AUTH_PASSKEY_DISPLAY_NAME=Support Ticket Bot AdminWEB_AUTH_GITHUB_ENABLED=trueWEB_AUTH_GITHUB_CLIENT_ID=your_github_client_idWEB_AUTH_GITHUB_CLIENT_SECRET=your_github_client_secretWEB_AUTH_GITHUB_ALLOWED_EMAILS=ops@example.comWEB_AUTH_GITHUB_ALLOWED_LOGINS=your-github-loginWEB_AUTH_TELEGRAM_ENABLED=trueWEB_AUTH_TELEGRAM_ALLOWED_IDS=123456789,987654321VALKEY_ADDR=/run/support-ticket-valkey/valkey.sockVALKEY_PASSWORD=VALKEY_DB=0Notes:
WEB_ADMIN_PUBLIC_URLshould be the public admin URL that users actually open in the browser.WEB_ADMIN_BASE_PATHis optional. If you omit it, the app uses the path fromWEB_ADMIN_PUBLIC_URL; if the URL has no path, the admin UI is served from/.- Passkeys derive RP ID and allowed origin from
WEB_ADMIN_PUBLIC_URL. - GitHub auth requires at least one allowlist entry in
WEB_AUTH_GITHUB_ALLOWED_EMAILSorWEB_AUTH_GITHUB_ALLOWED_LOGINS. - If
WEB_AUTH_TELEGRAM_ALLOWED_IDSis empty,ADMIN_IDSis used automatically. - Password auth expects bcrypt hashes in
WEB_AUTH_PASSWORD_USERS.
The bot container listens on :8080 for the web admin. In Docker Compose the service is reachable on the shared Docker network as http://support-ticket-bot:8080.
Always proxy /api/admin/ to the bot container.
For the frontend path:
- proxy
/whenWEB_ADMIN_PUBLIC_URLis on the domain root orWEB_ADMIN_BASE_PATH=/ - proxy
/admin/whenWEB_ADMIN_PUBLIC_URLuses/admin
If SWAG runs in a different Compose stack, attach it to the same Docker network first. This repository now creates that network with the fixed name support-ticket-bot, so SWAG can join it as an external network:
networks:
support-ticket-bot:
external: trueThen add that network to the SWAG service and use proxy_pass http://support-ticket-bot:8080;.
Example Nginx location blocks for root deployment:
location / {proxy_passhttp://support-ticket-bot:8080;proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;}location /api/admin/ {proxy_passhttp://support-ticket-bot:8080;proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;}Example Nginx location blocks for /admin deployment:
location /admin/ {proxy_passhttp://support-ticket-bot:8080;proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;}location /api/admin/ {proxy_passhttp://support-ticket-bot:8080;proxy_set_header Host $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;}The Docker Compose stack now always starts a bundled Valkey container. It is used for:
- web admin sessions
- GitHub OAuth state
- passkey registration/login ceremony state
Recommended VALKEY_ADDR inside Docker is the shared UNIX socket path:
VALKEY_ADDR=/run/support-ticket-valkey/valkey.sockThe bundled Valkey service also keeps TCP on support-ticket-valkey:6379, so existing configs continue to work. If VALKEY_ADDR is empty, the bot still falls back to an in-memory store.
| Command | Description |
|---|---|
/start | Start the bot and see welcome message |
/newticket | Create a new support ticket |
/mytickets | View your tickets |
/cancel | Cancel current operation |
/language | Change your language |
| Command | Description |
|---|---|
/help | Show admin help |
/tickets | List all open tickets |
/close | Close current ticket (in ticket thread) |
/reopen | Reopen closed ticket (in ticket thread) |
/priority [level] | Set ticket priority (low/normal/high/urgent) |
/info | View ticket details |
/ban [user_id] [reason] | Ban user from creating tickets |
/unban [user_id] | Unban user |
/banned | List all banned users |
/note [text] | Add internal note (admin-only) |
/notes | View ticket notes |
/stats [days] | View ticket statistics |
/canned | List canned responses |
/addcanned [shortcut] [title] | [content] | Add a canned response |
/delcanned [shortcut] | Delete a canned response |
/[shortcut] | Send a canned response inside a ticket thread |
bot:
log_level: infotickets:
auto_close_hours: 0# Auto-close after X hours (0 = disabled)max_open_per_user: 3# Maximum open tickets per usernumber_prefix: "TKT"# Ticket number prefixpriority_enabled: truedefault_priority: normalcaptcha:
enabled: truetype: custom # math, button, text, customtimeout_seconds: 120questions:
- question: "How much is 2 + 2?"correct_answer: "4"options: ["3", "4", "5", "6"]
- question: "Tap the word Support"correct_answer: "Support"options: ["Ticket", "Support", "Panel", "Hello"]hwid:
enabled: truedelete_cooldown_hours: 24trigger_keywords:
- "hwid"
- "хвид"
- "удалить устройство"
- "лимит устройств"
- "delete device"
- "device limit"business_hours:
enabled: truetimezone: "Europe/Moscow"start: "10:00"end: "19:00"messages:
welcome: | 👋 Welcome to Support! Send me a message to create a ticket.ticket_created: | ✅ Ticket #{ticket_number} created!ticket_welcome: | 👋 <b>Hello!</b> Your ticket #{ticket_number} is now open. You can send any extra details in this chat at any time.ticket_welcome_ru: | 👋 <b>Здравствуйте!</b> Ваш тикет #{ticket_number} открыт. Вы можете отправить дополнительные детали в этом чате в любое время.ticket_out_of_hours: | 🌙 <b>We are currently outside business hours.</b> We received your ticket #{ticket_number} and will reply during working hours. <b>Support hours:</b> <code>{working_hours}</code>.ticket_out_of_hours_ru: | 🌙 <b>Сейчас нерабочее время.</b> Мы получили ваш тикет #{ticket_number} и ответим в рабочие часы. <b>Часы поддержки:</b> <code>{working_hours}</code>.ticket_closed: | 🔒 Ticket #{ticket_number} has been closed.Optional .env values for panel integration:
REMNAWAVE_URL=https://panel.example.comREMNAWAVE_TOKEN=your_remnawave_token_hereREMNAWAVE_MODE=remoteREMNAWAVE_HEADERS=The bot uses golang-migrate for database schema management. Migrations run automatically on startup.
Located in internal/database/migrations/:
000001_initial_schema.up.sql # Creates tables
000001_initial_schema.down.sql # Drops tables (rollback)
000002_sync_tickets_id_sequence.up.sql
000002_sync_tickets_id_sequence.down.sql
000003_add_hwid_delete_tracking.up.sql
000003_add_hwid_delete_tracking.down.sql
000004_add_user_onboarding.up.sql
000004_add_user_onboarding.down.sql
Create a new migration file pair:
000002_add_feature.up.sql 000002_add_feature.down.sqlWrite your SQL changes in the
.up.sqlfileWrite the rollback in the
.down.sqlfileRestart the bot - migrations run automatically
If you need to manage migrations manually:
# Install migrate CLI
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Run migrations up
migrate -path internal/database/migrations -database "postgres://user:pass@localhost:5432/dbname?sslmode=disable" up
# Rollback one migration
migrate -path internal/database/migrations -database "postgres://user:pass@localhost:5432/dbname?sslmode=disable" down 1
# Check current version
migrate -path internal/database/migrations -database "postgres://user:pass@localhost:5432/dbname?sslmode=disable" version