Codumentor logo Codumentor

Telegram Plugin

The Telegram plugin connects Codumentor to a Telegram bot so users can talk to the mentor agent from their phone or desktop — with voice support:

Speech-to-text and text-to-speech are provided by the general Speech service (config.speech), which uses OpenAI-compatible audio APIs and is reusable beyond this plugin.

Two modes

The plugin runs in one of two modes, chosen by mode:

ModeBotTurns run asToken fromBest for
technical_userone shared bota single service usertelegram_bot_token_global user secret (stored under the service user), or the bot_token_env env var as fallbacka team/instance bot
per_userone bot per userthe bot's ownereach user's telegram_bot_token user secreteveryone brings their own private bot

Both modes resolve their bot token through the central User Secrets store and declare it via ui_manifest() so it shows up in the User Secrets pane ("needed by Telegram, not set yet"). technical_user additionally accepts the legacy bot_token_env environment variable as a fallback when the secret is unset.

Overview

  1. Long-polls Telegram's getUpdates — no public URL or webhook receiver required.
  2. Maps each Telegram chat to a single Codumentor conversation in plugin-owned SQLite (data/telegram_integration.sqlite3). Subsequent messages in the same chat continue the same conversation.
  3. Subscribes to onResponsePersist to auto-post the agent's final response back to the originating chat (only main-agent answers, never subagent intermediate output).
  4. If the incoming message was a voice note, transcribes it before the turn and synthesizes the reply back as a voice note afterward.
  5. Runs the poller from the onServerReady hook so exactly one poller starts per process, and guards single-runner across processes with a file lock (Telegram returns HTTP 409 if two pollers share a token).
  6. Runs a daily TTL sweep to age out stale update-dedup rows, per-turn reply modes, and old chat mappings.

Requirements

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:

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. The outbound auto-reply hook is always attached but no-ops for non-Telegram conversations.

Authorization: with allow_all: false (the default) and an empty allowed_user_ids, the bot rejects everyone. Add your numeric Telegram id to allowed_user_ids (ask @userinfobot for it) 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

In 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.

Configuration parameters

ParameterTypeDefaultDescription
enabledbooltrueMaster on/off switch.
modestringtechnical_usertechnical_user (one shared bot) or per_user (one bot per user).
global_token_secret_namestringtelegram_bot_token_globaltechnical_user: name of the user-secret (stored under service_user_id) holding the shared bot token. Preferred over bot_token_env; declared to the User Secrets pane.
bot_token_envstringTELEGRAM_BOT_TOKENtechnical_user: env var holding the bot token, used only as a fallback when the global_token_secret_name secret is unset. Read from the environment, never the YAML — only the env-var name is recorded, never the token.
service_user_idstringtelegram-bottechnical_user: Codumentor user that owns Telegram-originated conversations and stores the shared bot-token secret. With auth.provider: none, any string is accepted.
allow_allboolfalsetechnical_user: if true, anyone who finds the bot may use it. Leave false and use allowed_user_ids.
allowed_user_idslist[int][]technical_user: Telegram numeric user-ids allowed to use the bot. Empty + allow_all: false ⇒ nobody is allowed.
token_secret_namestringtelegram_bot_tokenper_user: user-secret name each user stores their bot token under.
per_user_authzstringtofuper_user: who may use a user's bot — tofu or allowlist (see Authorization).
per_user_allowed_ids_secretstringtelegram_allowed_idsper_user/allowlist: user-secret holding the owner's allowed Telegram ids (comma/space separated).
bot_discoverystringperiodicper_user: periodic rescans for added/removed bots every reconcile_interval; startup discovers once.
reconcile_intervalint60per_user/periodic: seconds between rescans.
permission_policystringauto_allowTool-permission policy for turns from this channel. Only auto_allow is wired today: tool prompts are auto-allowed (global deny rules still apply), and a turn stuck on a durable approval is auto-approved and resumed.
target_agentsstringmainWhich agent types this applies to: main, subagent, all.
voice_replybooltrueHonor voice-in → voice-out. Requires config.speech enabled; otherwise replies stay text.
long_poll_timeoutint50getUpdates long-poll timeout (seconds).
request_timeoutfloat70.0HTTP timeout (seconds); must exceed long_poll_timeout.

Speech service (STT / TTS)

Voice support is provided by a general, system-level speech service configured under the top-level speech: key. It is not Telegram-specific — it can power future web/TUI voice features too.

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
ParameterDefaultDescription
enabledfalseMaster switch. Even when true, the service reports not usable until base_url and api_key resolve to real values.
base_urlhttps://api.openai.com/v1OpenAI-compatible audio endpoint. Point it elsewhere to self-host later.
api_keySupports ${ENV_VAR} substitution and ${secret:<name>} resolution. For a service-account caller ${OPENAI_API_KEY} is simplest. When set to ${secret:<name>}, the Telegram plugin declares it to the User Secrets pane and installs the relevant user's secrets around background transcription so the key resolves there too (see Speech key as a user secret).
stt_modelwhisper-1Transcription model (/v1/audio/transcriptions).
stt_languagenullISO code to force a language, or null to auto-detect.
stt_max_audio_bytes26214400Reject larger uploads before calling the provider (OpenAI caps at 25 MB).
tts_modeltts-1Synthesis model (/v1/audio/speech).
tts_voicealloyVoice name.
tts_formatopusOutput format. opus yields Ogg/Opus, which Telegram renders as a native voice note.
tts_speed1.0Playback speed.
tts_max_input_chars4096Truncate longer text before synthesis (the full text is still sent as a message).
read_timeout120.0HTTP read timeout (seconds).
max_retries2Transient-error retries.

If speech.enabled is false or the key/URL don't resolve, the plugin runs text-only: voice messages get a "voice isn't enabled for this bot" notice, and replies are always text.

Speech key as a user secret

The speech api_key is a shared service credential, so the simplest setup is an environment variable (${OPENAI_API_KEY}, the default), which resolves everywhere — including the background transcription loop, which runs outside any agent turn.

If you prefer to keep it out of the environment, set api_key: "${secret:speech_api_key}" (any name). Then:

Store the secret under the same account that owns the bot token: the 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:

  1. Create a bot: message @BotFather/newbot → follow the prompts → copy the token (123456789:AA...).
  2. Store the token in Codumentor: Settings → User Secrets → Add, name it telegram_bot_token (or whatever token_secret_name is set to), paste the token.
  3. With bot_discovery: periodic (default), the bot starts polling within reconcile_interval seconds — no restart needed. Removing the secret stops it on the next rescan.
  4. First message binds you as the owner (default tofu authorization): 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.

To make step 2 obvious, the plugin declares this secret to the User Secrets pane via ui_manifest() secret_requirements, so it shows "Telegram Bot Token — needed by Telegram" with set/unset state. See Secret requirements.

Voice messages

When voice is enabled (config.speech.enabled and voice_reply: true):

  1. A user sends a Telegram voice note (Ogg/Opus).
  2. The plugin downloads the audio (getFile + file download) and transcribes it with the Speech service (Whisper accepts Ogg/Opus directly — no conversion).
  3. The transcript is passed to the agent prefixed with [Voice message — transcribed] so the agent knows it was speech, and the turn is flagged to reply with voice.
  4. After the turn, the agent's final answer is synthesized (tts_format: opus) and sent as a Telegram voice note via sendVoice — followed by the full text so nothing is lost.

Text messages are unaffected: text in → text out.

Long replies are summarized poorly as a single long voice note, so the text follow-up is always sent. The 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:

per_user mode (per_user_authz):

Find your numeric Telegram id by messaging @userinfobot.

Telegram bot setup

  1. Open @BotFather in Telegram and send /newbot.
  2. Choose a display name and a unique @username ending in bot.
  3. Copy the token BotFather gives you (123456789:AA...).
  4. technical_user: store the token as the telegram_bot_token_global user-secret under the service account (or, as a fallback, in the bot_token_env environment variable). per_user: each user stores it as the telegram_bot_token user-secret.
  5. (Optional) /setprivacyDisable if you want the bot to see all messages in groups; for 1:1 chats the default is fine.
  6. Open a chat with your bot and send a message. (In per_user/tofu this binds you as owner; in technical_user make sure your id is in allowed_user_ids.)

State & storage

Plugin-private SQLite at data/telegram_integration.sqlite3:

A background TTL task runs every 24 hours and purges stale processed_updates, turn_reply_mode, and 30-day-old chat_mappings rows.

Behavior notes

Limitations

See Also