Telegram Plugin
Connects Codumentor to a Telegram bot so people can talk to the agent from their phone or desktop — including voice:
- Text in → text out — a text message starts an agent turn; the reply is posted back to the chat.
- Voice in → voice out — a voice message is transcribed, passed to the agent as a voice input, and the reply is synthesized and sent as a Telegram voice note. The full text is also sent so nothing is lost.
Speech-to-text and text-to-speech come from the shared Speech service (config.speech), which uses OpenAI-compatible audio APIs.
The plugin long-polls Telegram (getUpdates) — no public URL or webhook is required.
Two modes
Chosen by mode:
| Mode | Bot | Turns run as | Token from | Best for |
|---|---|---|---|---|
technical_user | one shared bot | a single service user | telegram_bot_token_global user secret (stored under the service user), or the bot_token_env env var as fallback | a team/instance bot |
per_user | one bot per user | the bot's owner | each user's telegram_bot_token user secret | everyone brings their own private bot |
Both modes resolve the bot token through User Secrets. The Settings → Extensions → User Secrets pane lists the secret as needed by Telegram. technical_user also accepts the legacy bot_token_env environment variable when the secret is unset.
Load user_secrets alongside this plugin. The env-var fallback for technical_user still works if user_secrets is absent.
Quick start (technical_user)
# codumentor.yaml
speech:
enabled: true
api_key: "${OPENAI_API_KEY}" # voice; omit to run text-only
plugins:
- module: codumentor.plugins.telegram
class: TelegramPlugin
args:
enabled: true
mode: technical_user
service_user_id: telegram-bot # the shared token is this user's secret
allowed_user_ids: [123456789] # who may talk to the bot (your Telegram id)
Provide the bot token one of two ways:
- Preferred — User Secrets: sign in as the service account (
service_user_id, defaulttelegram-bot) and add a secret namedtelegram_bot_token_globalwith the bot token (Settings → Extensions → User Secrets). The pane lists it as needed by Telegram. - Fallback — environment variable: export the
bot_token_envvar (defaultTELEGRAM_BOT_TOKEN). Used only when the secret is unset.
export TELEGRAM_BOT_TOKEN="123456789:AA..." # only if not using the secret
export OPENAI_API_KEY="sk-..." # only needed for voice
If neither the telegram_bot_token_global secret nor bot_token_env resolves, the plugin loads in degraded mode — no poller, no crash — and logs a warning.
With allow_all: false (the default) and an empty allowed_user_ids, the bot rejects everyone. Add your numeric Telegram id (ask @userinfobot) before the bot will respond.
Configuration — technical_user mode
plugins:
- module: codumentor.plugins.telegram
class: TelegramPlugin
priority: 50
args:
enabled: true
mode: technical_user
global_token_secret_name: telegram_bot_token_global # user-secret holding the shared token
bot_token_env: TELEGRAM_BOT_TOKEN # env-var fallback when the secret is unset
service_user_id: telegram-bot # Codumentor user that owns these conversations (and the token secret)
allow_all: false
allowed_user_ids: [123456789, 987654321]
permission_policy: auto_allow
target_agents: main
voice_reply: true # honor voice-in → voice-out
long_poll_timeout: 50
request_timeout: 70.0
Configuration — per_user mode
Each Codumentor user registers their own bot token. The system polls every configured bot and runs turns as the owning user (their secrets, their repo context). No global token is needed.
plugins:
- module: codumentor.plugins.telegram
class: TelegramPlugin
priority: 50
args:
enabled: true
mode: per_user
token_secret_name: telegram_bot_token # user-secret each user stores their token under
per_user_authz: tofu # tofu | allowlist
per_user_allowed_ids_secret: telegram_allowed_ids # only for allowlist authz
bot_discovery: periodic # periodic | startup
reconcile_interval: 60 # seconds between rescans (periodic)
voice_reply: true
See Per-user private bot setup for the end-user steps.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Master on/off switch. |
mode | string | technical_user | technical_user (one shared bot) or per_user (one bot per user). |
global_token_secret_name | string | telegram_bot_token_global | technical_user: name of the user secret (stored under service_user_id) holding the shared bot token. Preferred over bot_token_env. |
bot_token_env | string | TELEGRAM_BOT_TOKEN | technical_user: env var holding the bot token, used only as a fallback when the secret is unset. Only the env-var name is recorded, never the token. |
service_user_id | string | telegram-bot | technical_user: Codumentor user that owns Telegram-originated conversations and stores the shared bot-token secret. With auth.provider: none, any string is accepted. |
allow_all | bool | false | technical_user: if true, anyone who finds the bot may use it. Leave false and use allowed_user_ids. |
allowed_user_ids | list[int] | [] | technical_user: Telegram numeric user-ids allowed to use the bot. Empty + allow_all: false ⇒ nobody is allowed. |
token_secret_name | string | telegram_bot_token | per_user: user-secret name each user stores their bot token under. |
per_user_authz | string | tofu | per_user: who may use a user's bot — tofu or allowlist (see Authorization). |
per_user_allowed_ids_secret | string | telegram_allowed_ids | per_user/allowlist: user-secret holding the owner's allowed Telegram ids (comma/space separated). |
bot_discovery | string | periodic | per_user: periodic rescans for added/removed bots every reconcile_interval; startup discovers once. |
reconcile_interval | int | 60 | per_user/periodic: seconds between rescans. |
permission_policy | string | auto_allow | Tool-permission policy for turns from this channel. Only auto_allow is available: tool prompts are auto-allowed (global deny rules still apply), and a turn stuck on an approval is auto-approved and resumed. |
target_agents | string | main | Which agent types this applies to: main, subagent, all. |
voice_reply | bool | true | Honor voice-in → voice-out. Requires config.speech enabled; otherwise replies stay text. |
long_poll_timeout | int | 50 | getUpdates long-poll timeout (seconds). |
request_timeout | float | 70.0 | HTTP timeout (seconds); must exceed long_poll_timeout. |
Speech service (STT / TTS)
Voice is a system-level service under the top-level speech: key. It is not Telegram-specific — the same block powers Voice Mode and Read Aloud.
speech:
enabled: true
base_url: "https://api.openai.com/v1" # OpenAI-compatible audio endpoint
api_key: "${OPENAI_API_KEY}" # ${ENV_VAR} or ${secret:<name>}
stt_model: "whisper-1" # speech-to-text model
stt_language: null # null = auto-detect
tts_model: "tts-1" # text-to-speech model
tts_voice: "alloy" # alloy | echo | fable | onyx | nova | shimmer
tts_format: "opus" # opus → native Telegram voice note
tts_speed: 1.0
| Parameter | Default | Description |
|---|---|---|
enabled | false | Master switch. Even when true, the service is not usable until base_url and api_key resolve. |
base_url | https://api.openai.com/v1 | OpenAI-compatible audio endpoint. |
api_key | — | ${ENV_VAR} or ${secret:<name>}. For a service-account bot, ${OPENAI_API_KEY} is simplest. When set to ${secret:<name>}, Telegram lists that secret in User Secrets as needed by Telegram. |
stt_model | whisper-1 | Transcription model. |
stt_language | null | ISO code to force a language, or null to auto-detect. |
stt_max_audio_bytes | 26214400 | Reject larger uploads before calling the provider (OpenAI caps at 25 MB). |
tts_model | tts-1 | Synthesis model. |
tts_voice | alloy | Voice name. |
tts_format | opus | Output format. opus yields Ogg/Opus, which Telegram renders as a native voice note. |
tts_speed | 1.0 | Playback speed. |
tts_max_input_chars | 4096 | Truncate longer text before synthesis (the full text is still sent as a message). |
read_timeout | 120.0 | HTTP read timeout (seconds). |
max_retries | 2 | Transient-error retries. |
If speech.enabled is false or the key/URL don't resolve, the plugin runs text-only: voice messages get "Voice messages aren't enabled for this bot.", and replies are always text.
Speech key as a user secret
The speech api_key is a shared service credential, so an environment variable (${OPENAI_API_KEY}) is the simplest setup.
To keep it out of the environment, set api_key: "${secret:speech_api_key}" (any name). Store that secret under the same account that owns the bot token: service_user_id for technical_user, or each owner for per_user. Mixing a ${secret:...} speech key with a technical_user bot whose token comes from bot_token_env is fine — the two credentials resolve independently.
Per-user private bot setup
In per_user mode each user runs their own bot under their own Codumentor account:
- Create a bot: message @BotFather →
/newbot→ follow the prompts → copy the token (123456789:AA...). - Store the token: Settings → Extensions → User Secrets → Add, name it
telegram_bot_token(or whatevertoken_secret_nameis set to), paste the token. The pane shows "Telegram Bot Token — needed by Telegram" with set/unset state. - With
bot_discovery: periodic(default), the bot starts polling withinreconcile_intervalseconds — no restart needed. Removing the secret stops it on the next rescan. - First message binds you as the owner (default
tofuauthorization): open your bot in Telegram, send any message, and you're bound as the only account allowed to use it. Turns then run as your Codumentor user, with your secrets and repo context.
Voice messages
When voice is enabled (config.speech.enabled and voice_reply: true):
- A user sends a Telegram voice note (Ogg/Opus).
- The plugin transcribes it and prefixes the transcript with
[Voice message — transcribed]so the agent knows it was speech. - The agent's final answer is synthesized (
tts_format: opus) and sent as a Telegram voice note, followed by the full text.
Text messages are unaffected: text in → text out. Long replies are a poor fit for a single voice note, so the text follow-up is always sent. TTS input is capped at tts_max_input_chars.
Authorization
A Telegram bot is reachable by anyone who discovers its @username, and a turn runs with real agent capabilities (and, in per_user mode, the owner's secrets). Authorization gates who may actually use a bot.
technical_user mode — static allowlist:
allow_all: true— anyone may use the bot.allow_all: false(default) — onlyallowed_user_ids(Telegram numeric ids) may use it. Empty list ⇒ nobody.
per_user mode (per_user_authz):
tofu(trust-on-first-use, default) — the first Telegram account to message a user's bot is bound as its owner; everyone else is rejected.allowlist— the owner stores allowed Telegram numeric ids in thetelegram_allowed_idsuser secret (comma/space separated). If unset, the bot rejects everyone.
Find your numeric Telegram id by messaging @userinfobot.
Telegram bot setup
- Open @BotFather in Telegram and send
/newbot. - Choose a display name and a unique
@usernameending inbot. - Copy the token BotFather gives you (
123456789:AA...). - technical_user: store the token as the
telegram_bot_token_globaluser secret under the service account (or, as a fallback, in thebot_token_envenvironment variable). per_user: each user stores it as thetelegram_bot_tokenuser secret. - (Optional)
/setprivacy→ Disable if you want the bot to see all messages in groups; for 1:1 chats the default is fine. - Open a chat with your bot and send a message. (In
per_user/tofuthis binds you as owner; intechnical_usermake sure your id is inallowed_user_ids.)
Behavior
- One in-flight turn per chat. If a second message arrives while the previous turn is still running, Telegram gets "Still working on your previous message — one moment." and the new message is dropped.
- Auto-reply. Only the main agent's final response is posted back; empty text is skipped. Replies longer than 4,000 characters are split at paragraph breaks (Telegram's limit is 4,096).
- One poller. If you run more than one API process, only one of them polls Telegram; the others still post auto-replies. That avoids Telegram's HTTP 409 when two pollers share a token.
- Tokens are never written to logs. Shared and per-user tokens live encrypted in User Secrets (
bot_token_envis the plaintext-env fallback fortechnical_user). - Each Telegram chat maps to one Codumentor conversation. Stale mappings (older than 30 days) are cleaned up automatically.
/mode,/effort,/thinkingchange or show the chat's thinking mode and reasoning effort without starting a turn, when the Thinking Modes plugin is enabled. Every other/commandis sent to the assistant unchanged.
Limitations
- Web UI live updates. Telegram-originated conversations are stored like UI conversations, but a web client already open on the live stream will not see real-time updates until it reloads. The transcript is fully visible after refresh. (Same trade-off as the Slack plugin.)
- Permissions are
auto_allow. Nobody is watching the web approval dialog from Telegram, so tool prompts are auto-allowed (global deny rules still apply). If a turn does suspend on an approval, the bot auto-approves it and resumes rather than blocking the chat. - No group-thread isolation. A whole chat maps to one conversation; per-topic threads are not split out.
See also
- Example config — copy-pasteable
speech:+ plugin entry for both modes - Slack Integration — the same two-way pattern for Slack
- Thinking Modes — the
/modeand/effortcommands - User Secrets — where bot tokens are stored
- Configuration Reference