File size: 4,989 Bytes
7d8aa82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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<string, any> = {}
      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<string> {
  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<string, number> }
    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`)
})