Files
discord-rpg-concept/shared/modules/guild-settings/guild-settings.service.ts
syntaxbullet ee7d63df3e feat(service): add guild settings service layer
Implement service for managing per-guild configuration with methods for
getting, upserting, updating, and deleting settings. Includes helpers
for color role management.
2026-02-12 14:58:41 +01:00

159 lines
6.4 KiB
TypeScript

import { eq } from "drizzle-orm";
import { guildSettings } from "@db/schema";
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { UserError } from "@shared/lib/errors";
export interface GuildSettingsData {
guildId: string;
studentRoleId?: string;
visitorRoleId?: string;
colorRoleIds?: string[];
welcomeChannelId?: string;
welcomeMessage?: string;
feedbackChannelId?: string;
terminalChannelId?: string;
terminalMessageId?: string;
moderationLogChannelId?: string;
moderationDmOnWarn?: boolean;
moderationAutoTimeoutThreshold?: number;
featureOverrides?: Record<string, boolean>;
}
export const guildSettingsService = {
getSettings: async (guildId: string): Promise<GuildSettingsData | null> => {
const settings = await DrizzleClient.query.guildSettings.findFirst({
where: eq(guildSettings.guildId, BigInt(guildId)),
});
if (!settings) return null;
return {
guildId: settings.guildId.toString(),
studentRoleId: settings.studentRoleId?.toString(),
visitorRoleId: settings.visitorRoleId?.toString(),
colorRoleIds: settings.colorRoleIds ?? [],
welcomeChannelId: settings.welcomeChannelId?.toString(),
welcomeMessage: settings.welcomeMessage ?? undefined,
feedbackChannelId: settings.feedbackChannelId?.toString(),
terminalChannelId: settings.terminalChannelId?.toString(),
terminalMessageId: settings.terminalMessageId?.toString(),
moderationLogChannelId: settings.moderationLogChannelId?.toString(),
moderationDmOnWarn: settings.moderationDmOnWarn ?? true,
moderationAutoTimeoutThreshold: settings.moderationAutoTimeoutThreshold ?? undefined,
featureOverrides: settings.featureOverrides ?? {},
};
},
upsertSettings: async (data: Partial<GuildSettingsData> & { guildId: string }) => {
const values: typeof guildSettings.$inferInsert = {
guildId: BigInt(data.guildId),
studentRoleId: data.studentRoleId ? BigInt(data.studentRoleId) : null,
visitorRoleId: data.visitorRoleId ? BigInt(data.visitorRoleId) : null,
colorRoleIds: data.colorRoleIds ?? [],
welcomeChannelId: data.welcomeChannelId ? BigInt(data.welcomeChannelId) : null,
welcomeMessage: data.welcomeMessage ?? null,
feedbackChannelId: data.feedbackChannelId ? BigInt(data.feedbackChannelId) : null,
terminalChannelId: data.terminalChannelId ? BigInt(data.terminalChannelId) : null,
terminalMessageId: data.terminalMessageId ? BigInt(data.terminalMessageId) : null,
moderationLogChannelId: data.moderationLogChannelId ? BigInt(data.moderationLogChannelId) : null,
moderationDmOnWarn: data.moderationDmOnWarn ?? true,
moderationAutoTimeoutThreshold: data.moderationAutoTimeoutThreshold ?? null,
featureOverrides: data.featureOverrides ?? {},
updatedAt: new Date(),
};
const [result] = await DrizzleClient.insert(guildSettings)
.values(values)
.onConflictDoUpdate({
target: guildSettings.guildId,
set: values,
})
.returning();
return result;
},
updateSetting: async (
guildId: string,
key: string,
value: string | string[] | boolean | number | Record<string, boolean> | null
) => {
const keyMap: Record<string, keyof typeof guildSettings.$inferSelect> = {
studentRole: "studentRoleId",
visitorRole: "visitorRoleId",
colorRoles: "colorRoleIds",
welcomeChannel: "welcomeChannelId",
welcomeMessage: "welcomeMessage",
feedbackChannel: "feedbackChannelId",
terminalChannel: "terminalChannelId",
terminalMessage: "terminalMessageId",
moderationLogChannel: "moderationLogChannelId",
moderationDmOnWarn: "moderationDmOnWarn",
moderationAutoTimeoutThreshold: "moderationAutoTimeoutThreshold",
featureOverrides: "featureOverrides",
};
const column = keyMap[key];
if (!column) {
throw new UserError(`Unknown setting: ${key}`);
}
const updates: Record<string, unknown> = { updatedAt: new Date() };
if (column === "colorRoleIds" && Array.isArray(value)) {
updates[column] = value;
} else if (column === "featureOverrides" && typeof value === "object" && value !== null) {
updates[column] = value;
} else if (column === "moderationDmOnWarn" && typeof value === "boolean") {
updates[column] = value;
} else if (column === "moderationAutoTimeoutThreshold" && typeof value === "number") {
updates[column] = value;
} else if (typeof value === "string") {
updates[column] = BigInt(value);
} else if (value === null) {
updates[column] = null;
} else {
updates[column] = value;
}
const [result] = await DrizzleClient.update(guildSettings)
.set(updates)
.where(eq(guildSettings.guildId, BigInt(guildId)))
.returning();
if (!result) {
throw new UserError(`No settings found for guild ${guildId}. Use /settings set to create settings first.`);
}
return result;
},
deleteSettings: async (guildId: string) => {
const [result] = await DrizzleClient.delete(guildSettings)
.where(eq(guildSettings.guildId, BigInt(guildId)))
.returning();
return result;
},
addColorRole: async (guildId: string, roleId: string) => {
const settings = await guildSettingsService.getSettings(guildId);
const colorRoleIds = settings?.colorRoleIds ?? [];
if (colorRoleIds.includes(roleId)) {
return settings;
}
colorRoleIds.push(roleId);
return await guildSettingsService.upsertSettings({ guildId, colorRoleIds });
},
removeColorRole: async (guildId: string, roleId: string) => {
const settings = await guildSettingsService.getSettings(guildId);
if (!settings) return null;
const colorRoleIds = (settings.colorRoleIds ?? []).filter(id => id !== roleId);
return await guildSettingsService.upsertSettings({ guildId, colorRoleIds });
},
};