telegram and whatsapp channel integrations, validate bot tokens before saving
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+270
-12
@@ -1,11 +1,16 @@
|
||||
import { createRouter } from '@@/create-router';
|
||||
import {
|
||||
getServerIntegration,
|
||||
upsertServerIntegration,
|
||||
getUserIntegration,
|
||||
deleteUserIntegration,
|
||||
} from 'officerdb';
|
||||
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();
|
||||
@@ -52,27 +57,36 @@ channelsRouter.put('/discord/config', async (ctx) => {
|
||||
if (serverInvite !== undefined) newConfig.serverInvite = serverInvite;
|
||||
if (botHandle !== undefined) newConfig.botHandle = botHandle;
|
||||
|
||||
await upsertServerIntegration('discord', newConfig, enabled ?? existing?.enabled ?? true);
|
||||
|
||||
// Restart bot if running or if we have a token and it's enabled
|
||||
const shouldRun = enabled ?? existing?.enabled ?? true;
|
||||
const token = (botToken ?? existingConfig.botToken) as string | undefined;
|
||||
|
||||
if (token && shouldRun) {
|
||||
// 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) });
|
||||
}
|
||||
return ctx.json({ success: true, botStarted: true });
|
||||
}
|
||||
|
||||
if (!shouldRun) {
|
||||
await stopDiscordBot();
|
||||
}
|
||||
|
||||
return ctx.json({ success: true, botStarted: false });
|
||||
return ctx.json({ success: true, botStarted: token && shouldRun });
|
||||
});
|
||||
|
||||
channelsRouter.get('/discord/status', async (ctx) => {
|
||||
@@ -114,3 +128,247 @@ channelsRouter.delete('/discord/connection', async (ctx) => {
|
||||
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 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user