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.
This commit is contained in:
Arne Moor 2025-12-06 03:26:23 +01:00
parent 6e9f615a95
commit 4ac19117c1
9 changed files with 155 additions and 95 deletions

View File

@ -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) 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 2. **Same security model**: `allowFrom` whitelist controls who can trigger auto-replies
3. **Provider abstraction**: Unified interface across Twilio, Web, and Telegram providers 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 ```mermaid
flowchart LR flowchart LR
subgraph Warelay["warelay CLI (Container)"] subgraph Warelay["clawdis CLI (Container)"]
CLI[[CLI / Commander]] CLI[[CLI / Commander]]
Send[[Send Command]] Send[[Send Command]]
Relay[[Relay Command]] Relay[[Relay Command]]
@ -57,7 +57,7 @@ flowchart LR
WhatsApp -->|Baileys| IDP 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` **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<Provider> {
| **Auth Model** | QR code scan | Phone + code + 2FA | | **Auth Model** | QR code scan | Phone + code + 2FA |
| **Connection Type** | Persistent WebSocket | Persistent TCP/WebSocket | | **Connection Type** | Persistent WebSocket | Persistent TCP/WebSocket |
| **Library** | Baileys | GramJS | | **Library** | Baileys | GramJS |
| **Session Storage** | `~/.warelay/credentials/` | `~/.warelay/telegram/session/` | | **Session Storage** | `~/.clawdis/credentials/` | `~/.clawdis/telegram/session/` |
| **Message Send** | Socket message | MTProto request | | **Message Send** | Socket message | MTProto request |
| **Inbound Handling** | Event listener | Event listener | | **Inbound Handling** | Event listener | Event listener |
| **Delivery Status** | Limited (receipts) | Full (receipts + read) | | **Delivery Status** | Limited (receipts) | Full (receipts + read) |
@ -189,7 +189,7 @@ sequenceDiagram
**Key characteristics:** **Key characteristics:**
- Persistent connection with session state - 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 - Media sent as buffers with automatic optimization
- IPC server for relay mode to prevent session corruption - IPC server for relay mode to prevent session corruption
@ -216,7 +216,7 @@ sequenceDiagram
**Key characteristics:** **Key characteristics:**
- Persistent connection with session state (like WhatsApp Web) - 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 - Media sent as buffers
- Same patterns as Baileys for session management - Same patterns as Baileys for session management
@ -276,7 +276,7 @@ flowchart TB
end end
subgraph Storage["Local Storage"] subgraph Storage["Local Storage"]
SessionFile[("~/.warelay/telegram/session/")] SessionFile[("~/.clawdis/telegram/session/")]
end end
Login -->|phone + code| Client Login -->|phone + code| Client
@ -640,7 +640,7 @@ TELEGRAM_API_ID=12345678
TELEGRAM_API_HASH=0123456789abcdef0123456789abcdef TELEGRAM_API_HASH=0123456789abcdef0123456789abcdef
``` ```
### 5.2 Extended warelay.json Schema ### 5.2 Extended clawdis.json Schema
```typescript ```typescript
// Extended WarelayConfig type // 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 ```json5
{ {
"logging": { "logging": {
"level": "info", "level": "info",
"file": "~/.warelay/logs/warelay.log" "file": "~/.clawdis/logs/clawdis.log"
}, },
// Telegram-specific settings // Telegram-specific settings
@ -979,28 +979,14 @@ warelay relay --provider telegram --verbose
**Context:** Need to store Telegram session like WhatsApp Web credentials. **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:** **Consequences:**
- Consistent with `~/.warelay/credentials/` for WhatsApp - Consistent with `~/.clawdis/credentials/` for WhatsApp
- User-specific storage - User-specific storage
- Easy to backup/restore - Easy to backup/restore
- Clear separation between providers - 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 ## Appendix A: File Evidence Index

View File

@ -33,24 +33,26 @@ You'll be prompted for:
2. **SMS verification code** (sent to your Telegram app or SMS) 2. **SMS verification code** (sent to your Telegram app or SMS)
3. **2FA password** (if you have two-factor authentication enabled) 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) ### 4. Configure Whitelist (Optional)
In `~/.warelay/warelay.json`: In `~/.clawdis/clawdis.json` (or `~/.warelay/warelay.json` for legacy):
```json5 ```json5
{ {
telegram: { inbound: {
// Only these users can trigger auto-replies // Only these users can trigger auto-replies (works for both Telegram and WhatsApp)
allowFrom: [ allowFrom: [
"@username", // Telegram username (with @) "telegram:@username", // Telegram username (with telegram: prefix)
"+1234567890", // Phone number (with +) "telegram:123456789", // Telegram user ID (numeric)
"123456789" // 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. **Security note:** If `allowFrom` is empty or omitted, all incoming messages will trigger auto-replies. Use a whitelist in production.
## CLI Usage ## CLI Usage
@ -113,7 +115,7 @@ Shows recent sent/received messages with delivery status.
warelay logout --provider telegram 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 ## Features
@ -150,21 +152,22 @@ Control who can trigger auto-replies via `allowFrom` config:
```json5 ```json5
{ {
telegram: { inbound: {
allowFrom: ["@alice", "@bob", "123456789"] allowFrom: ["telegram:@alice", "telegram:@bob", "telegram:123456789"]
} }
} }
``` ```
- **Username** (`@alice`): Match by Telegram username - **Username** (`telegram:@alice`): Match by Telegram username (requires `telegram:` prefix)
- **Phone number** (`+15551234567`): Match by phone number - **User ID** (`telegram:123456789`): Match by numeric Telegram user ID (requires `telegram:` prefix)
- **User ID** (`123456789`): Match by numeric Telegram user ID
**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). If `allowFrom` is empty or omitted, **all messages trigger auto-replies** (use with caution).
### Session Storage ### 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 - Contains authentication tokens and keys
- Persists across restarts - Persists across restarts
- Should be treated as sensitive (equivalent to login credentials) - Should be treated as sensitive (equivalent to login credentials)
@ -236,7 +239,7 @@ warelay login --provider telegram
**Solution:** **Solution:**
```bash ```bash
# Remove corrupted session # Remove corrupted session
rm -rf ~/.warelay/telegram/session/ rm -rf ~/.clawdis/telegram/session/
# Re-login # Re-login
warelay login --provider telegram warelay login --provider telegram
@ -248,10 +251,8 @@ warelay login --provider telegram
```json5 ```json5
{ {
telegram: {
allowFrom: ["@alice", "@bob"]
},
inbound: { inbound: {
allowFrom: ["telegram:@alice", "telegram:@bob"],
reply: { reply: {
mode: "text", mode: "text",
text: "Thanks for your message! I'll get back to you soon." text: "Thanks for your message! I'll get back to you soon."
@ -264,10 +265,8 @@ warelay login --provider telegram
```json5 ```json5
{ {
telegram: {
allowFrom: ["@alice", "+15551234567"]
},
inbound: { inbound: {
allowFrom: ["telegram:@alice", "+15551234567"],
reply: { reply: {
mode: "command", mode: "command",
bodyPrefix: "You are a helpful assistant on Telegram. Be concise.\n\n", bodyPrefix: "You are a helpful assistant on Telegram. Be concise.\n\n",
@ -287,10 +286,8 @@ warelay login --provider telegram
```json5 ```json5
{ {
telegram: {
allowFrom: ["@alice", "@bob", "@charlie"]
},
inbound: { inbound: {
allowFrom: ["telegram:@alice", "telegram:@bob", "telegram:@charlie"],
reply: { reply: {
mode: "command", mode: "command",
command: ["claude", "{{BodyStripped}}"], command: ["claude", "{{BodyStripped}}"],
@ -347,10 +344,10 @@ Telegram has rate limits for personal accounts:
Session files contain authentication tokens: Session files contain authentication tokens:
```bash ```bash
# Backup # Backup
cp -r ~/.warelay/telegram/session/ ~/backups/warelay-telegram-session/ cp -r ~/.clawdis/telegram/session/ ~/backups/clawdis-telegram-session/
# Restore # Restore
cp -r ~/backups/warelay-telegram-session/ ~/.warelay/telegram/session/ cp -r ~/backups/clawdis-telegram-session/ ~/.clawdis/telegram/session/
``` ```
### 4. Monitor Logs ### 4. Monitor Logs
@ -391,14 +388,7 @@ tmux new -s warelay-telegram -d "warelay relay --provider telegram --verbose"
### Custom Session Storage ### Custom Session Storage
Override session path in config: 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.
```json5
{
telegram: {
sessionPath: "/custom/path/to/session/"
}
}
```
### Verbose Output ### Verbose Output
@ -428,7 +418,7 @@ Output includes:
Media downloads use streaming to temporary files, eliminating memory buffering: 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 - No memory spike regardless of file size
- Automatic cleanup after send (success or failure) - Automatic cleanup after send (success or failure)
- Orphaned files cleaned on process restart (1 hour TTL) - Orphaned files cleaned on process restart (1 hour TTL)
@ -491,7 +481,7 @@ If you encounter issues:
1. **Check logs:** Run with `--verbose` flag 1. **Check logs:** Run with `--verbose` flag
2. **Verify credentials:** Ensure API ID/Hash are correct 2. **Verify credentials:** Ensure API ID/Hash are correct
3. **Test login:** Try `warelay login --provider telegram` manually 3. **Test login:** Try `warelay login --provider telegram` manually
4. **Check session:** Verify `~/.warelay/telegram/session/` exists and is readable 4. **Check session:** Verify `~/.clawdis/telegram/session/` (or `~/.warelay/telegram/session/` for legacy) exists and is readable
5. **Review config:** Ensure `~/.warelay/warelay.json` is valid JSON5 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. For bugs or feature requests, file an issue on GitHub.

View File

@ -27,11 +27,42 @@ import { ensureTwilioEnv } from "../env.js";
import { runCommandWithTimeout } from "../process/exec.js"; import { runCommandWithTimeout } from "../process/exec.js";
import { pickProvider } from "../provider-web.js"; import { pickProvider } from "../provider-web.js";
import { createInitializedProvider } from "../providers/factory.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 { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import type { Provider } from "../utils.js"; import type { Provider } from "../utils.js";
import { sendViaIpc } from "../web/ipc.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 = { type AgentCommandOpts = {
message: string; message: string;
to?: string; to?: string;
@ -413,21 +444,27 @@ export async function agentCommand(
}; };
const telegramProvider = const telegramProvider =
await createInitializedProvider("telegram", telegramConfig); await createInitializedProvider("telegram", telegramConfig);
const chunks = chunkText(text, 4096);
if (chunks.length > 0 || media.length > 0) { try {
const firstChunk = chunks.length > 0 ? chunks[0] : ""; const chunks = chunkText(text, 4096);
const firstMedia = media[0]; if (chunks.length > 0 || media.length > 0) {
await telegramProvider.send(opts.to, firstChunk, { const firstChunk = chunks.length > 0 ? chunks[0] : "";
media: firstMedia ? [{ type: "image", url: firstMedia }] : undefined, 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 < chunks.length; i++) {
for (let i = 1; i < media.length; i++) { await telegramProvider.send(opts.to, chunks[i]);
await telegramProvider.send(opts.to, "", { }
media: [{ type: "image", url: media[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 { } else {
const chunks = chunkText(text, 1600); const chunks = chunkText(text, 1600);

View File

@ -233,11 +233,6 @@ const WarelaySchema = z.object({
.optional(), .optional(),
}) })
.optional(), .optional(),
telegram: z
.object({
allowFrom: z.array(z.string()).optional(),
})
.optional(),
}); });
export function loadConfig(): WarelayConfig { export function loadConfig(): WarelayConfig {

View File

@ -42,7 +42,7 @@ export interface DownloadResult {
/** /**
* Get temp directory for Telegram downloads. * 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 { export function getTelegramTempDir(): string {
return TEMP_DIR; return TEMP_DIR;

View File

@ -42,23 +42,38 @@ function formatTimestamp(ts: number, config?: ReturnType<typeof loadConfig>): st
/** /**
* Convert ProviderMessage to MsgContext for auto-reply system. * Convert ProviderMessage to MsgContext for auto-reply system.
*/ */
function providerMessageToContext( async function providerMessageToContext(
message: ProviderMessage, message: ProviderMessage,
config?: ReturnType<typeof loadConfig>, config?: ReturnType<typeof loadConfig>,
): MsgContext { ): Promise<MsgContext> {
const timestampPrefix = formatTimestamp(message.timestamp, config); const timestampPrefix = formatTimestamp(message.timestamp, config);
const bodyWithTimestamp = timestampPrefix const bodyWithTimestamp = timestampPrefix
? `${timestampPrefix}${message.body}` ? `${timestampPrefix}${message.body}`
: 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 { return {
Body: bodyWithTimestamp, Body: bodyWithTimestamp,
From: message.from, From: message.from,
To: message.to, To: message.to,
MessageSid: message.id, MessageSid: message.id,
MediaUrl: message.media?.[0]?.url, MediaUrl: mediaUrl,
MediaType: message.media?.[0]?.mimeType, MediaType: message.media?.[0]?.mimeType,
MediaPath: message.media?.[0]?.fileName, MediaPath: mediaPath,
}; };
} }
@ -144,7 +159,7 @@ async function handleInboundMessage(
runtime: RuntimeEnv, runtime: RuntimeEnv,
): Promise<void> { ): Promise<void> {
const config = loadConfig(); const config = loadConfig();
const ctx = providerMessageToContext(message, config); const ctx = await providerMessageToContext(message, config);
// Check allowFrom filter // Check allowFrom filter
const allowFrom = config.inbound?.allowFrom; const allowFrom = config.inbound?.allowFrom;

View File

@ -39,7 +39,7 @@ export async function sendTextMessage(
* *
* Media sources: * Media sources:
* - Buffer: Sent directly from memory (existing behavior) * - 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. * Streaming eliminates OOM risk for large files by avoiding memory buffering.
* Temp files are automatically cleaned up after send (success or failure). * Temp files are automatically cleaned up after send (success or failure).

View File

@ -25,8 +25,19 @@ export function normalizeAllowFromEntry(
if (!trimmed) return ""; if (!trimmed) return "";
if (provider === "telegram") { if (provider === "telegram") {
// Telegram uses @username format // Strip telegram: prefix if present (allowFrom entries may or may not have it)
return trimmed.startsWith("@") ? trimmed : `@${trimmed}`; 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 // WhatsApp (both web and twilio) use E.164 phone numbers

View File

@ -232,6 +232,19 @@ export async function pickProvider(pref: Provider | "auto"): Promise<Provider> {
* Select providers for multi-provider relay. * Select providers for multi-provider relay.
* Validates authentication and filters out unavailable providers. * 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( export async function selectProviders(
prefs: (Provider | "auto")[], prefs: (Provider | "auto")[],
): Promise<Provider[]> { ): Promise<Provider[]> {
@ -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) { if (available.length > 0 && skipped.length > 0) {
console.log( console.log(
@ -295,7 +315,13 @@ export async function selectProviders(
); );
} }
} else if (pref === "twilio") { } 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)",
);
}
} }
} }