diff --git a/bot/commands/admin/create_color.ts b/bot/commands/admin/create_color.ts index 7ab6a6d..493c779 100644 --- a/bot/commands/admin/create_color.ts +++ b/bot/commands/admin/create_color.ts @@ -1,6 +1,7 @@ import { createCommand } from "@shared/lib/utils"; 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 { items } from "@db/schema"; import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds"; @@ -49,7 +50,7 @@ export const createColor = createCommand({ // 2. Create Role const role = await interaction.guild?.roles.create({ 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}` }); @@ -57,11 +58,9 @@ export const createColor = createCommand({ throw new Error("Failed to create role."); } - // 3. Update Config - if (!config.colorRoles.includes(role.id)) { - config.colorRoles.push(role.id); - saveConfig(config); - } + // 3. Add to guild settings + await guildSettingsService.addColorRole(interaction.guildId!, role.id); + invalidateGuildConfigCache(interaction.guildId!); // 4. Create Item await DrizzleClient.insert(items).values({ diff --git a/bot/commands/admin/warn.ts b/bot/commands/admin/warn.ts index b6e72c8..aa78858 100644 --- a/bot/commands/admin/warn.ts +++ b/bot/commands/admin/warn.ts @@ -6,7 +6,7 @@ import { getModerationErrorEmbed, getUserWarningEmbed } from "@/modules/moderation/moderation.view"; -import { config } from "@shared/lib/config"; +import { getGuildConfig } from "@shared/lib/config"; export const warn = createCommand({ data: new SlashCommandBuilder() @@ -50,6 +50,9 @@ export const warn = createCommand({ return; } + // Fetch guild config for moderation settings + const guildConfig = await getGuildConfig(interaction.guildId!); + // Issue the warning via service const { moderationCase, warningCount, autoTimeoutIssued } = await ModerationService.issueWarning({ userId: targetUser.id, @@ -59,7 +62,11 @@ export const warn = createCommand({ reason, guildName: interaction.guild?.name || undefined, 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 diff --git a/bot/commands/feedback/feedback.ts b/bot/commands/feedback/feedback.ts index 7e775e5..437468b 100644 --- a/bot/commands/feedback/feedback.ts +++ b/bot/commands/feedback/feedback.ts @@ -1,6 +1,6 @@ import { createCommand } from "@shared/lib/utils"; import { SlashCommandBuilder } from "discord.js"; -import { config } from "@shared/lib/config"; +import { getGuildConfig } from "@shared/lib/config"; import { createErrorEmbed } from "@/lib/embeds"; import { getFeedbackTypeMenu } from "@/modules/feedback/feedback.view"; @@ -9,8 +9,10 @@ export const feedback = createCommand({ .setName("feedback") .setDescription("Submit feedback, feature requests, or bug reports"), execute: async (interaction) => { + const guildConfig = await getGuildConfig(interaction.guildId!); + // Check if feedback channel is configured - if (!config.feedbackChannelId) { + if (!guildConfig.feedbackChannelId) { await interaction.reply({ embeds: [createErrorEmbed("Feedback system is not configured. Please contact an administrator.")], ephemeral: true diff --git a/bot/commands/inventory/use.ts b/bot/commands/inventory/use.ts index 61dbac5..681e19a 100644 --- a/bot/commands/inventory/use.ts +++ b/bot/commands/inventory/use.ts @@ -6,7 +6,7 @@ import { createErrorEmbed } from "@lib/embeds"; import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view"; import type { ItemUsageData } from "@shared/lib/types"; import { UserError } from "@shared/lib/errors"; -import { config } from "@shared/lib/config"; +import { getGuildConfig } from "@shared/lib/config"; export const use = createCommand({ data: new SlashCommandBuilder() @@ -21,6 +21,9 @@ export const use = createCommand({ execute: async (interaction) => { await interaction.deferReply(); + const guildConfig = await getGuildConfig(interaction.guildId!); + const colorRoles = guildConfig.colorRoles ?? []; + const itemId = interaction.options.getNumber("item", true); const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username); if (!user) { @@ -42,7 +45,7 @@ export const use = createCommand({ await member.roles.add(effect.roleId); } else if (effect.type === 'COLOR_ROLE') { // 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); await member.roles.add(effect.roleId); } diff --git a/bot/events/guildMemberAdd.ts b/bot/events/guildMemberAdd.ts index c94537a..cd2d412 100644 --- a/bot/events/guildMemberAdd.ts +++ b/bot/events/guildMemberAdd.ts @@ -1,20 +1,26 @@ import { Events } from "discord.js"; 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"; -// Visitor role const event: Event = { name: Events.GuildMemberAdd, execute: async (member) => { console.log(`👤 New member joined: ${member.user.tag} (${member.id})`); + + const guildConfig = await getGuildConfig(member.guild.id); + try { const user = await userService.getUserById(member.id); if (user && user.class) { console.log(`🔄 Returning student detected: ${member.user.tag}`); - await member.roles.remove(config.visitorRole); - await member.roles.add(config.studentRole); + if (guildConfig.visitorRole) { + await member.roles.remove(guildConfig.visitorRole); + } + if (guildConfig.studentRole) { + await member.roles.add(guildConfig.studentRole); + } if (user.class.roleId) { await member.roles.add(user.class.roleId); @@ -22,8 +28,10 @@ const event: Event = { } console.log(`Restored student role to ${member.user.tag}`); } else { - await member.roles.add(config.visitorRole); - console.log(`Assigned visitor role to ${member.user.tag}`); + if (guildConfig.visitorRole) { + 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(", ")}`); } catch (error) { diff --git a/bot/modules/feedback/feedback.interaction.ts b/bot/modules/feedback/feedback.interaction.ts index 4c67d6d..a511a23 100644 --- a/bot/modules/feedback/feedback.interaction.ts +++ b/bot/modules/feedback/feedback.interaction.ts @@ -1,6 +1,6 @@ import type { Interaction } 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 { buildFeedbackMessage, getFeedbackModal } from "./feedback.view"; 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."); } - 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."); } @@ -52,7 +58,7 @@ export const handleFeedbackInteraction = async (interaction: Interaction) => { }; // 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) { throw new UserError("Feedback channel not found. Please contact an administrator."); diff --git a/bot/modules/system/scheduler.ts b/bot/modules/system/scheduler.ts index 069d387..1b05d54 100644 --- a/bot/modules/system/scheduler.ts +++ b/bot/modules/system/scheduler.ts @@ -1,4 +1,5 @@ import { temporaryRoleService } from "@shared/modules/system/temp-role.service"; +import { terminalService } from "@shared/modules/terminal/terminal.service"; export const schedulerService = { start: () => { @@ -10,7 +11,6 @@ export const schedulerService = { }, 60 * 1000); // 2. Terminal Update Loop (every 60s) - const { terminalService } = require("@shared/modules/terminal/terminal.service"); setInterval(() => { terminalService.update(); }, 60 * 1000); diff --git a/bot/modules/user/enrollment.interaction.ts b/bot/modules/user/enrollment.interaction.ts index bec444a..dbeddcf 100644 --- a/bot/modules/user/enrollment.interaction.ts +++ b/bot/modules/user/enrollment.interaction.ts @@ -1,5 +1,5 @@ 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 { classService } from "@shared/modules/class/class.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."); } - const { studentRole, visitorRole } = config; + const guildConfig = await getGuildConfig(interaction.guildId); + const { studentRole, visitorRole, welcomeChannelId, welcomeMessage } = guildConfig; if (!studentRole || !visitorRole) { 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) - if (config.welcomeChannelId) { - const welcomeChannel = interaction.guild.channels.cache.get(config.welcomeChannelId); + if (welcomeChannelId) { + const welcomeChannel = interaction.guild.channels.cache.get(welcomeChannelId); 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 .replace(/{user}/g, member.toString()) diff --git a/shared/modules/economy/lootdrop.service.ts b/shared/modules/economy/lootdrop.service.ts index 0364a9f..6d1317b 100644 --- a/shared/modules/economy/lootdrop.service.ts +++ b/shared/modules/economy/lootdrop.service.ts @@ -116,7 +116,7 @@ class LootdropService { }); // Trigger Terminal Update - terminalService.update(); + terminalService.update(channel.guildId); } catch (error) { console.error("Failed to spawn lootdrop:", error); @@ -153,7 +153,7 @@ class LootdropService { `Claimed lootdrop in channel ${drop.channelId}` ); - // Trigger Terminal Update + // Trigger Terminal Update (uses primary guild from env) terminalService.update(); return { success: true, amount: drop.rewardAmount, currency: drop.currency }; diff --git a/shared/modules/moderation/moderation.service.ts b/shared/modules/moderation/moderation.service.ts index 4537df2..84c1384 100644 --- a/shared/modules/moderation/moderation.service.ts +++ b/shared/modules/moderation/moderation.service.ts @@ -2,10 +2,14 @@ import { moderationCases } from "@db/schema"; import { eq, and, desc } from "drizzle-orm"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "@/modules/moderation/moderation.types"; -import { config } from "@shared/lib/config"; import { getUserWarningEmbed } from "@/modules/moderation/moderation.view"; import { CaseType } from "@shared/lib/constants"; +export interface ModerationConfig { + dmOnWarn?: boolean; + autoTimeoutThreshold?: number; +} + export class ModerationService { /** * Generate the next sequential case ID @@ -62,6 +66,7 @@ export class ModerationService { guildName?: string; dmTarget?: { send: (options: any) => Promise }; timeoutTarget?: { timeout: (duration: number, reason: string) => Promise }; + config?: ModerationConfig; }) { const moderationCase = await this.createCase({ type: CaseType.WARN, @@ -77,9 +82,10 @@ export class ModerationService { } const warningCount = await this.getActiveWarningCount(options.userId); + const config = options.config ?? {}; // Try to DM the user if configured - if (config.moderation.cases.dmOnWarn && options.dmTarget) { + if (config.dmOnWarn !== false && options.dmTarget) { try { await options.dmTarget.send({ embeds: [getUserWarningEmbed( @@ -96,8 +102,8 @@ export class ModerationService { // Check for auto-timeout threshold let autoTimeoutIssued = false; - if (config.moderation.cases.autoTimeoutThreshold && - warningCount >= config.moderation.cases.autoTimeoutThreshold && + if (config.autoTimeoutThreshold && + warningCount >= config.autoTimeoutThreshold && options.timeoutTarget) { try { diff --git a/shared/modules/terminal/terminal.service.ts b/shared/modules/terminal/terminal.service.ts index 0fe9c59..87674d1 100644 --- a/shared/modules/terminal/terminal.service.ts +++ b/shared/modules/terminal/terminal.service.ts @@ -12,24 +12,36 @@ import { AuroraClient } from "@/lib/BotClient"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import { users, transactions, lootdrops, inventory } from "@db/schema"; 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 = { - HEADER: 0x9B59B6, // Purple - mystical - LEADERS: 0xF1C40F, // Gold - achievement - ACTIVITY: 0x3498DB, // Blue - activity - ALERT: 0xE74C3C // Red - active events + HEADER: 0x9B59B6, + LEADERS: 0xF1C40F, + ACTIVITY: 0x3498DB, + ALERT: 0xE74C3C }; +function getPrimaryGuildId(): string | null { + return env.DISCORD_GUILD_ID ?? null; +} + export const terminalService = { init: async (channel: TextChannel) => { - // Limit to one terminal for now - if (config.terminal) { + const guildId = channel.guildId; + 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 { - 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) { - 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(); } } catch (e) { @@ -39,25 +51,37 @@ export const terminalService = { const msg = await channel.send({ content: "🔄 Initializing Aurora Station..." }); - config.terminal = { - channelId: channel.id, - messageId: msg.id - }; - saveConfig(config); + // Save to database + await guildSettingsService.upsertSettings({ + guildId, + terminalChannelId: channel.id, + terminalMessageId: msg.id, + }); + invalidateGuildConfigCache(guildId); - await terminalService.update(); + await terminalService.update(guildId); }, - update: async () => { - if (!config.terminal) return; + update: async (guildId?: string) => { + 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 { - 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) { console.warn("Terminal channel not found"); 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) { console.warn("Terminal message not found"); return;