diff --git a/shared/modules/guild-settings/guild-settings.service.ts b/shared/modules/guild-settings/guild-settings.service.ts new file mode 100644 index 0000000..43f2b6b --- /dev/null +++ b/shared/modules/guild-settings/guild-settings.service.ts @@ -0,0 +1,158 @@ +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; +} + +export const guildSettingsService = { + getSettings: async (guildId: string): Promise => { + 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 & { 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 | null + ) => { + const keyMap: Record = { + 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 = { 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 }); + }, +};