Skip to content

Commit b9137bf

Browse files
committed
docs: create dedicated Telegram documentation
- New file docs/TELEGRAM.md (293 lines) covering: - Architecture overview with component diagram - Configuration (env vars, validation) - Bot API client (all methods, helpers, fallback URLs, token budget) - Long-polling (poller offset tracking, timeouts) - Message handler (callbacks, access control, inline keyboards) - Slash commands (all 14 built-in commands) - Session manager (per-chat persistence, cache, TTL, clarify channels) - Plan management (CRUD, slug generation, prefix matching) - Media download (voice + photo, fileID truncation, extension fallback) - Types reference - Testing guide - README.md: - Add 🤖 Telegram Bot to strategic features section - Add Telegram Bot to docs reference table - docs/CHEATSHEET.md: - Add Telegram Bot section with env var, commands, features listing
1 parent 54c2e93 commit b9137bf

3 files changed

Lines changed: 313 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ Any OpenAI-compatible endpoint: Deepseek, OpenAI, Anthropic, Ollama, vLLM, Groq,
5656
### 🌐 Web UI
5757
`odek serve` — browser-based agent with `@` resource completion (`@file.go`, `@sess:abc123`), **drag-and-drop file attachments**, WebSocket streaming, and a full IDE-style console. [docs/WEBUI.md](docs/WEBUI.md)
5858

59+
### 🤖 Telegram Bot
60+
Run agent tasks directly from Telegram via long-polling. Supports slash commands (`/plan`, `/sessions`, `/resume`, `/prune`, `/help`, etc.), voice message transcription, photo analysis, conversation persistence across restarts, saved plan files, and daily token budgeting. No external Telegram libraries — built on stdlib `net/http`. [docs/TELEGRAM.md](docs/TELEGRAM.md)
61+
5962
### 📎 File Attachments
6063
Attach files to any prompt with `--ctx` / `-c` (CLI), `@filename` inline references (CLI + REPL + Web UI), or drag-and-drop (Web UI). File content is injected as context blocks before the task — no tool calls needed. Comma-separate multiple files: `--ctx main.go,lib.go`. [docs/CLI.md#file-attachments](docs/CLI.md#file-attachments)
6164

@@ -154,6 +157,7 @@ odek run "@README.md what does this project do?"
154157
| [Prompt Caching](docs/CACHING.md) | Anthropic/OpenAI/DeepSeek caching support, config, metrics |
155158
| [Memory](docs/MEMORY.md) | Three-tier design, go-vector merge-on-write, `memory` tool |
156159
| [Sessions](docs/SESSIONS.md) | Multi-turn conversations, save/resume/trim/cleanup |
160+
| [Telegram Bot](docs/TELEGRAM.md) | Telegram integration: bot client, slash commands, session management, plans, media downloads |
157161
| [Sandboxing](docs/SANDBOXING.md) | Docker isolation model, config, security hardening |
158162
| [Security](docs/SECURITY.md) | Threat model, prompt injection defense, sandbox model |
159163
| [Sub-Agents](docs/SUBAGENTS.md) | Task decomposition, delegation tool, subagent protocol |

docs/CHEATSHEET.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,22 @@ Env vars: `ODEK_SANDBOX=true`, `ODEK_SANDBOX_IMAGE`, `ODEK_SANDBOX_NETWORK`, etc
138138

139139
Default network: `bridge` (internet access). Set `none` for air-gapped execution.
140140

141+
## Telegram Bot
142+
143+
- Requires `ODEK_TELEGRAM_BOT_TOKEN` env var
144+
- Slash commands: `/start`, `/help`, `/new`, `/plan`, `/plans`, `/plan-view`, `/plan-delete`, `/sessions`, `/resume`, `/prune`, `/stats`, `/stop`, `/mode`, `/restart`
145+
- Voice messages: automatically processed via `DownloadVoice` → OGG files in `~/.odek/media/`
146+
- Photos: automatically processed via `DownloadPhoto` → JPG files in `~/.odek/media/`
147+
- Conversations persist across bot restarts (`tg-<chatID>` sessions)
148+
- Session TTL: 24h default (configurable)
149+
- Daily token budget tracking in `~/.odek/telegram_token_usage_<date>`
150+
- Fallback API URLs for regions where `api.telegram.org` is blocked
151+
- Access control: restrict by chat ID or user ID
152+
- Logging: configurable log level and log file
153+
154+
See [docs/TELEGRAM.md](docs/TELEGRAM.md) for full documentation.
155+
156+
141157
## Sessions
142158

143159
- Stored in `~/.odek/sessions/<uuid>.json`

docs/TELEGRAM.md

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
# Telegram Bot Integration
2+
3+
odek includes a built-in Telegram bot that lets you run agent tasks directly from Telegram. The bot supports long-polling, slash commands, voice messages, photo messages, session persistence, and plan management.
4+
5+
## Architecture
6+
7+
```
8+
Telegram Bot API ◄── bot.go (HTTP client)
9+
10+
poller.go (long-polling loop)
11+
12+
handler.go (message dispatcher)
13+
14+
┌─────┴──────┐
15+
│ │
16+
commands.go session.go (per-chat agent sessions)
17+
(slash │
18+
commands) plan.go (.md plan files)
19+
20+
download.go (voice/photo media)
21+
```
22+
23+
The package is self-contained under `internal/telegram/` with 409 tests and 86.9% coverage. All Telegram API calls use the Bot struct, which wraps `net/http` with JSON marshaling and multipart upload support. No external Telegram libraries are used.
24+
25+
## Configuration
26+
27+
### Environment Variables
28+
29+
All configuration flows through `TelegramConfig` and can be set via environment variables:
30+
31+
| Variable | Field | Default |
32+
|---|---|---|
33+
| `ODEK_TELEGRAM_BOT_TOKEN` | Token | — (required) |
34+
| `ODEK_TELEGRAM_ALLOWED_CHATS` | AllowedChats | all |
35+
| `ODEK_TELEGRAM_ALLOWED_USERS` | AllowedUsers | all |
36+
| `ODEK_TELEGRAM_BOT_USERNAME` | BotUsername ||
37+
| `ODEK_TELEGRAM_POLL_INTERVAL` | PollInterval | 1s |
38+
| `ODEK_TELEGRAM_POLL_TIMEOUT` | PollTimeout | 30s |
39+
| `ODEK_TELEGRAM_MAX_MSG_LENGTH` | MaxMsgLength | 4096 |
40+
| `ODEK_TELEGRAM_DAILY_TOKEN_BUDGET` | DailyTokenBudget | unlimited |
41+
| `ODEK_TELEGRAM_SESSION_TTL_HOURS` | SessionTTL | 24h |
42+
| `ODEK_TELEGRAM_FALLBACK_URLS` | FallbackURLs ||
43+
| `ODEK_TELEGRAM_LOG_LEVEL` | LogLevel | info |
44+
| `ODEK_TELEGRAM_LOG_FILE` | LogFile | stderr |
45+
46+
### Config Validation
47+
48+
`ValidateConfig` checks:
49+
- **Token** must not be empty
50+
- **PollInterval** must be ≥ 1
51+
- **PollTimeout** must be between 1 and 60
52+
- **MaxMsgLength** must be between 1 and 4096
53+
- **SessionTTL** must be ≥ 1
54+
55+
## Bot API Client (`bot.go`)
56+
57+
The `Bot` struct is a lightweight Telegram Bot API client built on the standard library. It provides methods for all major API endpoints.
58+
59+
### Core Methods
60+
61+
| Method | Endpoint | Purpose |
62+
|---|---|---|
63+
| `GetUpdates` | `getUpdates` | Long-poll for incoming updates |
64+
| `SendMessage` | `sendMessage` | Send a text message (supports parse mode, reply markup, web page preview) |
65+
| `EditMessageText` | `editMessageText` | Edit a previously sent message |
66+
| `SendPhoto` | `sendPhoto` | Send a photo file (multipart upload) |
67+
| `SendVoice` | `sendVoice` | Send a voice note (multipart upload) |
68+
| `SendChatAction` | `sendChatAction` | Show typing/recording status in chat |
69+
| `GetFile` | `getFile` | Get file metadata for download |
70+
| `DownloadFile` || Download file bytes from Telegram's file server |
71+
| `AnswerCallbackQuery` | `answerCallbackQuery` | Answer inline keyboard callbacks |
72+
| `SetMyCommands` | `setMyCommands` | Register bot command list |
73+
| `GetMe` | `getMe` | Health check / bot info |
74+
| `CheckDailyBudget` || Enforce daily token usage limit |
75+
76+
### Underlying HTTP Helpers
77+
78+
- **`doJSON`** — Sends JSON POST requests and unmarshals the `result` field from Telegram's response envelope
79+
- **`doUpload`** — Sends multipart/form-data POST requests for file uploads (photo, voice)
80+
81+
### Fallback URLs
82+
83+
`SetFallbackURLs` configures alternate Telegram API endpoints. If the primary endpoint is unreachable, the bot falls through to the next URL in the list. This is useful for regions where `api.telegram.org` may be blocked.
84+
85+
### Daily Token Budget
86+
87+
`SetDailyTokenBudget` and `CheckDailyBudget` implement a simple daily token usage tracker:
88+
- Usage is persisted to `~/.odek/telegram_token_usage_<YYYY-MM-DD>`
89+
- Budget resets automatically each calendar day
90+
- Returns an error if the total exceeds the configured budget
91+
- No-op when budget is 0 (unlimited)
92+
93+
## Long-Polling (`poller.go`)
94+
95+
The `Poller` struct implements Telegram's long-polling update mechanism:
96+
97+
- **Offset tracking**: updates are acknowledged by advancing the offset past the highest received update ID, preventing duplicate processing
98+
- **Configurable timeout**: default 30s long-poll (Telegram holds the connection open until updates arrive or the timeout expires)
99+
- **Polling interval**: 1s between poll cycles (configurable via `PollInterval`)
100+
101+
```go
102+
poller := telegram.NewPoller(bot)
103+
poller.SetLogger(logger)
104+
105+
for {
106+
updates, err := poller.Poll(ctx)
107+
if err != nil {
108+
continue // log and retry
109+
}
110+
for _, update := range updates {
111+
handler.Handle(ctx, update)
112+
}
113+
}
114+
```
115+
116+
## Message Handler (`handler.go`)
117+
118+
The `Handler` struct routes incoming updates to the appropriate callback based on message type. It is the bridge between raw Telegram updates and the agent.
119+
120+
### Callbacks
121+
122+
| Callback | Trigger | Signature |
123+
|---|---|---|
124+
| `OnTextMessage` | Plain text message | `(chatID int64, text string) (string, error)` |
125+
| `OnCommand` | Slash command (e.g. `/start`) | `(chatID int64, command, args string) (string, error)` |
126+
| `OnVoiceMessage` | Voice message (OGG) | `(chatID int64, fileID string) (string, error)` |
127+
| `OnPhotoMessage` | Photo message | `(chatID int64, fileIDs []string) (string, error)` |
128+
| `OnCallbackQuery` | Inline keyboard callback | `(chatID int64, callbackData string) (string, error)` |
129+
130+
All callbacks return a response string (may be empty) and an error. The `Handle` method:
131+
1. Sends `SendChatAction("typing")` immediately
132+
2. Dispatches to the appropriate callback
133+
3. Sends the response text back to the chat
134+
4. Splits long messages exceeding `MaxMsgLength` into chunks
135+
136+
### Access Control
137+
138+
`HandlerConfig` supports:
139+
- **AllowedChats** — restrict to specific chat IDs (empty = allow all)
140+
- **AllowedUsers** — restrict to specific user IDs (empty = allow all)
141+
- **BotUsername** — for `@mention` detection in groups
142+
143+
### Inline Keyboards
144+
145+
The handler uses `sync.Map` for `TelegramApprover` instances, keyed by `chatID`. This allows the agent to send inline keyboard approval requests (yes/no) and receive responses via callback queries. The handler intercepts callback queries matching pending approval requests before dispatching to `OnCallbackQuery`.
146+
147+
## Slash Commands (`commands.go`)
148+
149+
### Built-in Commands
150+
151+
| Command | Description |
152+
|---|---|
153+
| `/start` | Welcome message and bot introduction |
154+
| `/help` | Show all available commands with descriptions |
155+
| `/new` | Reset the current conversation (clear context) |
156+
| `/stats` | Show session statistics (turn count, model used, etc.) |
157+
| `/stop` | Cancel a running agent task |
158+
| `/mode` | Toggle agent modes (sandbox, verbose) |
159+
| `/restart` | Gracefully restart the bot process |
160+
| `/plan <description>` | Create a new plan from a natural language description |
161+
| `/plans` | List all saved plans |
162+
| `/plan-view <slug>` | View a specific plan's content |
163+
| `/plan-delete <slug>` | Delete a saved plan |
164+
| `/sessions` | List recent conversation sessions |
165+
| `/resume <session_id>` | Resume a previous session by ID |
166+
| `/prune [days]` | Clean up old sessions (default: 30 days) |
167+
168+
### Architecture
169+
170+
Commands are registered via `CommandDescriptor` structs in the `DefaultCommands` slice. Each descriptor has:
171+
- **Command** — the slash command name (without `/`)
172+
- **Description** — shown in `/help` and registered via `SetMyCommands`
173+
- **Handler** — function `func(args string) (string, error)`
174+
175+
The handler uses `init()` to populate `DefaultCommands`, avoiding initialization cycles between the command definitions and the handler functions they reference.
176+
177+
## Session Manager (`session.go`)
178+
179+
The `SessionManager` manages per-chat Telegram agent conversations, backed by the existing [`session.Store`](SESSIONS.md).
180+
181+
### How It Works
182+
183+
1. Each Telegram chat gets a session identified by `tg-<chatID>`
184+
2. Sessions are persisted to `~/.odek/sessions/tg-<chatID>.json`
185+
3. An in-memory cache (`map[int64]*ChatSession`) avoids redundant disk reads
186+
4. Session TTL (default 24h) controls inactivity timeout
187+
5. Active sessions survive bot restarts — on reconnect, the session is loaded from disk
188+
189+
### Key Methods
190+
191+
| Method | Purpose |
192+
|---|---|
193+
| `GetOrCreate(chatID)` | Get existing session or create new one |
194+
| `Append(chatID, messages)` | Add messages to session |
195+
| `Save(chatID)` | Persist session to disk |
196+
| `ClearMessages(chatID)` | Clear conversation history (keeps metadata) |
197+
198+
### Clarify Channels
199+
200+
The `clarifyChannels` sync.Map provides per-chat channels for the agent to ask the user questions and receive answers asynchronously. This bridges the gap between the agent's synchronous `clarify` tool call and Telegram's asynchronous message flow.
201+
202+
## Plan Management (`plan.go`)
203+
204+
Plans are stored as markdown files in `~/.odek/plans/<slug>.md`. Each plan is created from a natural language description and persisted for later review.
205+
206+
### Key Functions
207+
208+
| Function | Purpose |
209+
|---|---|
210+
| `Slugify(description)` | Convert description to filesystem-safe slug (max 60 chars) |
211+
| `ListPlans(limit)` | List plans sorted by modification time (newest first) |
212+
| `ReadPlan(slug)` | Load plan content by exact slug or prefix match |
213+
| `DeletePlan(slug)` | Delete plan by exact slug or prefix match |
214+
| `MostRecentPlan()` | Return the most recently modified plan's content |
215+
| `EnsurePlansDir()` | Create plans directory if it doesn't exist |
216+
217+
### Slug Generation
218+
219+
Slug generation (`slugify`) collapses a description into a lowercase, hyphen-separated identifier:
220+
- Strips non-alphanumeric characters
221+
- Collapses whitespace to hyphens
222+
- Truncates to 60 characters max
223+
- Falls back to `"plan"` if no valid characters remain
224+
225+
### Prefix Matching
226+
227+
`ReadPlan` and `DeletePlan` support prefix matching: if the given slug uniquely prefixes an existing plan file, it matches. Ambiguous prefixes return an error listing the matching plans.
228+
229+
## Media Download (`download.go`)
230+
231+
Supports downloading voice messages and photos from Telegram to the local filesystem.
232+
233+
### Media Directory
234+
235+
Media files are saved to `~/.odek/media/` (created automatically on first download).
236+
237+
### DownloadVoice
238+
239+
- Gets file metadata via `GetFile`
240+
- Downloads raw bytes via `DownloadFile`
241+
- Saves as `voice_<truncated_fileID>.<ext>` (default extension: `.ogg`)
242+
- Truncates fileID to 16 chars for filenames
243+
244+
### DownloadPhoto
245+
246+
- Takes a slice of `PhotoSize` IDs (Telegram sends multiple sizes)
247+
- Uses the last (largest) photo size
248+
- Saves as `photo_<truncated_fileID>.<ext>` (default extension: `.jpg`)
249+
- Same fileID truncation as voice downloads
250+
251+
## Types (`types.go`)
252+
253+
The package defines Telegram API types used throughout:
254+
255+
| Type | Purpose |
256+
|---|---|
257+
| `Bot` | API client struct |
258+
| `Message` | Incoming/outgoing message |
259+
| `Update` | Incoming update (message, callback query, etc.) |
260+
| `User` | Telegram user |
261+
| `File` | File metadata for download |
262+
| `PhotoSize` | Photo with dimensions |
263+
| `BotCommand` | Command descriptor for `SetMyCommands` |
264+
| `SendOpts` | Optional parameters for `SendMessage` |
265+
| `ReplyKeyboardMarkup` | Keyboard layout |
266+
| `InlineKeyboardMarkup` | Inline keyboard layout |
267+
| `InlineKeyboardButton` | Inline keyboard button |
268+
| `CallbackQuery` | Inline keyboard callback |
269+
| `HandlerConfig` | Message handler configuration |
270+
| `TelegramConfig` | Full bot configuration |
271+
| `PlanInfo` | Plan file summary |
272+
| `ChatSession` | Per-chat agent session |
273+
| `SessionManager` | Session lifecycle manager |
274+
| `Logger` | Logging interface |
275+
| `Poller` | Long-polling update fetcher |
276+
277+
## Testing
278+
279+
The Telegram package has **409 tests** with **86.9% coverage**. Tests use:
280+
- `httptest.NewServer` to mock Telegram API responses
281+
- HTTP handler functions for each API endpoint (getFile, sendMessage, sendDocument, etc.)
282+
- `t.TempDir()` + `t.Setenv("HOME", ...)` for filesystem isolation
283+
- Long fileID truncation tests for voice/photo downloads
284+
- Plan CRUD tests with prefix matching, ambiguous matches, and error paths
285+
- Session manager tests with TTL expiry and cache behavior
286+
287+
```bash
288+
# Run all Telegram tests
289+
go test ./internal/telegram/ -v -count=1
290+
291+
# With coverage
292+
go test -cover ./internal/telegram/
293+
```

0 commit comments

Comments
 (0)