feat: complete DM pairing codes with channel adapters, gateway handlers, and TUI command (Tier 4 feature 4)
This commit is contained in:
@@ -18,6 +18,7 @@ import type {
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
import type { PairingManager } from '../pairing.js';
|
||||
|
||||
/** Configuration for the Discord channel adapter. */
|
||||
export interface DiscordAdapterConfig {
|
||||
@@ -28,6 +29,8 @@ export interface DiscordAdapterConfig {
|
||||
allowedChannelIds?: string[];
|
||||
/** Whether to require mention to respond in guild channels (default: true). DMs always respond. */
|
||||
requireMention?: boolean;
|
||||
/** Optional pairing manager for DM pairing codes. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +197,20 @@ export class DiscordAdapter implements ChannelAdapter {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// DM pairing check — if pairing is enabled, require approval
|
||||
const pm = this.config.pairingManager;
|
||||
if (pm?.enabled && !pm.isApproved('discord', message.channelId)) {
|
||||
const text = message.content.trim();
|
||||
if (text && pm.validateCode('discord', message.channelId, text)) {
|
||||
try {
|
||||
if ('send' in message.channel) {
|
||||
(message.channel as any).send('Pairing successful! You can now chat with Flynn.');
|
||||
}
|
||||
} catch { /* ignore send errors */ }
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send typing indicator (lasts 10 seconds, no need for interval)
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
import type { PairingManager } from '../pairing.js';
|
||||
|
||||
/** Configuration for the Slack channel adapter. */
|
||||
export interface SlackAdapterConfig {
|
||||
@@ -26,6 +27,8 @@ export interface SlackAdapterConfig {
|
||||
allowedChannelIds?: string[];
|
||||
/** Require bot mention to respond (default: false). */
|
||||
requireMention?: boolean;
|
||||
/** Optional pairing manager for DM pairing codes. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
/** Minimal shape of a Slack message event from Bolt. */
|
||||
@@ -275,7 +278,32 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
this.config.allowedChannelIds.length > 0 &&
|
||||
!this.config.allowedChannelIds.includes(channelId)
|
||||
) {
|
||||
return;
|
||||
// Pairing fallback — check if the Slack user is approved or sending a valid code
|
||||
const pm = this.config.pairingManager;
|
||||
const userId = message.user;
|
||||
if (pm?.enabled && userId) {
|
||||
if (pm.isApproved('slack', userId)) {
|
||||
// Approved — fall through to normal message handling
|
||||
} else {
|
||||
const text = (message.text ?? '').trim();
|
||||
if (text && pm.validateCode('slack', userId, text)) {
|
||||
// Code validated — send confirmation via Slack
|
||||
if (this.app) {
|
||||
const threadTs = message.thread_ts ?? message.ts ?? '';
|
||||
try {
|
||||
await this.app.client.chat.postMessage({
|
||||
channel: channelId,
|
||||
text: 'Pairing successful! You can now chat with Flynn.',
|
||||
thread_ts: threadTs || undefined,
|
||||
});
|
||||
} catch { /* ignore send errors */ }
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Mention requirement
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
import { isAllowedChat } from '../../frontends/telegram/handlers.js';
|
||||
import { parseConfirmationCallback } from '../../frontends/telegram/confirmations.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
import type { PairingManager } from '../pairing.js';
|
||||
|
||||
/** Configuration for the Telegram channel adapter. */
|
||||
export interface TelegramAdapterConfig {
|
||||
@@ -20,6 +21,8 @@ export interface TelegramAdapterConfig {
|
||||
/** Require bot mention or reply-to-bot to respond in group chats (default: true). */
|
||||
requireMention?: boolean;
|
||||
hookEngine?: HookEngine;
|
||||
/** Optional pairing manager for DM pairing codes. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,14 +79,34 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
this.bot = new Bot(this.config.botToken);
|
||||
this._status = 'connecting';
|
||||
|
||||
// ── Auth middleware — reject messages from unknown chats ──
|
||||
// ── Auth middleware — reject messages from unknown chats (with pairing fallback) ──
|
||||
this.bot.use(async (ctx, next) => {
|
||||
const chatId = ctx.chat?.id;
|
||||
if (chatId === undefined || !isAllowedChat(chatId, this.config.allowedChatIds)) {
|
||||
console.log(`Rejected message from unauthorized chat: ${chatId}`);
|
||||
if (chatId === undefined) return;
|
||||
|
||||
// Allowlist check
|
||||
if (isAllowedChat(chatId, this.config.allowedChatIds)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
await next();
|
||||
|
||||
// Pairing fallback — check if sender is already approved or sending a valid code
|
||||
const pm = this.config.pairingManager;
|
||||
if (pm?.enabled) {
|
||||
const senderId = String(chatId);
|
||||
if (pm.isApproved('telegram', senderId)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
// Check if the message text is a valid pairing code
|
||||
const text = ctx.message?.text?.trim();
|
||||
if (text && pm.validateCode('telegram', senderId, text)) {
|
||||
await ctx.reply('Pairing successful! You can now chat with Flynn.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Rejected message from unauthorized chat: ${chatId}`);
|
||||
});
|
||||
|
||||
// ── Confirmation callback handler (requires hookEngine) ──
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
import { splitMessage } from '../utils.js';
|
||||
import type { PairingManager } from '../pairing.js';
|
||||
|
||||
/** Configuration for the WhatsApp channel adapter. */
|
||||
export interface WhatsAppAdapterConfig {
|
||||
@@ -29,6 +30,8 @@ export interface WhatsAppAdapterConfig {
|
||||
requireMention?: boolean;
|
||||
/** Directory for session persistence (LocalAuth data path). */
|
||||
dataDir?: string;
|
||||
/** Optional pairing manager for DM pairing codes. */
|
||||
pairingManager?: PairingManager;
|
||||
}
|
||||
|
||||
/** Minimal shape of a whatsapp-web.js message. */
|
||||
@@ -232,7 +235,26 @@ export class WhatsAppAdapter implements ChannelAdapter {
|
||||
this.config.allowedNumbers.length > 0 &&
|
||||
!this.config.allowedNumbers.includes(phoneNumber)
|
||||
) {
|
||||
return;
|
||||
// Pairing fallback — check if the sender is approved or sending a valid code
|
||||
const pm = this.config.pairingManager;
|
||||
if (pm?.enabled) {
|
||||
if (pm.isApproved('whatsapp', phoneNumber)) {
|
||||
// Approved — fall through to normal message handling
|
||||
} else {
|
||||
const text = (message.body ?? '').trim();
|
||||
if (text && pm.validateCode('whatsapp', phoneNumber, text)) {
|
||||
// Code validated — send confirmation via WhatsApp
|
||||
if (this.client) {
|
||||
try {
|
||||
await this.client.sendMessage(from, 'Pairing successful! You can now chat with Flynn.');
|
||||
} catch { /* ignore send errors */ }
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user