/** * API routes for bot options and personalities */ import { Hono } from "hono"; import { db } from "../database"; import { personalities, botOptions, guilds } from "../database/schema"; import { eq } from "drizzle-orm"; import { requireAuth } from "./session"; import * as oauth from "./oauth"; import type { BotClient } from "../core/client"; import { personalitiesList, viewPromptModal, editPromptModal } from "./templates"; export function createApiRoutes(client: BotClient) { const api = new Hono(); // All API routes require authentication api.use("/*", requireAuth); // Get guilds the user has access to (shared with Joel) api.get("/guilds", async (c) => { const session = c.get("session"); try { const userGuilds = await oauth.getUserGuilds(session.accessToken); // Get guilds that Joel is in const botGuildIds = new Set(client.guilds.cache.map((g) => g.id)); // Filter to only guilds shared with Joel const sharedGuilds = userGuilds.filter((g) => botGuildIds.has(g.id)); return c.json(sharedGuilds); } catch (error) { return c.json({ error: "Failed to fetch guilds" }, 500); } }); // Get personalities for a guild api.get("/guilds/:guildId/personalities", async (c) => { const guildId = c.req.param("guildId"); const session = c.get("session"); // Verify user has access to this guild const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client); if (!hasAccess) { return c.json({ error: "Access denied" }, 403); } const guildPersonalities = await db .select() .from(personalities) .where(eq(personalities.guild_id, guildId)); return c.json(guildPersonalities); }); // Create a personality for a guild api.post("/guilds/:guildId/personalities", async (c) => { const guildId = c.req.param("guildId"); const session = c.get("session"); const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client); if (!hasAccess) { return c.json({ error: "Access denied" }, 403); } const contentType = c.req.header("content-type"); let name: string, system_prompt: string; if (contentType?.includes("application/x-www-form-urlencoded")) { const form = await c.req.parseBody(); name = form.name as string; system_prompt = form.system_prompt as string; } else { const body = await c.req.json<{ name: string; system_prompt: string }>(); name = body.name; system_prompt = body.system_prompt; } if (!name || !system_prompt) { return c.json({ error: "Name and system_prompt are required" }, 400); } const id = crypto.randomUUID(); await db.insert(personalities).values({ id, guild_id: guildId, name, system_prompt, }); // Check if HTMX request if (c.req.header("hx-request")) { const guildPersonalities = await db .select() .from(personalities) .where(eq(personalities.guild_id, guildId)); return c.html(personalitiesList(guildId, guildPersonalities)); } return c.json({ id, guild_id: guildId, name, system_prompt }, 201); }); // View a personality (returns modal HTML for HTMX) api.get("/guilds/:guildId/personalities/:personalityId/view", async (c) => { const guildId = c.req.param("guildId"); const personalityId = c.req.param("personalityId"); const session = c.get("session"); const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client); if (!hasAccess) { return c.json({ error: "Access denied" }, 403); } const result = await db .select() .from(personalities) .where(eq(personalities.id, personalityId)) .limit(1); if (result.length === 0) { return c.json({ error: "Personality not found" }, 404); } return c.html(viewPromptModal(result[0])); }); // Edit form for a personality (returns modal HTML for HTMX) api.get("/guilds/:guildId/personalities/:personalityId/edit", async (c) => { const guildId = c.req.param("guildId"); const personalityId = c.req.param("personalityId"); const session = c.get("session"); const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client); if (!hasAccess) { return c.json({ error: "Access denied" }, 403); } const result = await db .select() .from(personalities) .where(eq(personalities.id, personalityId)) .limit(1); if (result.length === 0) { return c.json({ error: "Personality not found" }, 404); } return c.html(editPromptModal(guildId, result[0])); }); // Update a personality api.put("/guilds/:guildId/personalities/:personalityId", async (c) => { const guildId = c.req.param("guildId"); const personalityId = c.req.param("personalityId"); const session = c.get("session"); const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client); if (!hasAccess) { return c.json({ error: "Access denied" }, 403); } const contentType = c.req.header("content-type"); let name: string | undefined, system_prompt: string | undefined; if (contentType?.includes("application/x-www-form-urlencoded")) { const form = await c.req.parseBody(); name = form.name as string; system_prompt = form.system_prompt as string; } else { const body = await c.req.json<{ name?: string; system_prompt?: string }>(); name = body.name; system_prompt = body.system_prompt; } await db .update(personalities) .set({ name, system_prompt, updated_at: new Date().toISOString(), }) .where(eq(personalities.id, personalityId)); // Check if HTMX request if (c.req.header("hx-request")) { const guildPersonalities = await db .select() .from(personalities) .where(eq(personalities.guild_id, guildId)); return c.html(personalitiesList(guildId, guildPersonalities)); } return c.json({ success: true }); }); // Delete a personality api.delete("/guilds/:guildId/personalities/:personalityId", async (c) => { const guildId = c.req.param("guildId"); const personalityId = c.req.param("personalityId"); const session = c.get("session"); const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client); if (!hasAccess) { return c.json({ error: "Access denied" }, 403); } await db.delete(personalities).where(eq(personalities.id, personalityId)); // Check if HTMX request if (c.req.header("hx-request")) { const guildPersonalities = await db .select() .from(personalities) .where(eq(personalities.guild_id, guildId)); return c.html(personalitiesList(guildId, guildPersonalities)); } return c.json({ success: true }); }); // Get bot options for a guild api.get("/guilds/:guildId/options", async (c) => { const guildId = c.req.param("guildId"); const session = c.get("session"); const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client); if (!hasAccess) { return c.json({ error: "Access denied" }, 403); } const options = await db .select() .from(botOptions) .where(eq(botOptions.guild_id, guildId)) .limit(1); if (options.length === 0) { // Return defaults return c.json({ guild_id: guildId, active_personality_id: null, free_will_chance: 2, memory_chance: 30, mention_probability: 0, gif_search_enabled: 0, }); } return c.json(options[0]); }); // Update bot options for a guild api.put("/guilds/:guildId/options", async (c) => { const guildId = c.req.param("guildId"); const session = c.get("session"); const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client); if (!hasAccess) { return c.json({ error: "Access denied" }, 403); } const contentType = c.req.header("content-type"); let body: { active_personality_id?: string | null; free_will_chance?: number; memory_chance?: number; mention_probability?: number; gif_search_enabled?: boolean | string; }; if (contentType?.includes("application/x-www-form-urlencoded")) { const form = await c.req.parseBody(); body = { active_personality_id: form.active_personality_id as string || null, free_will_chance: form.free_will_chance ? parseInt(form.free_will_chance as string) : undefined, memory_chance: form.memory_chance ? parseInt(form.memory_chance as string) : undefined, mention_probability: form.mention_probability ? parseInt(form.mention_probability as string) : undefined, gif_search_enabled: form.gif_search_enabled === "on" || form.gif_search_enabled === "true", }; } else { body = await c.req.json(); } // Convert gif_search_enabled to integer for SQLite const gifSearchEnabled = body.gif_search_enabled ? 1 : 0; // Upsert options const existing = await db .select() .from(botOptions) .where(eq(botOptions.guild_id, guildId)) .limit(1); if (existing.length === 0) { await db.insert(botOptions).values({ guild_id: guildId, active_personality_id: body.active_personality_id, free_will_chance: body.free_will_chance, memory_chance: body.memory_chance, mention_probability: body.mention_probability, gif_search_enabled: gifSearchEnabled, }); } else { await db .update(botOptions) .set({ active_personality_id: body.active_personality_id, free_will_chance: body.free_will_chance, memory_chance: body.memory_chance, mention_probability: body.mention_probability, gif_search_enabled: gifSearchEnabled, updated_at: new Date().toISOString(), }) .where(eq(botOptions.guild_id, guildId)); } return c.json({ success: true }); }); return api; } async function verifyGuildAccess( accessToken: string, guildId: string, client: BotClient ): Promise { // Check if bot is in this guild if (!client.guilds.cache.has(guildId)) { return false; } // Check if user is in this guild try { const userGuilds = await oauth.getUserGuilds(accessToken); return userGuilds.some((g) => g.id === guildId); } catch { return false; } }