feat: dashboard
This commit is contained in:
756
src/web/api.ts
756
src/web/api.ts
@@ -2,346 +2,526 @@
|
||||
* API routes for bot options and personalities
|
||||
*/
|
||||
|
||||
import { Hono } from "hono";
|
||||
import { Elysia } from "elysia";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { ChannelType, PermissionFlagsBits } from "discord.js";
|
||||
import { db } from "../database";
|
||||
import { personalities, botOptions, guilds } from "../database/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { requireAuth } from "./session";
|
||||
import { personalities, botOptions } from "../database/schema";
|
||||
import * as oauth from "./oauth";
|
||||
import { requireApiAuth } from "./session";
|
||||
import { htmlResponse, isHtmxRequest, jsonResponse, parseBody } from "./http";
|
||||
import type { BotClient } from "../core/client";
|
||||
import { personalitiesList, viewPromptModal, editPromptModal } from "./templates";
|
||||
|
||||
const DEFAULT_FREE_WILL_CHANCE = 2;
|
||||
const DEFAULT_MEMORY_CHANCE = 30;
|
||||
const DEFAULT_MENTION_PROBABILITY = 0;
|
||||
const DEFAULT_RESPONSE_MODE = "free-will";
|
||||
|
||||
export function createApiRoutes(client: BotClient) {
|
||||
const api = new Hono();
|
||||
return new Elysia({ prefix: "/api" })
|
||||
.get("/guilds", async ({ request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
// All API routes require authentication
|
||||
api.use("/*", requireAuth);
|
||||
try {
|
||||
const userGuilds = await oauth.getUserGuilds(auth.session.accessToken);
|
||||
const botGuildIds = new Set(client.guilds.cache.map((guild) => guild.id));
|
||||
const sharedGuilds = userGuilds.filter((guild) => botGuildIds.has(guild.id));
|
||||
return jsonResponse(sharedGuilds);
|
||||
} catch {
|
||||
return jsonResponse({ error: "Failed to fetch guilds" }, 500);
|
||||
}
|
||||
})
|
||||
.get("/guilds/:guildId/personalities", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
const guildId = params.guildId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ error: "Access denied" }, 403);
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
return jsonResponse(guildPersonalities);
|
||||
})
|
||||
.post("/guilds/:guildId/personalities", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
// 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 guildId = params.guildId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ error: "Access denied" }, 403);
|
||||
}
|
||||
|
||||
const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return c.json({ error: "Access denied" }, 403);
|
||||
}
|
||||
const body = await parseBody(request);
|
||||
const name = String(body.name ?? "").trim();
|
||||
const systemPrompt = String(body.system_prompt ?? "").trim();
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(personalities)
|
||||
.where(eq(personalities.id, personalityId))
|
||||
.limit(1);
|
||||
if (!name || !systemPrompt) {
|
||||
return jsonResponse({ error: "Name and system_prompt are required" }, 400);
|
||||
}
|
||||
|
||||
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({
|
||||
const id = crypto.randomUUID();
|
||||
await db.insert(personalities).values({
|
||||
id,
|
||||
guild_id: guildId,
|
||||
name,
|
||||
system_prompt,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(personalities.id, personalityId));
|
||||
system_prompt: systemPrompt,
|
||||
});
|
||||
|
||||
// Check if HTMX request
|
||||
if (c.req.header("hx-request")) {
|
||||
const guildPersonalities = await db
|
||||
if (isHtmxRequest(request)) {
|
||||
const guildPersonalities = await db
|
||||
.select()
|
||||
.from(personalities)
|
||||
.where(eq(personalities.guild_id, guildId));
|
||||
return htmlResponse(personalitiesList(guildId, guildPersonalities));
|
||||
}
|
||||
|
||||
return jsonResponse({ id, guild_id: guildId, name, system_prompt: systemPrompt }, 201);
|
||||
})
|
||||
.get("/guilds/:guildId/personalities/:personalityId/view", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
const guildId = params.guildId;
|
||||
const personalityId = params.personalityId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ error: "Access denied" }, 403);
|
||||
}
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(personalities)
|
||||
.where(eq(personalities.guild_id, guildId));
|
||||
return c.html(personalitiesList(guildId, guildPersonalities));
|
||||
}
|
||||
.where(eq(personalities.id, personalityId))
|
||||
.limit(1);
|
||||
|
||||
return c.json({ success: true });
|
||||
});
|
||||
if (result.length === 0) {
|
||||
return jsonResponse({ error: "Personality not found" }, 404);
|
||||
}
|
||||
|
||||
// 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");
|
||||
return htmlResponse(viewPromptModal(result[0]));
|
||||
})
|
||||
.get("/guilds/:guildId/personalities/:personalityId/edit", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return c.json({ error: "Access denied" }, 403);
|
||||
}
|
||||
const guildId = params.guildId;
|
||||
const personalityId = params.personalityId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ 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
|
||||
const result = await db
|
||||
.select()
|
||||
.from(personalities)
|
||||
.where(eq(personalities.guild_id, guildId));
|
||||
return c.html(personalitiesList(guildId, guildPersonalities));
|
||||
}
|
||||
.where(eq(personalities.id, personalityId))
|
||||
.limit(1);
|
||||
|
||||
return c.json({ success: true });
|
||||
});
|
||||
if (result.length === 0) {
|
||||
return jsonResponse({ error: "Personality not found" }, 404);
|
||||
}
|
||||
|
||||
// Get bot options for a guild
|
||||
api.get("/guilds/:guildId/options", async (c) => {
|
||||
const guildId = c.req.param("guildId");
|
||||
const session = c.get("session");
|
||||
return htmlResponse(editPromptModal(guildId, result[0]));
|
||||
})
|
||||
.put("/guilds/:guildId/personalities/:personalityId", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
const hasAccess = await verifyGuildAccess(session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return c.json({ error: "Access denied" }, 403);
|
||||
}
|
||||
const guildId = params.guildId;
|
||||
const personalityId = params.personalityId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ error: "Access denied" }, 403);
|
||||
}
|
||||
|
||||
const options = await db
|
||||
.select()
|
||||
.from(botOptions)
|
||||
.where(eq(botOptions.guild_id, guildId))
|
||||
.limit(1);
|
||||
const body = await parseBody(request);
|
||||
const name = typeof body.name === "string" ? body.name : undefined;
|
||||
const systemPrompt = typeof body.system_prompt === "string" ? body.system_prompt : undefined;
|
||||
|
||||
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,
|
||||
image_gen_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;
|
||||
image_gen_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",
|
||||
image_gen_enabled: form.image_gen_enabled === "on" || form.image_gen_enabled === "true",
|
||||
};
|
||||
} else {
|
||||
body = await c.req.json();
|
||||
}
|
||||
|
||||
// Convert boolean options to integer for SQLite
|
||||
const gifSearchEnabled = body.gif_search_enabled ? 1 : 0;
|
||||
const imageGenEnabled = body.image_gen_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,
|
||||
image_gen_enabled: imageGenEnabled,
|
||||
});
|
||||
} else {
|
||||
await db
|
||||
.update(botOptions)
|
||||
.update(personalities)
|
||||
.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,
|
||||
image_gen_enabled: imageGenEnabled,
|
||||
name,
|
||||
system_prompt: systemPrompt,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(botOptions.guild_id, guildId));
|
||||
.where(eq(personalities.id, personalityId));
|
||||
|
||||
if (isHtmxRequest(request)) {
|
||||
const guildPersonalities = await db
|
||||
.select()
|
||||
.from(personalities)
|
||||
.where(eq(personalities.guild_id, guildId));
|
||||
return htmlResponse(personalitiesList(guildId, guildPersonalities));
|
||||
}
|
||||
|
||||
return jsonResponse({ success: true });
|
||||
})
|
||||
.delete("/guilds/:guildId/personalities/:personalityId", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
const guildId = params.guildId;
|
||||
const personalityId = params.personalityId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ error: "Access denied" }, 403);
|
||||
}
|
||||
|
||||
await db.delete(personalities).where(eq(personalities.id, personalityId));
|
||||
|
||||
if (isHtmxRequest(request)) {
|
||||
const guildPersonalities = await db
|
||||
.select()
|
||||
.from(personalities)
|
||||
.where(eq(personalities.guild_id, guildId));
|
||||
return htmlResponse(personalitiesList(guildId, guildPersonalities));
|
||||
}
|
||||
|
||||
return jsonResponse({ success: true });
|
||||
})
|
||||
.get("/guilds/:guildId/options", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
const guildId = params.guildId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ error: "Access denied" }, 403);
|
||||
}
|
||||
|
||||
const options = await db
|
||||
.select()
|
||||
.from(botOptions)
|
||||
.where(eq(botOptions.guild_id, guildId))
|
||||
.limit(1);
|
||||
|
||||
if (options.length === 0) {
|
||||
return jsonResponse({
|
||||
guild_id: guildId,
|
||||
active_personality_id: null,
|
||||
response_mode: DEFAULT_RESPONSE_MODE,
|
||||
free_will_chance: 2,
|
||||
memory_chance: 30,
|
||||
mention_probability: 0,
|
||||
gif_search_enabled: 0,
|
||||
image_gen_enabled: 0,
|
||||
nsfw_image_enabled: 0,
|
||||
spontaneous_posts_enabled: 1,
|
||||
spontaneous_interval_min_ms: null,
|
||||
spontaneous_interval_max_ms: null,
|
||||
restricted_channel_id: null,
|
||||
spontaneous_channel_ids: null,
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse(options[0]);
|
||||
})
|
||||
.get("/guilds/:guildId/channels", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
const guildId = params.guildId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ error: "Access denied" }, 403);
|
||||
}
|
||||
|
||||
const guild = client.guilds.cache.get(guildId);
|
||||
if (!guild) {
|
||||
return jsonResponse({ error: "Guild not found" }, 404);
|
||||
}
|
||||
|
||||
await guild.channels.fetch();
|
||||
|
||||
const threadTypes = new Set<ChannelType>([
|
||||
ChannelType.PublicThread,
|
||||
ChannelType.PrivateThread,
|
||||
ChannelType.AnnouncementThread,
|
||||
]);
|
||||
|
||||
const channels = guild.channels.cache
|
||||
.filter((channel) => {
|
||||
if (!channel.isTextBased()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (threadTypes.has(channel.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return "name" in channel;
|
||||
})
|
||||
.map((channel) => {
|
||||
const permissions = client.user ? channel.permissionsFor(client.user) : null;
|
||||
const writable = Boolean(
|
||||
permissions?.has(PermissionFlagsBits.ViewChannel) &&
|
||||
permissions.has(PermissionFlagsBits.SendMessages),
|
||||
);
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
type: ChannelType[channel.type] ?? String(channel.type),
|
||||
writable,
|
||||
position: "rawPosition" in channel ? channel.rawPosition : 0,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => {
|
||||
if (left.position !== right.position) {
|
||||
return left.position - right.position;
|
||||
}
|
||||
|
||||
return left.name.localeCompare(right.name);
|
||||
})
|
||||
.map(({ position: _position, ...channel }) => channel);
|
||||
|
||||
return jsonResponse(channels);
|
||||
})
|
||||
.put("/guilds/:guildId/options", async ({ params, request }) => {
|
||||
const auth = await requireApiAuth(request);
|
||||
if (!auth.ok) {
|
||||
return auth.response;
|
||||
}
|
||||
|
||||
const guildId = params.guildId;
|
||||
const hasAccess = await verifyGuildAccess(auth.session.accessToken, guildId, client);
|
||||
if (!hasAccess) {
|
||||
return jsonResponse({ error: "Access denied" }, 403);
|
||||
}
|
||||
|
||||
const body = await parseBody(request);
|
||||
|
||||
const activePersonalityId = body.active_personality_id
|
||||
? String(body.active_personality_id).trim()
|
||||
: null;
|
||||
|
||||
if (activePersonalityId) {
|
||||
const matchingPersonality = await db
|
||||
.select({ id: personalities.id })
|
||||
.from(personalities)
|
||||
.where(and(eq(personalities.id, activePersonalityId), eq(personalities.guild_id, guildId)))
|
||||
.limit(1);
|
||||
|
||||
if (matchingPersonality.length === 0) {
|
||||
return jsonResponse({ error: "Selected personality does not belong to this server" }, 400);
|
||||
}
|
||||
}
|
||||
|
||||
const responseMode = normalizeOptionalResponseMode(body.response_mode);
|
||||
const freeWillChance = normalizePercentage(body.free_will_chance, DEFAULT_FREE_WILL_CHANCE);
|
||||
const memoryChance = normalizePercentage(body.memory_chance, DEFAULT_MEMORY_CHANCE);
|
||||
const mentionProbability = normalizePercentage(
|
||||
body.mention_probability,
|
||||
DEFAULT_MENTION_PROBABILITY,
|
||||
);
|
||||
const gifSearchEnabled = normalizeOptionalFlag(body.gif_search_enabled) ?? 0;
|
||||
const imageGenEnabled = normalizeOptionalFlag(body.image_gen_enabled) ?? 0;
|
||||
const nsfwImageEnabled = normalizeOptionalFlag(body.nsfw_image_enabled);
|
||||
const spontaneousPostsEnabled = normalizeOptionalFlag(body.spontaneous_posts_enabled);
|
||||
const intervalRange = normalizeIntervalRange(
|
||||
normalizeOptionalIntervalMs(body.spontaneous_interval_min_ms),
|
||||
normalizeOptionalIntervalMs(body.spontaneous_interval_max_ms),
|
||||
);
|
||||
const restrictedChannelId = normalizeChannelId(body.restricted_channel_id);
|
||||
const spontaneousChannelIds = normalizeSpontaneousChannelIds(body.spontaneous_channel_ids);
|
||||
|
||||
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: activePersonalityId,
|
||||
response_mode: responseMode,
|
||||
free_will_chance: freeWillChance,
|
||||
memory_chance: memoryChance,
|
||||
mention_probability: mentionProbability,
|
||||
gif_search_enabled: gifSearchEnabled,
|
||||
image_gen_enabled: imageGenEnabled,
|
||||
nsfw_image_enabled: nsfwImageEnabled,
|
||||
spontaneous_posts_enabled: spontaneousPostsEnabled,
|
||||
spontaneous_interval_min_ms: intervalRange.min,
|
||||
spontaneous_interval_max_ms: intervalRange.max,
|
||||
restricted_channel_id: restrictedChannelId,
|
||||
spontaneous_channel_ids: spontaneousChannelIds,
|
||||
});
|
||||
} else {
|
||||
await db
|
||||
.update(botOptions)
|
||||
.set({
|
||||
active_personality_id: activePersonalityId,
|
||||
response_mode: responseMode,
|
||||
free_will_chance: freeWillChance,
|
||||
memory_chance: memoryChance,
|
||||
mention_probability: mentionProbability,
|
||||
gif_search_enabled: gifSearchEnabled,
|
||||
image_gen_enabled: imageGenEnabled,
|
||||
nsfw_image_enabled: nsfwImageEnabled,
|
||||
spontaneous_posts_enabled: spontaneousPostsEnabled,
|
||||
spontaneous_interval_min_ms: intervalRange.min,
|
||||
spontaneous_interval_max_ms: intervalRange.max,
|
||||
restricted_channel_id: restrictedChannelId,
|
||||
spontaneous_channel_ids: spontaneousChannelIds,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(botOptions.guild_id, guildId));
|
||||
}
|
||||
|
||||
return jsonResponse({ success: true });
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeChannelId(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function normalizeSpontaneousChannelIds(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (Array.isArray(parsed)) {
|
||||
const ids = parsed
|
||||
.filter((entry): entry is string => typeof entry === "string")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return ids.length > 0 ? JSON.stringify(ids) : null;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to parsing CSV/whitespace/newline-delimited input.
|
||||
}
|
||||
|
||||
return c.json({ success: true });
|
||||
});
|
||||
const ids = trimmed
|
||||
.split(/[\s,]+/)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return api;
|
||||
return ids.length > 0 ? JSON.stringify(ids) : null;
|
||||
}
|
||||
|
||||
function normalizePercentage(value: unknown, fallback: number): number {
|
||||
const parsed = Number.parseInt(String(value ?? ""), 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, parsed));
|
||||
}
|
||||
|
||||
function normalizeOptionalResponseMode(value: unknown): "free-will" | "mention-only" | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const raw = String(value ?? "").trim();
|
||||
if (raw === "mention-only") {
|
||||
return "mention-only";
|
||||
}
|
||||
|
||||
return "free-will";
|
||||
}
|
||||
|
||||
function normalizeOptionalFlag(value: unknown): 0 | 1 | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (value === "on" || value === "true" || value === true || value === "1" || value === 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function normalizeOptionalIntervalMs(value: unknown): number | null | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const raw = String(value).trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Math.max(1_000, parsed);
|
||||
}
|
||||
|
||||
function normalizeIntervalRange(
|
||||
min: number | null | undefined,
|
||||
max: number | null | undefined,
|
||||
): { min: number | null | undefined; max: number | null | undefined } {
|
||||
if (min == null || max == null) {
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
if (min <= max) {
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
return { min: max, max: min };
|
||||
}
|
||||
|
||||
async function verifyGuildAccess(
|
||||
accessToken: string,
|
||||
guildId: string,
|
||||
client: BotClient
|
||||
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);
|
||||
return userGuilds.some((guild) => guild.id === guildId);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user