🇬🇧 English | 🇷🇺 Русский
The bot can:
- list groups and their members (
/groups); - list all users with pagination and a per-user card — their groups, add/remove group membership (
/users); - create groups (
/newgroup Name); - add a user to a group (search by username/email via buttons, or straight from the user card);
- remove a user from a group, delete a group;
- enable/disable a user (user card → status button);
- notify you in Telegram about new users and/or every login to Keycloak (checked every
POLL_INTERVAL_SECONDS, default 60s) — both notifications toggle independently via the/settingscommand, with the container-startup default set byNOTIFY_NEW_USERS/NOTIFY_LOGINS.
Access is restricted to the Telegram IDs listed in ADMIN_CHAT_IDS — everyone else is silently ignored.
The bot talks to Keycloak via the Admin REST API as a dedicated client (client credentials grant), not under your personal login.
- Open the admin console:
https://<keycloak-host>/admin/master/console/#/<realm>. - Clients → Create client:
- Client ID:
telegram-bot(or anything else, just put it in.envlater); - Client type:
OpenID Connect, click Next.
- Client ID:
- On the Capability config step:
Client authentication→ On (makes the client confidential and enables the service account);Authorization— can stay off;Authentication flow: enable Service accounts roles, the others (Standard flow, Direct access) can be disabled.
- Save the client.
- Open the Credentials tab of the new client — copy the
Client secret, it goes intoKEYCLOAK_CLIENT_SECRET. - Open Service account roles → Assign role:
- switch to
Filter by clients; - find the
realm-managementclient and assign these roles:manage-users— creating/removing users from groups, enabling/disabling a user;query-users— searching and listing users;query-groupsandmanage-realm(or at leastview-realm) — working with groups;view-events— reading login events (only needed forNOTIFY_LOGINS).
- If unsure which role to pick, you can temporarily grant
realm-admin(fullrealm-managementrights) and narrow it down later.
- switch to
Result: you now have Client ID = telegram-bot, Client secret = a long string, realm = <realm>, url = https://<keycloak-host>.
Keycloak doesn't store login events by default — the Admin REST API will just return an empty list, even with the view-events role granted. For login notifications to have anything to work with:
- In the admin console: Realm settings → Events → User events settings.
- Turn on Save events.
- Add (or make sure it's there)
LOGINunder Saved event types. - Pay attention to Expiration — Keycloak will accumulate these events in its database; set a sane TTL (e.g. 1-2 weeks) so old events get cleaned up and don't bloat the database.
This changes realm-wide behavior (events get logged for every client, not just the bot), so enable it deliberately — it's not the same as just configuring the bot itself.
- In Telegram, message @BotFather →
/newbotand follow the prompts. - You'll get a token like
123456789:AA...— that'sTELEGRAM_BOT_TOKEN. - Find your Telegram ID: message @userinfobot — it replies with your numeric ID. That goes into
ADMIN_CHAT_IDS(comma-separate several IDs if there's more than one admin).
cp .env.example .envFill in every field: TELEGRAM_BOT_TOKEN, ADMIN_CHAT_IDS, KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET.
NOTIFY_NEW_USERS / NOTIFY_LOGINS — the default notification state at container startup; flip them live with the bot's /settings command, no restart needed. PERSIST_SETTINGS decides whether a /settings toggle survives a container restart:
false(default) — after a restart the state falls back toNOTIFY_NEW_USERS/NOTIFY_LOGINS. Keep this ifdata/isn't mounted as persistent storage (e.g. Kubernetes without a PVC) — otherwise the toggle would appear to "save" but silently get lost on every pod recreation.true— a/settingstoggle is written todata/settings.jsonand survives a restart. Only enable this ifdata/is a real persistent volume/bind mount.
docker compose up -d --build
docker compose logs -fWhich users the bot has already seen (so it doesn't spam notifications on first launch) is tracked in ./data/seen_users.json; the timestamp of the last processed login event lives in ./data/last_login_event.json. On first startup the bot takes a "snapshot" of the current state and only notifies about what appears after that point. If PERSIST_SETTINGS=true, /settings toggles are also written there (./data/settings.json).
If the source only lives locally (not in git) and you want to deploy to a Docker host managed by Portainer:
- Build the image directly on the Docker engine Portainer manages, via the Docker Build API (which Portainer proxies):
tar -cf build-context.tar Dockerfile requirements.txt bot curl -sk -X POST \ -H "X-API-Key: <PORTAINER_API_TOKEN>" \ -H "Content-Type: application/x-tar" \ --data-binary @build-context.tar \ "https://<portainer-host>:9443/api/endpoints/<ENDPOINT_ID>/docker/build?t=keycloak-telegram-bot:latest"
ENDPOINT_IDis the id of the target Docker environment, found viaGET /api/endpoints. - Deploy the
docker-compose.portainer.ymlstack (it uses the already-builtimage:instead ofbuild:, and secrets are passed as environment variables rather than an.envfile — Portainer doesn't copy that to the host):wherecurl -sk -X POST \ -H "X-API-Key: <PORTAINER_API_TOKEN>" \ -H "Content-Type: application/json" \ -d @stack-payload.json \ "https://<portainer-host>:9443/api/stacks?type=2&method=string&endpointId=<ENDPOINT_ID>"
stack-payload.jsoncontains{"Name": "...", "StackFileContent": "<contents of docker-compose.portainer.yml>", "Env": [{"name": "TELEGRAM_BOT_TOKEN", "value": "..."}, ...]}.
When the code changes: rebuild the image with step 1 (overwrites the latest tag), then do an Update/Redeploy of the stack in Portainer so it picks up the new image.
python -m venv .venv
source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -r requirements.txt
cp .env.example .env # and fill it in
python -m bot.mainbot/
config.py — reads settings from .env
keycloak_client.py — async wrapper around the Keycloak Admin REST API
storage.py — file-backed stores for "already seen" user IDs and the last login event
settings.py — notification toggles (env default + optional persistence)
callback_cache.py — short tokens for Telegram inline buttons
middleware.py — restricts access to ADMIN_CHAT_IDS
handlers.py — commands and inline menus (/groups, /users, /settings, /newgroup, ...)
notifier.py — background polling of Keycloak and notifications about new users/logins
main.py — entry point
- Only top-level groups are supported (no nested subgroups).
- Notifications about new users and logins are polling-based, not instant on event; delay up to
POLL_INTERVAL_SECONDS. Instant notifications would need a custom Keycloak Event Listener SPI on the server itself — a separate task if it's ever needed. - Login polling fetches the last 200 events per cycle; if more than 200 logins happen between polls, some notifications could be skipped (unlikely for a personal/small realm).