refactor: migrate all code to use getGuildConfig() for guild settings

- Update all commands and events to fetch guild config once per execution
- Pass config to service methods that need it (ModerationService.issueWarning)
- Update terminal service to use guildSettingsService for persistence
- Remove direct imports of config for guild-specific settings

This consolidates configuration to database-backed guild settings,
eliminating the dual config system.
This commit is contained in:
syntaxbullet
2026-02-12 16:09:37 +01:00
parent ae6a068197
commit 58374d1746
11 changed files with 110 additions and 54 deletions

View File

@@ -1,6 +1,7 @@
import { createCommand } from "@shared/lib/utils"; import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, AttachmentBuilder } from "discord.js"; import { SlashCommandBuilder, PermissionFlagsBits, AttachmentBuilder } from "discord.js";
import { config, saveConfig } from "@shared/lib/config"; import { getGuildConfig, invalidateGuildConfigCache } from "@shared/lib/config";
import { guildSettingsService } from "@shared/modules/guild-settings/guild-settings.service";
import { DrizzleClient } from "@shared/db/DrizzleClient"; import { DrizzleClient } from "@shared/db/DrizzleClient";
import { items } from "@db/schema"; import { items } from "@db/schema";
import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds"; import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds";
@@ -49,7 +50,7 @@ export const createColor = createCommand({
// 2. Create Role // 2. Create Role
const role = await interaction.guild?.roles.create({ const role = await interaction.guild?.roles.create({
name: name, name: name,
color: colorInput as any, // Discord.js types are a bit strict on ColorResolvable, but string generally works or needs parsing color: colorInput as any,
reason: `Created via /createcolor by ${interaction.user.tag}` reason: `Created via /createcolor by ${interaction.user.tag}`
}); });
@@ -57,11 +58,9 @@ export const createColor = createCommand({
throw new Error("Failed to create role."); throw new Error("Failed to create role.");
} }
// 3. Update Config // 3. Add to guild settings
if (!config.colorRoles.includes(role.id)) { await guildSettingsService.addColorRole(interaction.guildId!, role.id);
config.colorRoles.push(role.id); invalidateGuildConfigCache(interaction.guildId!);
saveConfig(config);
}
// 4. Create Item // 4. Create Item
await DrizzleClient.insert(items).values({ await DrizzleClient.insert(items).values({

View File

@@ -6,7 +6,7 @@ import {
getModerationErrorEmbed, getModerationErrorEmbed,
getUserWarningEmbed getUserWarningEmbed
} from "@/modules/moderation/moderation.view"; } from "@/modules/moderation/moderation.view";
import { config } from "@shared/lib/config"; import { getGuildConfig } from "@shared/lib/config";
export const warn = createCommand({ export const warn = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -50,6 +50,9 @@ export const warn = createCommand({
return; return;
} }
// Fetch guild config for moderation settings
const guildConfig = await getGuildConfig(interaction.guildId!);
// Issue the warning via service // Issue the warning via service
const { moderationCase, warningCount, autoTimeoutIssued } = await ModerationService.issueWarning({ const { moderationCase, warningCount, autoTimeoutIssued } = await ModerationService.issueWarning({
userId: targetUser.id, userId: targetUser.id,
@@ -59,7 +62,11 @@ export const warn = createCommand({
reason, reason,
guildName: interaction.guild?.name || undefined, guildName: interaction.guild?.name || undefined,
dmTarget: targetUser, dmTarget: targetUser,
timeoutTarget: await interaction.guild?.members.fetch(targetUser.id) timeoutTarget: await interaction.guild?.members.fetch(targetUser.id),
config: {
dmOnWarn: guildConfig.moderation?.cases?.dmOnWarn,
autoTimeoutThreshold: guildConfig.moderation?.cases?.autoTimeoutThreshold,
},
}); });
// Send success message to moderator // Send success message to moderator

View File

@@ -1,6 +1,6 @@
import { createCommand } from "@shared/lib/utils"; import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder } from "discord.js"; import { SlashCommandBuilder } from "discord.js";
import { config } from "@shared/lib/config"; import { getGuildConfig } from "@shared/lib/config";
import { createErrorEmbed } from "@/lib/embeds"; import { createErrorEmbed } from "@/lib/embeds";
import { getFeedbackTypeMenu } from "@/modules/feedback/feedback.view"; import { getFeedbackTypeMenu } from "@/modules/feedback/feedback.view";
@@ -9,8 +9,10 @@ export const feedback = createCommand({
.setName("feedback") .setName("feedback")
.setDescription("Submit feedback, feature requests, or bug reports"), .setDescription("Submit feedback, feature requests, or bug reports"),
execute: async (interaction) => { execute: async (interaction) => {
const guildConfig = await getGuildConfig(interaction.guildId!);
// Check if feedback channel is configured // Check if feedback channel is configured
if (!config.feedbackChannelId) { if (!guildConfig.feedbackChannelId) {
await interaction.reply({ await interaction.reply({
embeds: [createErrorEmbed("Feedback system is not configured. Please contact an administrator.")], embeds: [createErrorEmbed("Feedback system is not configured. Please contact an administrator.")],
ephemeral: true ephemeral: true

View File

@@ -6,7 +6,7 @@ import { createErrorEmbed } from "@lib/embeds";
import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view"; import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view";
import type { ItemUsageData } from "@shared/lib/types"; import type { ItemUsageData } from "@shared/lib/types";
import { UserError } from "@shared/lib/errors"; import { UserError } from "@shared/lib/errors";
import { config } from "@shared/lib/config"; import { getGuildConfig } from "@shared/lib/config";
export const use = createCommand({ export const use = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -21,6 +21,9 @@ export const use = createCommand({
execute: async (interaction) => { execute: async (interaction) => {
await interaction.deferReply(); await interaction.deferReply();
const guildConfig = await getGuildConfig(interaction.guildId!);
const colorRoles = guildConfig.colorRoles ?? [];
const itemId = interaction.options.getNumber("item", true); const itemId = interaction.options.getNumber("item", true);
const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username); const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
if (!user) { if (!user) {
@@ -42,7 +45,7 @@ export const use = createCommand({
await member.roles.add(effect.roleId); await member.roles.add(effect.roleId);
} else if (effect.type === 'COLOR_ROLE') { } else if (effect.type === 'COLOR_ROLE') {
// Remove existing color roles // Remove existing color roles
const rolesToRemove = config.colorRoles.filter(r => member.roles.cache.has(r)); const rolesToRemove = colorRoles.filter(r => member.roles.cache.has(r));
if (rolesToRemove.length > 0) await member.roles.remove(rolesToRemove); if (rolesToRemove.length > 0) await member.roles.remove(rolesToRemove);
await member.roles.add(effect.roleId); await member.roles.add(effect.roleId);
} }

View File

@@ -1,20 +1,26 @@
import { Events } from "discord.js"; import { Events } from "discord.js";
import type { Event } from "@shared/lib/types"; import type { Event } from "@shared/lib/types";
import { config } from "@shared/lib/config"; import { getGuildConfig } from "@shared/lib/config";
import { userService } from "@shared/modules/user/user.service"; import { userService } from "@shared/modules/user/user.service";
// Visitor role
const event: Event<Events.GuildMemberAdd> = { const event: Event<Events.GuildMemberAdd> = {
name: Events.GuildMemberAdd, name: Events.GuildMemberAdd,
execute: async (member) => { execute: async (member) => {
console.log(`👤 New member joined: ${member.user.tag} (${member.id})`); console.log(`👤 New member joined: ${member.user.tag} (${member.id})`);
const guildConfig = await getGuildConfig(member.guild.id);
try { try {
const user = await userService.getUserById(member.id); const user = await userService.getUserById(member.id);
if (user && user.class) { if (user && user.class) {
console.log(`🔄 Returning student detected: ${member.user.tag}`); console.log(`🔄 Returning student detected: ${member.user.tag}`);
await member.roles.remove(config.visitorRole); if (guildConfig.visitorRole) {
await member.roles.add(config.studentRole); await member.roles.remove(guildConfig.visitorRole);
}
if (guildConfig.studentRole) {
await member.roles.add(guildConfig.studentRole);
}
if (user.class.roleId) { if (user.class.roleId) {
await member.roles.add(user.class.roleId); await member.roles.add(user.class.roleId);
@@ -22,8 +28,10 @@ const event: Event<Events.GuildMemberAdd> = {
} }
console.log(`Restored student role to ${member.user.tag}`); console.log(`Restored student role to ${member.user.tag}`);
} else { } else {
await member.roles.add(config.visitorRole); if (guildConfig.visitorRole) {
console.log(`Assigned visitor role to ${member.user.tag}`); await member.roles.add(guildConfig.visitorRole);
console.log(`Assigned visitor role to ${member.user.tag}`);
}
} }
console.log(`User Roles: ${member.roles.cache.map(role => role.name).join(", ")}`); console.log(`User Roles: ${member.roles.cache.map(role => role.name).join(", ")}`);
} catch (error) { } catch (error) {

View File

@@ -1,6 +1,6 @@
import type { Interaction } from "discord.js"; import type { Interaction } from "discord.js";
import { TextChannel, MessageFlags } from "discord.js"; import { TextChannel, MessageFlags } from "discord.js";
import { config } from "@shared/lib/config"; import { getGuildConfig } from "@shared/lib/config";
import { AuroraClient } from "@/lib/BotClient"; import { AuroraClient } from "@/lib/BotClient";
import { buildFeedbackMessage, getFeedbackModal } from "./feedback.view"; import { buildFeedbackMessage, getFeedbackModal } from "./feedback.view";
import { FEEDBACK_CUSTOM_IDS, type FeedbackType, type FeedbackData } from "./feedback.types"; import { FEEDBACK_CUSTOM_IDS, type FeedbackType, type FeedbackData } from "./feedback.types";
@@ -33,7 +33,13 @@ export const handleFeedbackInteraction = async (interaction: Interaction) => {
throw new UserError("An error occurred processing your feedback. Please try again."); throw new UserError("An error occurred processing your feedback. Please try again.");
} }
if (!config.feedbackChannelId) { if (!interaction.guildId) {
throw new UserError("This action can only be performed in a server.");
}
const guildConfig = await getGuildConfig(interaction.guildId);
if (!guildConfig.feedbackChannelId) {
throw new UserError("Feedback channel is not configured. Please contact an administrator."); throw new UserError("Feedback channel is not configured. Please contact an administrator.");
} }
@@ -52,7 +58,7 @@ export const handleFeedbackInteraction = async (interaction: Interaction) => {
}; };
// Get feedback channel // Get feedback channel
const channel = await AuroraClient.channels.fetch(config.feedbackChannelId).catch(() => null) as TextChannel | null; const channel = await AuroraClient.channels.fetch(guildConfig.feedbackChannelId).catch(() => null) as TextChannel | null;
if (!channel) { if (!channel) {
throw new UserError("Feedback channel not found. Please contact an administrator."); throw new UserError("Feedback channel not found. Please contact an administrator.");

View File

@@ -1,4 +1,5 @@
import { temporaryRoleService } from "@shared/modules/system/temp-role.service"; import { temporaryRoleService } from "@shared/modules/system/temp-role.service";
import { terminalService } from "@shared/modules/terminal/terminal.service";
export const schedulerService = { export const schedulerService = {
start: () => { start: () => {
@@ -10,7 +11,6 @@ export const schedulerService = {
}, 60 * 1000); }, 60 * 1000);
// 2. Terminal Update Loop (every 60s) // 2. Terminal Update Loop (every 60s)
const { terminalService } = require("@shared/modules/terminal/terminal.service");
setInterval(() => { setInterval(() => {
terminalService.update(); terminalService.update();
}, 60 * 1000); }, 60 * 1000);

View File

@@ -1,5 +1,5 @@
import { ButtonInteraction, MessageFlags } from "discord.js"; import { ButtonInteraction, MessageFlags } from "discord.js";
import { config } from "@shared/lib/config"; import { getGuildConfig, invalidateGuildConfigCache } from "@shared/lib/config";
import { getEnrollmentSuccessMessage } from "./enrollment.view"; import { getEnrollmentSuccessMessage } from "./enrollment.view";
import { classService } from "@shared/modules/class/class.service"; import { classService } from "@shared/modules/class/class.service";
import { userService } from "@shared/modules/user/user.service"; import { userService } from "@shared/modules/user/user.service";
@@ -11,7 +11,8 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
throw new UserError("This action can only be performed in a server."); throw new UserError("This action can only be performed in a server.");
} }
const { studentRole, visitorRole } = config; const guildConfig = await getGuildConfig(interaction.guildId);
const { studentRole, visitorRole, welcomeChannelId, welcomeMessage } = guildConfig;
if (!studentRole || !visitorRole) { if (!studentRole || !visitorRole) {
throw new UserError("No student or visitor role configured for enrollment."); throw new UserError("No student or visitor role configured for enrollment.");
@@ -67,10 +68,10 @@ export async function handleEnrollmentInteraction(interaction: ButtonInteraction
}); });
// 5. Send Welcome Message (if configured) // 5. Send Welcome Message (if configured)
if (config.welcomeChannelId) { if (welcomeChannelId) {
const welcomeChannel = interaction.guild.channels.cache.get(config.welcomeChannelId); const welcomeChannel = interaction.guild.channels.cache.get(welcomeChannelId);
if (welcomeChannel && welcomeChannel.isTextBased()) { if (welcomeChannel && welcomeChannel.isTextBased()) {
const rawMessage = config.welcomeMessage || "Welcome to Aurora, {user}! You have been enrolled as a **{class}**."; const rawMessage = welcomeMessage || "Welcome to Aurora, {user}! You have been enrolled as a **{class}**.";
const processedMessage = rawMessage const processedMessage = rawMessage
.replace(/{user}/g, member.toString()) .replace(/{user}/g, member.toString())

View File

@@ -116,7 +116,7 @@ class LootdropService {
}); });
// Trigger Terminal Update // Trigger Terminal Update
terminalService.update(); terminalService.update(channel.guildId);
} catch (error) { } catch (error) {
console.error("Failed to spawn lootdrop:", error); console.error("Failed to spawn lootdrop:", error);
@@ -153,7 +153,7 @@ class LootdropService {
`Claimed lootdrop in channel ${drop.channelId}` `Claimed lootdrop in channel ${drop.channelId}`
); );
// Trigger Terminal Update // Trigger Terminal Update (uses primary guild from env)
terminalService.update(); terminalService.update();
return { success: true, amount: drop.rewardAmount, currency: drop.currency }; return { success: true, amount: drop.rewardAmount, currency: drop.currency };

View File

@@ -2,10 +2,14 @@ import { moderationCases } from "@db/schema";
import { eq, and, desc } from "drizzle-orm"; import { eq, and, desc } from "drizzle-orm";
import { DrizzleClient } from "@shared/db/DrizzleClient"; import { DrizzleClient } from "@shared/db/DrizzleClient";
import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "@/modules/moderation/moderation.types"; import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "@/modules/moderation/moderation.types";
import { config } from "@shared/lib/config";
import { getUserWarningEmbed } from "@/modules/moderation/moderation.view"; import { getUserWarningEmbed } from "@/modules/moderation/moderation.view";
import { CaseType } from "@shared/lib/constants"; import { CaseType } from "@shared/lib/constants";
export interface ModerationConfig {
dmOnWarn?: boolean;
autoTimeoutThreshold?: number;
}
export class ModerationService { export class ModerationService {
/** /**
* Generate the next sequential case ID * Generate the next sequential case ID
@@ -62,6 +66,7 @@ export class ModerationService {
guildName?: string; guildName?: string;
dmTarget?: { send: (options: any) => Promise<any> }; dmTarget?: { send: (options: any) => Promise<any> };
timeoutTarget?: { timeout: (duration: number, reason: string) => Promise<any> }; timeoutTarget?: { timeout: (duration: number, reason: string) => Promise<any> };
config?: ModerationConfig;
}) { }) {
const moderationCase = await this.createCase({ const moderationCase = await this.createCase({
type: CaseType.WARN, type: CaseType.WARN,
@@ -77,9 +82,10 @@ export class ModerationService {
} }
const warningCount = await this.getActiveWarningCount(options.userId); const warningCount = await this.getActiveWarningCount(options.userId);
const config = options.config ?? {};
// Try to DM the user if configured // Try to DM the user if configured
if (config.moderation.cases.dmOnWarn && options.dmTarget) { if (config.dmOnWarn !== false && options.dmTarget) {
try { try {
await options.dmTarget.send({ await options.dmTarget.send({
embeds: [getUserWarningEmbed( embeds: [getUserWarningEmbed(
@@ -96,8 +102,8 @@ export class ModerationService {
// Check for auto-timeout threshold // Check for auto-timeout threshold
let autoTimeoutIssued = false; let autoTimeoutIssued = false;
if (config.moderation.cases.autoTimeoutThreshold && if (config.autoTimeoutThreshold &&
warningCount >= config.moderation.cases.autoTimeoutThreshold && warningCount >= config.autoTimeoutThreshold &&
options.timeoutTarget) { options.timeoutTarget) {
try { try {

View File

@@ -12,24 +12,36 @@ import { AuroraClient } from "@/lib/BotClient";
import { DrizzleClient } from "@shared/db/DrizzleClient"; import { DrizzleClient } from "@shared/db/DrizzleClient";
import { users, transactions, lootdrops, inventory } from "@db/schema"; import { users, transactions, lootdrops, inventory } from "@db/schema";
import { desc, sql } from "drizzle-orm"; import { desc, sql } from "drizzle-orm";
import { config, saveConfig } from "@shared/lib/config"; import { getGuildConfig, invalidateGuildConfigCache } from "@shared/lib/config";
import { guildSettingsService } from "@shared/modules/guild-settings/guild-settings.service";
import { env } from "@shared/lib/env";
// Color palette for containers (hex as decimal)
const COLORS = { const COLORS = {
HEADER: 0x9B59B6, // Purple - mystical HEADER: 0x9B59B6,
LEADERS: 0xF1C40F, // Gold - achievement LEADERS: 0xF1C40F,
ACTIVITY: 0x3498DB, // Blue - activity ACTIVITY: 0x3498DB,
ALERT: 0xE74C3C // Red - active events ALERT: 0xE74C3C
}; };
function getPrimaryGuildId(): string | null {
return env.DISCORD_GUILD_ID ?? null;
}
export const terminalService = { export const terminalService = {
init: async (channel: TextChannel) => { init: async (channel: TextChannel) => {
// Limit to one terminal for now const guildId = channel.guildId;
if (config.terminal) { if (!guildId) {
console.error("Cannot initialize terminal: no guild ID");
return;
}
// Clean up old terminal if exists
const currentConfig = await getGuildConfig(guildId);
if (currentConfig.terminal?.channelId && currentConfig.terminal?.messageId) {
try { try {
const oldChannel = await AuroraClient.channels.fetch(config.terminal.channelId) as TextChannel; const oldChannel = await AuroraClient.channels.fetch(currentConfig.terminal.channelId).catch(() => null) as TextChannel | null;
if (oldChannel) { if (oldChannel) {
const oldMsg = await oldChannel.messages.fetch(config.terminal.messageId); const oldMsg = await oldChannel.messages.fetch(currentConfig.terminal.messageId).catch(() => null);
if (oldMsg) await oldMsg.delete(); if (oldMsg) await oldMsg.delete();
} }
} catch (e) { } catch (e) {
@@ -39,25 +51,37 @@ export const terminalService = {
const msg = await channel.send({ content: "🔄 Initializing Aurora Station..." }); const msg = await channel.send({ content: "🔄 Initializing Aurora Station..." });
config.terminal = { // Save to database
channelId: channel.id, await guildSettingsService.upsertSettings({
messageId: msg.id guildId,
}; terminalChannelId: channel.id,
saveConfig(config); terminalMessageId: msg.id,
});
invalidateGuildConfigCache(guildId);
await terminalService.update(); await terminalService.update(guildId);
}, },
update: async () => { update: async (guildId?: string) => {
if (!config.terminal) return; const effectiveGuildId = guildId ?? getPrimaryGuildId();
if (!effectiveGuildId) {
console.warn("No guild ID available for terminal update");
return;
}
const guildConfig = await getGuildConfig(effectiveGuildId);
if (!guildConfig.terminal?.channelId || !guildConfig.terminal?.messageId) {
return;
}
try { try {
const channel = await AuroraClient.channels.fetch(config.terminal.channelId).catch(() => null) as TextChannel; const channel = await AuroraClient.channels.fetch(guildConfig.terminal.channelId).catch(() => null) as TextChannel | null;
if (!channel) { if (!channel) {
console.warn("Terminal channel not found"); console.warn("Terminal channel not found");
return; return;
} }
const message = await channel.messages.fetch(config.terminal.messageId).catch(() => null); const message = await channel.messages.fetch(guildConfig.terminal.messageId).catch(() => null);
if (!message) { if (!message) {
console.warn("Terminal message not found"); console.warn("Terminal message not found");
return; return;