openclaw/src/telegram-gramjs/auth.ts
spiceoogway 84c1ab4d55 feat(telegram-gramjs): Phase 1 - User account adapter with tests and docs
Implements Telegram user account support via GramJS/MTProto (#937).

## What's New
- Complete GramJS channel adapter for user accounts (not bots)
- Interactive auth flow (phone → SMS → 2FA)
- Session persistence via StringSession
- DM and group message support
- Security policies (allowFrom, dmPolicy, groupPolicy)
- Multi-account configuration

## Files Added

### Core Implementation (src/telegram-gramjs/)
- auth.ts - Interactive authentication flow
- auth.test.ts - Auth flow tests (mocked)
- client.ts - GramJS TelegramClient wrapper
- config.ts - Config adapter for multi-account
- gateway.ts - Gateway adapter (poll/send)
- handlers.ts - Message conversion (GramJS → openclaw)
- handlers.test.ts - Message conversion tests
- setup.ts - CLI setup wizard
- types.ts - TypeScript type definitions
- index.ts - Module exports

### Configuration
- src/config/types.telegram-gramjs.ts - Config schema

### Plugin Extension
- extensions/telegram-gramjs/index.ts - Plugin registration
- extensions/telegram-gramjs/src/channel.ts - Channel plugin implementation
- extensions/telegram-gramjs/openclaw.plugin.json - Plugin manifest
- extensions/telegram-gramjs/package.json - Dependencies

### Documentation
- docs/channels/telegram-gramjs.md - Complete setup guide (14KB)
- GRAMJS-PHASE1-SUMMARY.md - Implementation summary

### Registry
- src/channels/registry.ts - Added telegram-gramjs to CHAT_CHANNEL_ORDER

## Test Coverage
-  Auth flow with phone/SMS/2FA (mocked)
-  Phone number validation
-  Session verification
-  Message conversion (DM, group, reply)
-  Session key routing
-  Command extraction
-  Edge cases (empty messages, special chars)

## Features Implemented (Phase 1)
-  User account authentication via MTProto
-  DM message send/receive
-  Group message send/receive
-  Reply context preservation
-  Security policies (pairing, allowlist)
-  Multi-account support
-  Session persistence
-  Command detection

## Next Steps (Phase 2)
- Media support (photos, videos, files)
- Voice messages and stickers
- Message editing and deletion
- Reactions
- Channel messages

## Documentation Highlights
- Getting API credentials from my.telegram.org
- Interactive setup wizard walkthrough
- DM and group policies configuration
- Multi-account examples
- Rate limits and troubleshooting
- Security best practices
- Migration guide from Bot API

Closes #937 (Phase 1)
2026-01-30 03:03:15 -05:00

199 lines
5.1 KiB
TypeScript

/**
* Authentication flow for Telegram GramJS user accounts.
*
* Handles interactive login via:
* 1. Phone number
* 2. SMS code
* 3. 2FA password (if enabled)
*
* Returns StringSession for persistence.
*/
import readline from "readline";
import { GramJSClient } from "./client.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import type { AuthState } from "./types.js";
const log = createSubsystemLogger("telegram-gramjs:auth");
/**
* Interactive authentication flow for CLI.
*/
export class AuthFlow {
private state: AuthState = { phase: "phone" };
private rl: readline.Interface;
constructor() {
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
}
/**
* Prompt user for input.
*/
private async prompt(question: string): Promise<string> {
return new Promise((resolve) => {
this.rl.question(question, (answer) => {
resolve(answer.trim());
});
});
}
/**
* Validate phone number format.
*/
private validatePhoneNumber(phone: string): boolean {
// Remove spaces and dashes
const cleaned = phone.replace(/[\s-]/g, "");
// Should start with + and contain only digits after
return /^\+\d{10,15}$/.test(cleaned);
}
/**
* Run the complete authentication flow.
*/
async authenticate(apiId: number, apiHash: string, sessionString?: string): Promise<string> {
try {
log.info("Starting Telegram authentication flow...");
log.info("You will need:");
log.info(" 1. Your phone number (format: +1234567890)");
log.info(" 2. Access to SMS for verification code");
log.info(" 3. Your 2FA password (if enabled)");
log.info("");
const client = new GramJSClient({
apiId,
apiHash,
sessionString,
});
this.state.phase = "phone";
const phoneNumber = await this.promptPhoneNumber();
this.state.phoneNumber = phoneNumber;
this.state.phase = "code";
const finalSessionString = await client.startWithAuth({
phoneNumber: async () => phoneNumber,
phoneCode: async () => {
return await this.promptSmsCode();
},
password: async () => {
return await this.prompt2faPassword();
},
onError: (err) => {
log.error("Authentication error:", err.message);
this.state.phase = "error";
this.state.error = err.message;
},
});
this.state.phase = "complete";
await client.disconnect();
this.rl.close();
log.success("✅ Authentication successful!");
log.info("Session string generated. This will be saved to your config.");
log.info("");
return finalSessionString;
} catch (err) {
this.state.phase = "error";
this.state.error = err instanceof Error ? err.message : String(err);
this.rl.close();
throw err;
}
}
/**
* Prompt for phone number with validation.
*/
private async promptPhoneNumber(): Promise<string> {
while (true) {
const phone = await this.prompt("Enter your phone number (format: +1234567890): ");
if (this.validatePhoneNumber(phone)) {
return phone;
}
log.error("❌ Invalid phone number format. Must start with + and contain 10-15 digits.");
log.info("Example: +12025551234");
}
}
/**
* Prompt for SMS verification code.
*/
private async promptSmsCode(): Promise<string> {
log.info("📱 A verification code has been sent to your phone via SMS.");
const code = await this.prompt("Enter the verification code: ");
return code.replace(/[\s-]/g, ""); // Remove spaces/dashes
}
/**
* Prompt for 2FA password (if enabled).
*/
private async prompt2faPassword(): Promise<string> {
log.info("🔒 Your account has Two-Factor Authentication enabled.");
const password = await this.prompt("Enter your 2FA password: ");
return password;
}
/**
* Get current authentication state.
*/
getState(): AuthState {
return { ...this.state };
}
/**
* Non-interactive authentication (for programmatic use).
* Throws if user interaction is required.
*/
static async authenticateNonInteractive(
apiId: number,
apiHash: string,
sessionString: string,
): Promise<boolean> {
const client = new GramJSClient({
apiId,
apiHash,
sessionString,
});
try {
await client.connect();
const state = await client.getConnectionState();
await client.disconnect();
return state.authorized;
} catch (err) {
log.error("Non-interactive auth failed:", err);
return false;
}
}
}
/**
* Run interactive authentication flow (for CLI use).
*/
export async function runAuthFlow(
apiId: number,
apiHash: string,
sessionString?: string,
): Promise<string> {
const auth = new AuthFlow();
return await auth.authenticate(apiId, apiHash, sessionString);
}
/**
* Verify an existing session is still valid.
*/
export async function verifySession(
apiId: number,
apiHash: string,
sessionString: string,
): Promise<boolean> {
return await AuthFlow.authenticateNonInteractive(apiId, apiHash, sessionString);
}