375 lines
12 KiB
TypeScript
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 });
|
|
});
|