import express, { Request, Response } from 'express' import { InteractionType, InteractionResponseType, verifyKey } from 'discord-interactions' import { GuildMember } from 'discord.js' import { handleAsk } from './commands/ask' import { handlePool } from './commands/pool' import { handleTier } from './commands/tier' import { handleAgents, handleStatus, handleSeal, handleAudit, handleMerkle } from './commands/info' import { startGateway, handleVerify } from './onboarding' const app = express() const PORT = parseInt(process.env.PORT ?? '3001', 10) app.use(express.raw({ type: 'application/json' })) // Start Discord gateway for member events + button interactions const gatewayClient = startGateway() app.get('/health', (_req, res) => { res.json({ status: 'online', agents: 11, bot: 'snapkitty', gateway: gatewayClient ? 'connected' : 'disabled', }) }) app.post('/interactions', async (req: Request, res: Response) => { const signature = req.headers['x-signature-ed25519'] as string const timestamp = req.headers['x-signature-timestamp'] as string const publicKey = process.env.DISCORD_PUBLIC_KEY if (!publicKey) { res.status(500).json({ error: 'DISCORD_PUBLIC_KEY not configured' }) return } const rawBody = req.body as Buffer const isValid = await verifyKey(rawBody, signature, timestamp, publicKey) if (!isValid) { res.status(401).end('Invalid signature') return } const body = JSON.parse(rawBody.toString()) if (body.type === InteractionType.PING) { res.json({ type: InteractionResponseType.PONG }) return } if (body.type === InteractionType.APPLICATION_COMMAND) { const { name, options = [] } = body.data const getOpt = (key: string) => options.find((o: any) => o.name === key)?.value let content = '' if (name === 'ask') { content = await handleAsk(getOpt('agent') || 'bridge', getOpt('message') || '') } else if (name === 'pool') { const sub = options[0] const subOpts: Record = {} for (const o of sub?.options ?? []) subOpts[o.name] = o.value content = await handlePool(sub?.name || '', subOpts) } else if (name === 'verify') { const githubUsername = getOpt('github-username') || '' if (!githubUsername) { content = 'GitHub username required. Use `/verify github-username:YourHandle`' } else if (!gatewayClient) { content = 'Gateway not connected — bot token required for role assignment.' } else { const guild = gatewayClient.guilds.cache.first() const member = guild ? await guild.members.fetch(body.member?.user?.id).catch(() => null) : null if (!member) { content = 'Could not find your server membership. Try again in a moment.' } else { content = await handleVerify( body.member.user.id, `${body.member.user.username}#${body.member.user.discriminator}`, githubUsername, member as GuildMember ) } } } else if (name === 'community') { content = await handleCommunity() } else if (name === 'tier') { content = await handleTier() } else if (name === 'agents') { content = await handleAgents() } else if (name === 'status') { content = await handleStatus() } else if (name === 'seal') { content = await handleSeal() } else if (name === 'audit') { content = await handleAudit() } else if (name === 'merkle') { content = await handleMerkle() } else { content = `Unknown command: ${name}. Try \`/agents\` for the full directory.` } if (content.length > 1990) content = content.slice(0, 1987) + '…' res.json({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: { content }, }) return } res.status(400).json({ error: 'Unknown interaction type' }) }) async function handleCommunity(): Promise { const base = process.env.SNAPKITTY_API_URL ?? 'http://localhost:3000' try { const res = await fetch(`${base}/api/community/members`) const data = await res.json() as { total: number; verified: number; roles: Record } const roleLines = Object.entries(data.roles) .map(([role, count]) => ` ${role.padEnd(22)} ${count}`) .join('\n') return `**SnapKitty Community**\n\`\`\`\nTotal members: ${data.total}\nVerified devs: ${data.verified}\n\nBy role:\n${roleLines}\n\nJoin: discord.gg/dugymT3rj\n\`\`\`` } catch { return '**SnapKitty Community**\n```\nCommunity hub at collectivekitty.com/community\nJoin: discord.gg/dugymT3rj\n```' } } app.listen(PORT, () => { console.log(`SnapKitty Bot online — port ${PORT}`) console.log(`Interactions: POST http://localhost:${PORT}/interactions`) console.log(`Health: GET http://localhost:${PORT}/health`) })