import { Hono } from 'hono' import { nanoid } from 'nanoid' import { createHash, randomBytes } from 'crypto' import { db, schema } from '../db/index.js' import { eq } from 'drizzle-orm' export const botsRouter = new Hono() function hashSecret(secret: string): string { return createHash('sha256').update(secret).digest('hex') } // Register a new bot botsRouter.post('/', async (c) => { const body = await c.req.json() const { name, webhook_url, avatar_seed } = body if (!name || typeof name !== 'string' || name.length < 2 || name.length > 32) { return c.json({ error: 'Name must be 2-32 characters.' }, 400) } if (!/^[a-zA-Z0-9_-]+$/.test(name)) { return c.json({ error: 'Name must be alphanumeric, hyphens, or underscores.' }, 400) } if (!webhook_url || typeof webhook_url !== 'string') { return c.json({ error: 'webhook_url is required.' }, 400) } try { new URL(webhook_url) } catch { return c.json({ error: 'webhook_url must be a valid URL.' }, 400) } // Check for duplicate name const existing = await db.select({ id: schema.bots.id }) .from(schema.bots) .where(eq(schema.bots.name, name)) .limit(1) if (existing.length > 0) { return c.json({ error: 'A bot with that name already exists.' }, 409) } const id = nanoid(12) const secret = randomBytes(32).toString('hex') await db.insert(schema.bots).values({ id, name, webhookUrl: webhook_url, avatarSeed: avatar_seed || name, secretHash: hashSecret(secret), createdAt: new Date().toISOString(), }) return c.json({ id, name, secret, message: 'Bot registered. Save your secret -- it will not be shown again.', }, 201) }) // List bots (public info only) botsRouter.get('/', async (c) => { const rows = await db.select({ id: schema.bots.id, name: schema.bots.name, avatarSeed: schema.bots.avatarSeed, eloRating: schema.bots.eloRating, wins: schema.bots.wins, losses: schema.bots.losses, winStreak: schema.bots.winStreak, bestStreak: schema.bots.bestStreak, tier: schema.bots.tier, isActive: schema.bots.isActive, createdAt: schema.bots.createdAt, }).from(schema.bots).orderBy(schema.bots.eloRating) return c.json(rows) }) // Get single bot profile botsRouter.get('/:name', async (c) => { const name = c.req.param('name') const rows = await db.select({ id: schema.bots.id, name: schema.bots.name, avatarSeed: schema.bots.avatarSeed, eloRating: schema.bots.eloRating, wins: schema.bots.wins, losses: schema.bots.losses, winStreak: schema.bots.winStreak, bestStreak: schema.bots.bestStreak, tier: schema.bots.tier, isActive: schema.bots.isActive, createdAt: schema.bots.createdAt, }).from(schema.bots).where(eq(schema.bots.name, name)).limit(1) if (rows.length === 0) { return c.json({ error: 'Bot not found.' }, 404) } return c.json(rows[0]) }) // Health check a bot's webhook botsRouter.post('/:name/health', async (c) => { const name = c.req.param('name') const rows = await db.select({ webhookUrl: schema.bots.webhookUrl, }).from(schema.bots).where(eq(schema.bots.name, name)).limit(1) if (rows.length === 0) { return c.json({ error: 'Bot not found.' }, 404) } try { const healthUrl = new URL('/health', rows[0].webhookUrl).toString() const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 5000) const res = await fetch(healthUrl, { signal: controller.signal }) clearTimeout(timeout) return c.json({ reachable: res.ok, status: res.status, }) } catch { return c.json({ reachable: false, status: 0 }) } })