Files
lettabot/src/channels/types.ts
Ani Tunturi 18010eb14f feat: Matrix adapter with E2EE, TTS/STT, reactions, and heartbeat routing
Full Matrix channel integration for LettaBot:

- E2EE via rust crypto (ephemeral mode, cross-signing bootstrap)
- Proactive SAS verification with Element clients
- TTS (VibeVoice) and STT (Faster-Whisper) voice pipeline
- Streaming message edits with 800ms throttle
- Collapsible reasoning blocks via <details> htmlPrefix
- Per-tool emoji reactions (brain, eyes, tool-specific, max 6)
- Heartbeat room conversation routing (heartbeatTargetChatId)
- Custom heartbeat prompt with first-person voice
- Per-room conversation isolation (per-chat mode)
- !pause, !resume, !status, !new, !timeout, !turns commands
- Audio/image/file upload handlers with E2EE media
- SDK 0.1.11 (approval recovery), CLI 0.18.2

Tested against Synapse homeserver with E2EE enabled for 2+ weeks,
handles key backup/restore and device verification.
2026-03-14 21:27:32 -04:00

77 lines
2.5 KiB
TypeScript

/**
* Channel Adapter Interface
*
* Each channel (Telegram, Slack, Discord, WhatsApp, Signal) implements this interface.
*/
import type { InboundMessage, OutboundMessage, OutboundFile, FormatterHints } from '../core/types.js';
import type { ChannelId } from './setup.js';
/**
* Channel adapter - implement this for each messaging platform
*/
export interface ChannelAdapter {
readonly id: ChannelId;
readonly name: string;
// Lifecycle
start(): Promise<void>;
stop(): Promise<void>;
isRunning(): boolean;
// Messaging
sendMessage(msg: OutboundMessage): Promise<{ messageId: string }>;
editMessage(chatId: string, messageId: string, text: string, htmlPrefix?: string): Promise<void>;
sendTypingIndicator(chatId: string): Promise<void>;
stopTypingIndicator?(chatId: string): Promise<void>;
// Capabilities (optional)
supportsEditing?(): boolean;
sendFile?(file: OutboundFile): Promise<{ messageId: string }>;
sendAudio?(chatId: string, text: string): Promise<void>;
addReaction?(chatId: string, messageId: string, emoji: string): Promise<void>;
removeReaction?(chatId: string, messageId: string, emoji: string): Promise<void>;
/** Called after a bot message is sent (for TTS mapping, etc.) */
onMessageSent?(chatId: string, messageId: string, stepId?: string): void;
/** Store text for TTS regeneration on 🎤 reaction */
storeAudioMessage?(messageId: string, conversationId: string, roomId: string, text: string): void;
getDmPolicy?(): string;
getFormatterHints(): FormatterHints;
// Event handlers (set by bot core)
onMessage?: (msg: InboundMessage) => Promise<void>;
onCommand?: (command: string, chatId?: string, args?: string, forcePerChat?: boolean) => Promise<string | null>;
onInvalidateSession?: (key?: string) => void;
}
/**
* Typing heartbeat helper - keeps "typing..." indicator active
*/
export class TypingHeartbeat {
private interval: NodeJS.Timeout | null = null;
private adapter: ChannelAdapter | null = null;
private chatId: string | null = null;
start(adapter: ChannelAdapter, chatId: string): void {
this.stop();
this.adapter = adapter;
this.chatId = chatId;
const sendTyping = () => {
this.adapter?.sendTypingIndicator(this.chatId!).catch(() => {});
};
sendTyping();
this.interval = setInterval(sendTyping, 4000); // Most platforms expire typing after 5s
}
stop(): void {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
this.adapter = null;
this.chatId = null;
}
}