From 4ac19117c1331974a5ba3abbbd973fbc7a83b878 Mon Sep 17 00:00:00 2001 From: Arne Moor Date: Sat, 6 Dec 2025 03:26:23 +0100 Subject: [PATCH] fix: update all Telegram paths to use clawdis branding - Fixed normalizeAllowFromEntry to properly handle telegram: prefix - Removed unused telegram.allowFrom config field from schema - Updated all documentation to use ~/.clawdis paths with legacy fallback notes - Fixed selectProviders to include Twilio support - Added provider disconnect in agent delivery - Improved media type detection - Made Telegram inbound media save buffer to disk for Claude access All Telegram identifiers now require telegram: prefix in shared inbound.allowFrom config. --- docs/architecture/telegram-integration.md | 38 ++++--------- docs/telegram.md | 64 +++++++++------------ src/commands/agent.ts | 69 +++++++++++++++++------ src/config/config.ts | 5 -- src/telegram/download.ts | 2 +- src/telegram/monitor.ts | 25 ++++++-- src/telegram/outbound.ts | 2 +- src/utils.ts | 15 ++++- src/web/session.ts | 30 +++++++++- 9 files changed, 155 insertions(+), 95 deletions(-) diff --git a/docs/architecture/telegram-integration.md b/docs/architecture/telegram-integration.md index a5369beef..3ab821a8c 100644 --- a/docs/architecture/telegram-integration.md +++ b/docs/architecture/telegram-integration.md @@ -9,7 +9,7 @@ This document provides architectural guidance for adding Telegram as a third mes 1. **MTProto client**: Users log in with their personal Telegram account (phone + 2FA) 2. **Same security model**: `allowFrom` whitelist controls who can trigger auto-replies 3. **Provider abstraction**: Unified interface across Twilio, Web, and Telegram providers -4. **Session storage**: File-based session like WhatsApp Web (`~/.warelay/telegram/session/`) +4. **Session storage**: File-based session like WhatsApp Web (`~/.clawdis/telegram/session/`) --- @@ -32,7 +32,7 @@ This document provides architectural guidance for adding Telegram as a third mes ```mermaid flowchart LR - subgraph Warelay["warelay CLI (Container)"] + subgraph Warelay["clawdis CLI (Container)"] CLI[[CLI / Commander]] Send[[Send Command]] Relay[[Relay Command]] @@ -57,7 +57,7 @@ flowchart LR WhatsApp -->|Baileys| IDP ``` -**Caption:** warelay system context showing CLI entry points and provider connections. +**Caption:** clawdis system context showing CLI entry points and provider connections. **Evidence:** `src/cli/program.ts:L1-L50`, `src/commands/send.ts:L1-L150` @@ -156,7 +156,7 @@ export async function pickProvider(pref: Provider | "auto"): Promise { | **Auth Model** | QR code scan | Phone + code + 2FA | | **Connection Type** | Persistent WebSocket | Persistent TCP/WebSocket | | **Library** | Baileys | GramJS | -| **Session Storage** | `~/.warelay/credentials/` | `~/.warelay/telegram/session/` | +| **Session Storage** | `~/.clawdis/credentials/` | `~/.clawdis/telegram/session/` | | **Message Send** | Socket message | MTProto request | | **Inbound Handling** | Event listener | Event listener | | **Delivery Status** | Limited (receipts) | Full (receipts + read) | @@ -189,7 +189,7 @@ sequenceDiagram **Key characteristics:** - Persistent connection with session state -- QR-based authentication stored at `~/.warelay/credentials/` +- QR-based authentication stored at `~/.clawdis/credentials/` - Media sent as buffers with automatic optimization - IPC server for relay mode to prevent session corruption @@ -216,7 +216,7 @@ sequenceDiagram **Key characteristics:** - Persistent connection with session state (like WhatsApp Web) -- Phone-based authentication stored at `~/.warelay/telegram/session/` +- Phone-based authentication stored at `~/.clawdis/telegram/session/` - Media sent as buffers - Same patterns as Baileys for session management @@ -276,7 +276,7 @@ flowchart TB end subgraph Storage["Local Storage"] - SessionFile[("~/.warelay/telegram/session/")] + SessionFile[("~/.clawdis/telegram/session/")] end Login -->|phone + code| Client @@ -640,7 +640,7 @@ TELEGRAM_API_ID=12345678 TELEGRAM_API_HASH=0123456789abcdef0123456789abcdef ``` -### 5.2 Extended warelay.json Schema +### 5.2 Extended clawdis.json Schema ```typescript // Extended WarelayConfig type @@ -668,13 +668,13 @@ export type WarelayConfig = { }; ``` -### 5.3 Sample warelay.json with Telegram +### 5.3 Sample clawdis.json with Telegram ```json5 { "logging": { "level": "info", - "file": "~/.warelay/logs/warelay.log" + "file": "~/.clawdis/logs/clawdis.log" }, // Telegram-specific settings @@ -979,28 +979,14 @@ warelay relay --provider telegram --verbose **Context:** Need to store Telegram session like WhatsApp Web credentials. -**Decision:** Store at `~/.warelay/telegram/session/` following existing patterns. +**Decision:** Store at `~/.clawdis/telegram/session/` following existing patterns. **Consequences:** -- Consistent with `~/.warelay/credentials/` for WhatsApp +- Consistent with `~/.clawdis/credentials/` for WhatsApp - User-specific storage - Easy to backup/restore - Clear separation between providers -### ADR-004: Security Model Consistency - -**Status:** Accepted - -**Context:** WhatsApp uses `allowFrom` whitelist. Telegram needs same model. - -**Decision:** Use `telegram.allowFrom` in config with usernames and user IDs. - -**Consequences:** -- Consistent security model across providers -- Users understand the pattern already -- Supports both `@username` and numeric IDs -- Same behavior: whitelist = only listed users trigger auto-reply - --- ## Appendix A: File Evidence Index diff --git a/docs/telegram.md b/docs/telegram.md index dd221d89a..9099d20c4 100644 --- a/docs/telegram.md +++ b/docs/telegram.md @@ -33,24 +33,26 @@ You'll be prompted for: 2. **SMS verification code** (sent to your Telegram app or SMS) 3. **2FA password** (if you have two-factor authentication enabled) -Session is saved to `~/.warelay/telegram/session/` and persists across restarts. +Session is saved to `~/.clawdis/telegram/session/` (or `~/.warelay/telegram/session/` for legacy compatibility) and persists across restarts. ### 4. Configure Whitelist (Optional) -In `~/.warelay/warelay.json`: +In `~/.clawdis/clawdis.json` (or `~/.warelay/warelay.json` for legacy): ```json5 { - telegram: { - // Only these users can trigger auto-replies + inbound: { + // Only these users can trigger auto-replies (works for both Telegram and WhatsApp) allowFrom: [ - "@username", // Telegram username (with @) - "+1234567890", // Phone number (with +) - "123456789" // User ID (numeric) + "telegram:@username", // Telegram username (with telegram: prefix) + "telegram:123456789", // Telegram user ID (numeric) + "+1234567890" // WhatsApp phone number (E.164 format) ] } } ``` +**Note:** Telegram identifiers in `allowFrom` should use the `telegram:` prefix (e.g., `telegram:@alice`). WhatsApp uses E.164 phone numbers (e.g., `+1234567890`). + **Security note:** If `allowFrom` is empty or omitted, all incoming messages will trigger auto-replies. Use a whitelist in production. ## CLI Usage @@ -113,7 +115,7 @@ Shows recent sent/received messages with delivery status. warelay logout --provider telegram ``` -Removes the saved session from `~/.warelay/telegram/session/`. +Removes the saved session from `~/.clawdis/telegram/session/` (or `~/.warelay/telegram/session/` for legacy). ## Features @@ -150,21 +152,22 @@ Control who can trigger auto-replies via `allowFrom` config: ```json5 { - telegram: { - allowFrom: ["@alice", "@bob", "123456789"] + inbound: { + allowFrom: ["telegram:@alice", "telegram:@bob", "telegram:123456789"] } } ``` -- **Username** (`@alice`): Match by Telegram username -- **Phone number** (`+15551234567`): Match by phone number -- **User ID** (`123456789`): Match by numeric Telegram user ID +- **Username** (`telegram:@alice`): Match by Telegram username (requires `telegram:` prefix) +- **User ID** (`telegram:123456789`): Match by numeric Telegram user ID (requires `telegram:` prefix) + +**Note:** The `telegram:` prefix is required for Telegram identifiers in the shared `inbound.allowFrom` config to distinguish them from WhatsApp phone numbers. If `allowFrom` is empty or omitted, **all messages trigger auto-replies** (use with caution). ### Session Storage -Session files are stored encrypted at `~/.warelay/telegram/session/`: +Session files are stored encrypted at `~/.clawdis/telegram/session/` (or `~/.warelay/telegram/session/` for legacy): - Contains authentication tokens and keys - Persists across restarts - Should be treated as sensitive (equivalent to login credentials) @@ -236,7 +239,7 @@ warelay login --provider telegram **Solution:** ```bash # Remove corrupted session -rm -rf ~/.warelay/telegram/session/ +rm -rf ~/.clawdis/telegram/session/ # Re-login warelay login --provider telegram @@ -248,10 +251,8 @@ warelay login --provider telegram ```json5 { - telegram: { - allowFrom: ["@alice", "@bob"] - }, inbound: { + allowFrom: ["telegram:@alice", "telegram:@bob"], reply: { mode: "text", text: "Thanks for your message! I'll get back to you soon." @@ -264,10 +265,8 @@ warelay login --provider telegram ```json5 { - telegram: { - allowFrom: ["@alice", "+15551234567"] - }, inbound: { + allowFrom: ["telegram:@alice", "+15551234567"], reply: { mode: "command", bodyPrefix: "You are a helpful assistant on Telegram. Be concise.\n\n", @@ -287,10 +286,8 @@ warelay login --provider telegram ```json5 { - telegram: { - allowFrom: ["@alice", "@bob", "@charlie"] - }, inbound: { + allowFrom: ["telegram:@alice", "telegram:@bob", "telegram:@charlie"], reply: { mode: "command", command: ["claude", "{{BodyStripped}}"], @@ -347,10 +344,10 @@ Telegram has rate limits for personal accounts: Session files contain authentication tokens: ```bash # Backup -cp -r ~/.warelay/telegram/session/ ~/backups/warelay-telegram-session/ +cp -r ~/.clawdis/telegram/session/ ~/backups/clawdis-telegram-session/ # Restore -cp -r ~/backups/warelay-telegram-session/ ~/.warelay/telegram/session/ +cp -r ~/backups/clawdis-telegram-session/ ~/.clawdis/telegram/session/ ``` ### 4. Monitor Logs @@ -391,14 +388,7 @@ tmux new -s warelay-telegram -d "warelay relay --provider telegram --verbose" ### Custom Session Storage -Override session path in config: -```json5 -{ - telegram: { - sessionPath: "/custom/path/to/session/" - } -} -``` +Session storage location is currently fixed at `~/.warelay/telegram/session/` (legacy path) or `~/.clawdis/telegram/session/` (new path). Custom session paths via config are not yet supported. ### Verbose Output @@ -428,7 +418,7 @@ Output includes: Media downloads use streaming to temporary files, eliminating memory buffering: -- Files downloaded to `~/.warelay/telegram-temp` +- Files downloaded to `~/.clawdis/telegram-temp` - No memory spike regardless of file size - Automatic cleanup after send (success or failure) - Orphaned files cleaned on process restart (1 hour TTL) @@ -491,7 +481,7 @@ If you encounter issues: 1. **Check logs:** Run with `--verbose` flag 2. **Verify credentials:** Ensure API ID/Hash are correct 3. **Test login:** Try `warelay login --provider telegram` manually -4. **Check session:** Verify `~/.warelay/telegram/session/` exists and is readable -5. **Review config:** Ensure `~/.warelay/warelay.json` is valid JSON5 +4. **Check session:** Verify `~/.clawdis/telegram/session/` (or `~/.warelay/telegram/session/` for legacy) exists and is readable +5. **Review config:** Ensure `~/.clawdis/clawdis.json` (or `~/.warelay/warelay.json` for legacy) is valid JSON5 For bugs or feature requests, file an issue on GitHub. diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 28401cad3..2b041414d 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -27,11 +27,42 @@ import { ensureTwilioEnv } from "../env.js"; import { runCommandWithTimeout } from "../process/exec.js"; import { pickProvider } from "../provider-web.js"; import { createInitializedProvider } from "../providers/factory.js"; -import type { TelegramProviderConfig } from "../providers/base/types.js"; +import type { TelegramProviderConfig, ProviderMedia } from "../providers/base/types.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import type { Provider } from "../utils.js"; import { sendViaIpc } from "../web/ipc.js"; +/** + * Detect media type from URL/file extension. + * Falls back to 'image' for unknown types. + */ +function detectMediaType(url: string): ProviderMedia["type"] { + const lower = url.toLowerCase(); + + // Video extensions + if (lower.match(/\.(mp4|mov|avi|mkv|webm|m4v)($|\?)/)) { + return "video"; + } + + // Audio extensions + if (lower.match(/\.(mp3|wav|ogg|m4a|aac|flac|opus)($|\?)/)) { + return "audio"; + } + + // Voice (specific audio formats) + if (lower.match(/\.(ogg|opus)($|\?)/) && lower.includes("voice")) { + return "voice"; + } + + // Document extensions + if (lower.match(/\.(pdf|doc|docx|txt|xls|xlsx|ppt|pptx|zip|rar)($|\?)/)) { + return "document"; + } + + // Image extensions (or default) + return "image"; +} + type AgentCommandOpts = { message: string; to?: string; @@ -413,21 +444,27 @@ export async function agentCommand( }; const telegramProvider = await createInitializedProvider("telegram", telegramConfig); - const chunks = chunkText(text, 4096); - if (chunks.length > 0 || media.length > 0) { - const firstChunk = chunks.length > 0 ? chunks[0] : ""; - const firstMedia = media[0]; - await telegramProvider.send(opts.to, firstChunk, { - media: firstMedia ? [{ type: "image", url: firstMedia }] : undefined, - }); - } - for (let i = 1; i < chunks.length; i++) { - await telegramProvider.send(opts.to, chunks[i]); - } - for (let i = 1; i < media.length; i++) { - await telegramProvider.send(opts.to, "", { - media: [{ type: "image", url: media[i] }], - }); + + try { + const chunks = chunkText(text, 4096); + if (chunks.length > 0 || media.length > 0) { + const firstChunk = chunks.length > 0 ? chunks[0] : ""; + const firstMedia = media[0]; + await telegramProvider.send(opts.to, firstChunk, { + media: firstMedia ? [{ type: detectMediaType(firstMedia), url: firstMedia }] : undefined, + }); + } + for (let i = 1; i < chunks.length; i++) { + await telegramProvider.send(opts.to, chunks[i]); + } + for (let i = 1; i < media.length; i++) { + await telegramProvider.send(opts.to, "", { + media: [{ type: detectMediaType(media[i]), url: media[i] }], + }); + } + } finally { + // Always disconnect to prevent connection leaks + await telegramProvider.disconnect(); } } else { const chunks = chunkText(text, 1600); diff --git a/src/config/config.ts b/src/config/config.ts index d35a0bea7..d75f5f282 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -233,11 +233,6 @@ const WarelaySchema = z.object({ .optional(), }) .optional(), - telegram: z - .object({ - allowFrom: z.array(z.string()).optional(), - }) - .optional(), }); export function loadConfig(): WarelayConfig { diff --git a/src/telegram/download.ts b/src/telegram/download.ts index 20a08fea4..f933f7e18 100644 --- a/src/telegram/download.ts +++ b/src/telegram/download.ts @@ -42,7 +42,7 @@ export interface DownloadResult { /** * Get temp directory for Telegram downloads. - * Uses ~/.warelay/telegram-temp for consistency with media store. + * Uses ~/.clawdis/telegram-temp for consistency with media store. */ export function getTelegramTempDir(): string { return TEMP_DIR; diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index 4ff7a6d96..8cccd2a30 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -42,23 +42,38 @@ function formatTimestamp(ts: number, config?: ReturnType): st /** * Convert ProviderMessage to MsgContext for auto-reply system. */ -function providerMessageToContext( +async function providerMessageToContext( message: ProviderMessage, config?: ReturnType, -): MsgContext { +): Promise { const timestampPrefix = formatTimestamp(message.timestamp, config); const bodyWithTimestamp = timestampPrefix ? `${timestampPrefix}${message.body}` : message.body; + // Handle media: if buffer exists but no URL, save the buffer and create a file path + let mediaUrl = message.media?.[0]?.url; + let mediaPath = message.media?.[0]?.fileName; + + if (!mediaUrl && message.media?.[0]?.buffer) { + // Save Telegram media buffer to disk + const { saveMediaBuffer } = await import("../media/store.js"); + const saved = await saveMediaBuffer( + message.media[0].buffer, + message.media[0].mimeType, + "telegram", + ); + mediaPath = saved.path; + } + return { Body: bodyWithTimestamp, From: message.from, To: message.to, MessageSid: message.id, - MediaUrl: message.media?.[0]?.url, + MediaUrl: mediaUrl, MediaType: message.media?.[0]?.mimeType, - MediaPath: message.media?.[0]?.fileName, + MediaPath: mediaPath, }; } @@ -144,7 +159,7 @@ async function handleInboundMessage( runtime: RuntimeEnv, ): Promise { const config = loadConfig(); - const ctx = providerMessageToContext(message, config); + const ctx = await providerMessageToContext(message, config); // Check allowFrom filter const allowFrom = config.inbound?.allowFrom; diff --git a/src/telegram/outbound.ts b/src/telegram/outbound.ts index 02a030dff..0de34d64d 100644 --- a/src/telegram/outbound.ts +++ b/src/telegram/outbound.ts @@ -39,7 +39,7 @@ export async function sendTextMessage( * * Media sources: * - Buffer: Sent directly from memory (existing behavior) - * - URL: Streamed to temp file (~/.warelay/telegram-temp), sent, then cleaned up + * - URL: Streamed to temp file (~/.clawdis/telegram-temp), sent, then cleaned up * * Streaming eliminates OOM risk for large files by avoiding memory buffering. * Temp files are automatically cleaned up after send (success or failure). diff --git a/src/utils.ts b/src/utils.ts index 05b6fcbab..5ce9555d2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,8 +25,19 @@ export function normalizeAllowFromEntry( if (!trimmed) return ""; if (provider === "telegram") { - // Telegram uses @username format - return trimmed.startsWith("@") ? trimmed : `@${trimmed}`; + // Strip telegram: prefix if present (allowFrom entries may or may not have it) + const withoutPrefix = trimmed.startsWith("telegram:") + ? trimmed.slice("telegram:".length) + : trimmed; + + // Ensure @username format, then add telegram: prefix + const username = withoutPrefix.startsWith("@") + ? withoutPrefix + : withoutPrefix.match(/^\d+$/) + ? withoutPrefix // numeric ID, keep as-is + : `@${withoutPrefix}`; + + return `telegram:${username}`; } // WhatsApp (both web and twilio) use E.164 phone numbers diff --git a/src/web/session.ts b/src/web/session.ts index a1d5d486f..ad6c42fae 100644 --- a/src/web/session.ts +++ b/src/web/session.ts @@ -232,6 +232,19 @@ export async function pickProvider(pref: Provider | "auto"): Promise { * Select providers for multi-provider relay. * Validates authentication and filters out unavailable providers. */ +/** + * Check if Twilio is configured via environment variables. + */ +function isTwilioConfigured(): boolean { + const required = ["TWILIO_ACCOUNT_SID", "TWILIO_WHATSAPP_FROM"]; + const hasRequired = required.every((k) => Boolean(process.env[k])); + const hasToken = Boolean(process.env.TWILIO_AUTH_TOKEN); + const hasKey = Boolean( + process.env.TWILIO_API_KEY && process.env.TWILIO_API_SECRET, + ); + return hasRequired && (hasToken || hasKey); +} + export async function selectProviders( prefs: (Provider | "auto")[], ): Promise { @@ -259,7 +272,14 @@ export async function selectProviders( ); } - // Note: twilio not heavily used in this branch + // Check twilio + if (isTwilioConfigured()) { + available.push("twilio"); + } else { + skipped.push( + "twilio (not configured - set TWILIO_ACCOUNT_SID, TWILIO_WHATSAPP_FROM, and auth credentials in .env)", + ); + } if (available.length > 0 && skipped.length > 0) { console.log( @@ -295,7 +315,13 @@ export async function selectProviders( ); } } else if (pref === "twilio") { - skipped.push("twilio (not heavily used in this branch)"); + if (isTwilioConfigured()) { + available.push("twilio"); + } else { + skipped.push( + "twilio (not configured - set TWILIO_ACCOUNT_SID, TWILIO_WHATSAPP_FROM, and auth credentials in .env)", + ); + } } }