Add Telegram as a third messaging provider alongside web and twilio. Core Features: - Interactive login flow with phone/SMS/2FA authentication - Send text and media messages (images, videos, audio, documents) - Monitor incoming messages with auto-reply support - Session management at ~/.clawdis/telegram/session/ - Full CLI integration (login, logout, status, send, relay commands) Implementation Details: - Uses telegram npm package for MTProto API access - Supports both URL and local file media sending - Cross-platform path handling (Windows/Unix) - Optional Twilio env vars (supports Telegram-only usage) - Minimal provider abstraction pattern - Comprehensive test coverage (440 tests passing) Changes: - Add Telegram module (client, login, monitor, inbound, outbound, session) - Add provider factory and base interfaces - Wire Telegram functions into CLI deps - Update env validation to make Twilio fields optional - Add telegram to all CLI commands (login, logout, status, send, relay) - Add null checks in Twilio code for optional env fields - Fix send command to properly load session and connect - Add local file support with cross-platform path handling - Update login message to show correct ~/.clawdis path - Add comprehensive tests and documentation Basic Usage: warelay login --provider telegram warelay send --provider telegram --to "@user" --message "Hi" warelay send --provider telegram --to "@user" --media "/path/to/file.jpg" warelay relay --provider telegram All tests pass (63 files, 440 tests). Zero TypeScript errors.
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { isVerbose, logVerbose } from "./globals.js";
|
|
|
|
export async function ensureDir(dir: string) {
|
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
}
|
|
|
|
export type Provider = "twilio" | "web" | "telegram";
|
|
|
|
export function assertProvider(input: string): asserts input is Provider {
|
|
if (input !== "twilio" && input !== "web" && input !== "telegram") {
|
|
throw new Error("Provider must be 'web', 'twilio', or 'telegram'");
|
|
}
|
|
}
|
|
|
|
export type AllowFromProvider = "telegram" | "web" | "twilio";
|
|
|
|
export function normalizeAllowFromEntry(
|
|
entry: string,
|
|
provider: AllowFromProvider,
|
|
): string {
|
|
const trimmed = entry.trim().toLowerCase();
|
|
if (!trimmed) return "";
|
|
|
|
if (provider === "telegram") {
|
|
// Telegram uses @username format
|
|
return trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
|
|
}
|
|
|
|
// WhatsApp (both web and twilio) use E.164 phone numbers
|
|
return normalizeE164(entry);
|
|
}
|
|
|
|
export function normalizePath(p: string): string {
|
|
if (!p.startsWith("/")) return `/${p}`;
|
|
return p;
|
|
}
|
|
|
|
export function withWhatsAppPrefix(number: string): string {
|
|
return number.startsWith("whatsapp:") ? number : `whatsapp:${number}`;
|
|
}
|
|
|
|
export function normalizeE164(number: string): string {
|
|
const withoutPrefix = number.replace(/^whatsapp:/, "").trim();
|
|
const digits = withoutPrefix.replace(/[^\d+]/g, "");
|
|
if (digits.startsWith("+")) return `+${digits.slice(1)}`;
|
|
return `+${digits}`;
|
|
}
|
|
|
|
export function toWhatsappJid(number: string): string {
|
|
const e164 = normalizeE164(number);
|
|
const digits = e164.replace(/\D/g, "");
|
|
return `${digits}@s.whatsapp.net`;
|
|
}
|
|
|
|
export function jidToE164(jid: string): string | null {
|
|
// Convert a WhatsApp JID (with optional device suffix, e.g. 1234:1@s.whatsapp.net) back to +1234.
|
|
const match = jid.match(/^(\d+)(?::\d+)?@s\.whatsapp\.net$/);
|
|
if (match) {
|
|
const digits = match[1];
|
|
return `+${digits}`;
|
|
}
|
|
|
|
// Support @lid format (WhatsApp Linked ID) - look up reverse mapping
|
|
const lidMatch = jid.match(/^(\d+)(?::\d+)?@lid$/);
|
|
if (lidMatch) {
|
|
const lid = lidMatch[1];
|
|
try {
|
|
const mappingPath = `${CONFIG_DIR}/credentials/lid-mapping-${lid}_reverse.json`;
|
|
const data = fs.readFileSync(mappingPath, "utf8");
|
|
const phone = JSON.parse(data);
|
|
if (phone) return `+${phone}`;
|
|
} catch {
|
|
if (isVerbose()) {
|
|
logVerbose(
|
|
`LID mapping not found for ${lid}; skipping inbound message`,
|
|
);
|
|
}
|
|
// Mapping not found, fall through
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function sleep(ms: number) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// Prefer new branding directory; fall back to legacy for compatibility.
|
|
export const CONFIG_DIR = (() => {
|
|
const clawdis = path.join(os.homedir(), ".clawdis");
|
|
const legacy = path.join(os.homedir(), ".warelay");
|
|
if (fs.existsSync(clawdis)) return clawdis;
|
|
return legacy;
|
|
})();
|