From b673632b0f11f16e61cdc4f846d5e4168dbde39f Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 10 Feb 2026 09:32:52 -0800 Subject: [PATCH] feat(setup): add channel setup flows Implement setupChannels function with support for Telegram, Discord, Slack, and WhatsApp. Includes WebChat gateway configuration and channel choice loop. Co-Authored-By: Claude Opus 4.6 --- src/cli/setup/channels.test.ts | 61 ++++++++++++++++++++++++ src/cli/setup/channels.ts | 86 ++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 src/cli/setup/channels.test.ts create mode 100644 src/cli/setup/channels.ts diff --git a/src/cli/setup/channels.test.ts b/src/cli/setup/channels.test.ts new file mode 100644 index 0000000..be31b27 --- /dev/null +++ b/src/cli/setup/channels.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { EventEmitter } from 'events'; +import { createPrompter } from './prompts.js'; +import { ConfigBuilder } from './config.js'; +import { setupChannels } from './channels.js'; + +function mockReadline(inputs: string[]) { + let questionIdx = 0; + const emitter = new EventEmitter(); + + return { + async question(query: string) { + const answer = inputs[questionIdx++]; + return answer ?? ''; + }, + + close() { + // no-op + }, + + [Symbol.asyncIterator]() { + return this; + }, + + async next() { + return { done: true }; + }, + } as any; +} + +describe('setupChannels', () => { + it('configures webchat only (default)', async () => { + const rl = mockReadline(['', 'n']); + const p = createPrompter(rl); + const builder = new ConfigBuilder(); + await setupChannels(p, builder); + const config = builder.build(); + expect(config.server.port).toBeDefined(); + expect(config.telegram).toBeUndefined(); + }); + + it('configures telegram channel', async () => { + const rl = mockReadline(['', 'y', '1', '123:ABC', '12345, 67890', 'n']); + const p = createPrompter(rl); + const builder = new ConfigBuilder(); + await setupChannels(p, builder); + const config = builder.build(); + expect(config.telegram.bot_token).toBe('123:ABC'); + expect(config.telegram.allowed_chat_ids).toEqual([12345, 67890]); + }); + + it('configures discord channel', async () => { + const rl = mockReadline(['', 'y', '2', 'MTIz.token', 'guild1, guild2', 'n']); + const p = createPrompter(rl); + const builder = new ConfigBuilder(); + await setupChannels(p, builder); + const config = builder.build(); + expect(config.discord.bot_token).toBe('MTIz.token'); + expect(config.discord.allowed_guild_ids).toEqual(['guild1', 'guild2']); + }); +}); diff --git a/src/cli/setup/channels.ts b/src/cli/setup/channels.ts new file mode 100644 index 0000000..58d79c5 --- /dev/null +++ b/src/cli/setup/channels.ts @@ -0,0 +1,86 @@ +import type { Prompter } from './prompts.js'; +import type { ConfigBuilder } from './config.js'; + +async function setupTelegram(p: Prompter, builder: ConfigBuilder): Promise { + const botToken = await p.password('Bot token (from @BotFather)'); + const chatIdsRaw = await p.ask('Allowed chat IDs (comma-separated)'); + const chatIds = chatIdsRaw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n)); + if (chatIds.length === 0) { + p.println('No valid chat IDs entered. Skipping Telegram.'); + return; + } + builder.setTelegram(botToken, chatIds); + p.println('✓ Telegram configured'); +} + +async function setupDiscord(p: Prompter, builder: ConfigBuilder): Promise { + const botToken = await p.password('Bot token'); + const guildIdsRaw = await p.ask('Allowed guild IDs (comma-separated, or * for all)'); + const guildIds = guildIdsRaw === '*' ? [] : guildIdsRaw.split(',').map(s => s.trim()).filter(Boolean); + builder.setDiscord(botToken, guildIds); + p.println('✓ Discord configured'); +} + +async function setupSlack(p: Prompter, builder: ConfigBuilder): Promise { + const botToken = await p.password('Bot token (xoxb-...)'); + const appToken = await p.password('App token (xapp-...)'); + const signingSecret = await p.password('Signing secret'); + const channelIdsRaw = await p.ask('Allowed channel IDs (comma-separated, or * for all)'); + const channelIds = channelIdsRaw === '*' ? [] : channelIdsRaw.split(',').map(s => s.trim()).filter(Boolean); + builder.setSlack(botToken, appToken, signingSecret, channelIds); + p.println('✓ Slack configured'); +} + +async function setupWhatsApp(p: Prompter, builder: ConfigBuilder): Promise { + p.println('⚠ WhatsApp requires QR code authentication on first connect.'); + p.println(' It will appear in the terminal when Flynn starts.'); + const numbersRaw = await p.ask('Allowed phone numbers (comma-separated, or * for all)'); + const numbers = numbersRaw === '*' ? [] : numbersRaw.split(',').map(s => s.trim()).filter(Boolean); + builder.setWhatsApp(numbers); + p.println('✓ WhatsApp configured'); +} + +const CHANNEL_OPTIONS = [ + { label: 'Telegram', value: 'telegram' as const }, + { label: 'Discord', value: 'discord' as const }, + { label: 'More channels...', value: 'more' as const }, +]; + +const MORE_CHANNEL_OPTIONS = [ + { label: 'Slack', value: 'slack' as const }, + { label: 'WhatsApp', value: 'whatsapp' as const }, +]; + +const CHANNEL_SETUP: Record Promise> = { + telegram: setupTelegram, + discord: setupDiscord, + slack: setupSlack, + whatsapp: setupWhatsApp, +}; + +export async function setupChannels(p: Prompter, builder: ConfigBuilder): Promise { + p.println(); + p.println('WebChat is enabled by default via the gateway.'); + const port = await p.ask('Gateway port', '18800'); + builder.setGatewayPort(parseInt(port, 10) || 18800); + p.println('✓ WebChat enabled — visit http://localhost:' + port + ' after starting'); + + let addMore = await p.confirm('Add a messaging channel?', false); + + while (addMore) { + p.println(); + const choice = await p.choose('Channel:', CHANNEL_OPTIONS); + + if (choice === 'more') { + const moreChoice = await p.choose('Channel:', MORE_CHANNEL_OPTIONS); + const setup = CHANNEL_SETUP[moreChoice]; + if (setup) await setup(p, builder); + } else { + const setup = CHANNEL_SETUP[choice]; + if (setup) await setup(p, builder); + } + + p.println(); + addMore = await p.confirm('Add another channel?', false); + } +}