343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
/**
|
|
* 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<boolean> {
|
|
// 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;
|
|
}
|
|
}
|