Files
platform/src/servers/channels/routes.ts
T

375 lines
12 KiB
TypeScript

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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
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<string, unknown>) => {
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<string, unknown>;
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 });
});