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)
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<Provider> {
| **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

View File

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

View File

@ -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);

View File

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

View File

@ -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;

View File

@ -42,23 +42,38 @@ function formatTimestamp(ts: number, config?: ReturnType<typeof loadConfig>): st
/**
* Convert ProviderMessage to MsgContext for auto-reply system.
*/
function providerMessageToContext(
async function providerMessageToContext(
message: ProviderMessage,
config?: ReturnType<typeof loadConfig>,
): MsgContext {
): Promise<MsgContext> {
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<void> {
const config = loadConfig();
const ctx = providerMessageToContext(message, config);
const ctx = await providerMessageToContext(message, config);
// Check allowFrom filter
const allowFrom = config.inbound?.allowFrom;

View File

@ -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).

View File

@ -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

View File

@ -232,6 +232,19 @@ export async function pickProvider(pref: Provider | "auto"): Promise<Provider> {
* 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<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) {
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)",
);
}
}
}