import { createRouter } from '@@/create-router'; import { getServerIntegration, upsertServerIntegration, getUserIntegration, deleteUserIntegration } from 'officerdb'; import { startDiscordBot, stopDiscordBot, isDiscordBotRunning, getDiscordBotUsername } from './discord/bot'; import { startTelegramBot, stopTelegramBot, isTelegramBotRunning, getTelegramBotUsername } from './telegram/bot'; import { startWhatsAppBot, stopWhatsAppBot, isWhatsAppBotRunning, getWhatsAppBotPhone, getWhatsAppQR, subscribeQR, disconnectWhatsApp, } from './whatsapp/bot'; import { generatePairingCode } from './pairing'; export const channelsRouter = createRouter(); // ── Admin: Discord config ── channelsRouter.get('/discord/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') return ctx.json({ error: 'Forbidden' }, 403); const integration = await getServerIntegration('discord'); if (!integration) return ctx.json({ configured: false }); const config = integration.config as Record; const botToken = config.botToken as string | undefined; return ctx.json({ configured: !!botToken, enabled: integration.enabled, botToken: botToken ? `${botToken.slice(0, 8)}...${botToken.slice(-4)}` : null, serverInvite: (config.serverInvite as string) ?? null, botHandle: (config.botHandle as string) ?? null, }); }); channelsRouter.put('/discord/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') return ctx.json({ error: 'Forbidden' }, 403); const body = ctx.get('body') as Record; const botToken = body.botToken as string | undefined; const enabled = body.enabled as boolean | undefined; const serverInvite = body.serverInvite as string | undefined; const botHandle = body.botHandle as string | undefined; if (!botToken && enabled === undefined && serverInvite === undefined && botHandle === undefined) { return ctx.json({ error: 'At least one field required' }, 400); } const existing = await getServerIntegration('discord'); const existingConfig = (existing?.config ?? {}) as Record; const newConfig = { ...existingConfig }; if (botToken) newConfig.botToken = botToken; if (serverInvite !== undefined) newConfig.serverInvite = serverInvite; if (botHandle !== undefined) newConfig.botHandle = botHandle; const shouldRun = enabled ?? existing?.enabled ?? true; const token = (botToken ?? existingConfig.botToken) as string | undefined; // If a new token is provided, validate it by starting the bot before saving if (botToken && shouldRun) { try { await startDiscordBot(botToken); } catch (err) { console.error('[channels] Discord bot token validation failed:', err); return ctx.json({ error: 'Invalid bot token — connection failed' }, 400); } } await upsertServerIntegration('discord', newConfig, shouldRun); // Start/restart with existing token (already validated on initial save) if (!botToken && token && shouldRun) { try { await startDiscordBot(token); } catch (err) { console.error('[channels] Failed to start Discord bot:', err); return ctx.json({ success: true, botStarted: false, error: String(err) }); } } if (!shouldRun) { await stopDiscordBot(); } return ctx.json({ success: true, botStarted: token && shouldRun }); }); channelsRouter.get('/discord/status', async (ctx) => { const integration = await getServerIntegration('discord'); const config = (integration?.config ?? {}) as Record; return ctx.json({ configured: !!config.botToken, enabled: integration?.enabled ?? false, running: isDiscordBotRunning(), botUsername: getDiscordBotUsername(), serverInvite: (config.serverInvite as string) ?? null, botHandle: (config.botHandle as string) ?? null, }); }); // ── User: Discord pairing ── channelsRouter.post('/discord/pair', async (ctx) => { const user = ctx.get('user'); const code = generatePairingCode(user.id, user.email, 'discord'); return ctx.json({ code, expiresIn: 600 }); }); channelsRouter.get('/discord/connection', async (ctx) => { const user = ctx.get('user'); const integration = await getUserIntegration(user.id, 'discord'); if (!integration) return ctx.json({ linked: false }); const config = integration.config as Record; return ctx.json({ linked: true, discordId: config.discordId, }); }); channelsRouter.delete('/discord/connection', async (ctx) => { const user = ctx.get('user'); const deleted = await deleteUserIntegration(user.id, 'discord'); return ctx.json({ success: deleted }); }); // ── Admin: Telegram config ── channelsRouter.get('/telegram/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') return ctx.json({ error: 'Forbidden' }, 403); const integration = await getServerIntegration('telegram'); if (!integration) return ctx.json({ configured: false }); const config = integration.config as Record; const botToken = config.botToken as string | undefined; return ctx.json({ configured: !!botToken, enabled: integration.enabled, botToken: botToken ? `${botToken.slice(0, 8)}...${botToken.slice(-4)}` : null, serverInvite: (config.serverInvite as string) ?? null, botHandle: (config.botHandle as string) ?? null, }); }); channelsRouter.put('/telegram/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') return ctx.json({ error: 'Forbidden' }, 403); const body = ctx.get('body') as Record; const botToken = body.botToken as string | undefined; const enabled = body.enabled as boolean | undefined; const serverInvite = body.serverInvite as string | undefined; const botHandle = body.botHandle as string | undefined; if (!botToken && enabled === undefined && serverInvite === undefined && botHandle === undefined) { return ctx.json({ error: 'At least one field required' }, 400); } const existing = await getServerIntegration('telegram'); const existingConfig = (existing?.config ?? {}) as Record; const newConfig = { ...existingConfig }; if (botToken) newConfig.botToken = botToken; if (serverInvite !== undefined) newConfig.serverInvite = serverInvite; if (botHandle !== undefined) newConfig.botHandle = botHandle; const shouldRun = enabled ?? existing?.enabled ?? true; const token = (botToken ?? existingConfig.botToken) as string | undefined; // If a new token is provided, validate it by starting the bot before saving if (botToken && shouldRun) { try { await startTelegramBot(botToken); } catch (err) { console.error('[channels] Telegram bot token validation failed:', err); return ctx.json({ error: 'Invalid bot token — connection failed' }, 400); } } await upsertServerIntegration('telegram', newConfig, shouldRun); // Start/restart with existing token (already validated on initial save) if (!botToken && token && shouldRun) { try { await startTelegramBot(token); } catch (err) { console.error('[channels] Failed to start Telegram bot:', err); return ctx.json({ success: true, botStarted: false, error: String(err) }); } } if (!shouldRun) { await stopTelegramBot(); } return ctx.json({ success: true, botStarted: token && shouldRun }); }); channelsRouter.get('/telegram/status', async (ctx) => { const integration = await getServerIntegration('telegram'); const config = (integration?.config ?? {}) as Record; return ctx.json({ configured: !!config.botToken, enabled: integration?.enabled ?? false, running: isTelegramBotRunning(), botUsername: getTelegramBotUsername(), serverInvite: (config.serverInvite as string) ?? null, botHandle: (config.botHandle as string) ?? null, }); }); // ── User: Telegram pairing ── channelsRouter.post('/telegram/pair', async (ctx) => { const user = ctx.get('user'); const code = generatePairingCode(user.id, user.email, 'telegram'); return ctx.json({ code, expiresIn: 600 }); }); channelsRouter.get('/telegram/connection', async (ctx) => { const user = ctx.get('user'); const integration = await getUserIntegration(user.id, 'telegram'); if (!integration) return ctx.json({ linked: false }); const config = integration.config as Record; return ctx.json({ linked: true, telegramId: config.telegramId, }); }); channelsRouter.delete('/telegram/connection', async (ctx) => { const user = ctx.get('user'); const deleted = await deleteUserIntegration(user.id, 'telegram'); return ctx.json({ success: deleted }); }); // ── Admin: WhatsApp config ── channelsRouter.get('/whatsapp/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') return ctx.json({ error: 'Forbidden' }, 403); const integration = await getServerIntegration('whatsapp'); return ctx.json({ configured: integration?.enabled ?? false, enabled: integration?.enabled ?? false, running: isWhatsAppBotRunning(), phone: getWhatsAppBotPhone(), }); }); channelsRouter.put('/whatsapp/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') return ctx.json({ error: 'Forbidden' }, 403); const body = ctx.get('body') as Record; const enabled = body.enabled as boolean | undefined; if (enabled === true) { await upsertServerIntegration('whatsapp', {}, true); try { await startWhatsAppBot(); } catch (err) { console.error('[channels] Failed to start WhatsApp bot:', err); return ctx.json({ success: true, botStarted: false, error: String(err) }); } return ctx.json({ success: true, botStarted: true }); } if (enabled === false) { await disconnectWhatsApp(); return ctx.json({ success: true, botStarted: false }); } return ctx.json({ error: 'enabled field required' }, 400); }); channelsRouter.get('/whatsapp/status', async (ctx) => { const integration = await getServerIntegration('whatsapp'); return ctx.json({ configured: integration?.enabled ?? false, enabled: integration?.enabled ?? false, running: isWhatsAppBotRunning(), phone: getWhatsAppBotPhone(), }); }); channelsRouter.get('/whatsapp/qr', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') return ctx.json({ error: 'Forbidden' }, 403); // SSE stream for QR code updates return new Response( new ReadableStream({ start(controller) { const encoder = new TextEncoder(); const sendEvent = (data: Record) => { controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); }; // Send current QR if available const currentQR = getWhatsAppQR(); if (currentQR) { sendEvent({ type: 'qr', qr: currentQR }); } else if (isWhatsAppBotRunning()) { sendEvent({ type: 'authenticated', phone: getWhatsAppBotPhone() }); } else { sendEvent({ type: 'waiting' }); } const unsubscribe = subscribeQR((qr, event) => { if (event === 'qr' && qr) { sendEvent({ type: 'qr', qr }); } else if (event === 'authenticated') { sendEvent({ type: 'authenticated', phone: getWhatsAppBotPhone() }); } else if (event === 'disconnected') { sendEvent({ type: 'disconnected' }); } }); // Clean up when client disconnects ctx.req.raw.signal.addEventListener('abort', () => { unsubscribe(); controller.close(); }); }, }), { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }, ); }); // ── User: WhatsApp pairing ── channelsRouter.post('/whatsapp/pair', async (ctx) => { const user = ctx.get('user'); const code = generatePairingCode(user.id, user.email, 'whatsapp'); return ctx.json({ code, expiresIn: 600 }); }); channelsRouter.get('/whatsapp/connection', async (ctx) => { const user = ctx.get('user'); const integration = await getUserIntegration(user.id, 'whatsapp'); if (!integration) return ctx.json({ linked: false }); const config = integration.config as Record; return ctx.json({ linked: true, whatsappId: config.whatsappId, }); }); channelsRouter.delete('/whatsapp/connection', async (ctx) => { const user = ctx.get('user'); const deleted = await deleteUserIntegration(user.id, 'whatsapp'); return ctx.json({ success: deleted }); });