From 47c0d28058db3e7a24071b4c3b105cb7df99545a Mon Sep 17 00:00:00 2001 From: Shrev Dev Date: Sun, 12 Oct 2025 11:57:54 -0500 Subject: [PATCH] Refactor configuration management to support multiple guilds, including migration from single-guild format. Update Discord service to handle multiple channels and improve command handling for guild-specific settings. Add reset configuration command and enhance logging for better error tracking. if you were on the build before the multi-guild configuration existed, run /reset-config to properly continue using the bot. --- src/config/storage.ts | 208 +++++++++++++++++++++---------- src/index.ts | 18 +-- src/services/commands.service.ts | 181 ++++++++++++++++++++++++--- src/services/discord.service.ts | 141 ++++++++++++--------- 4 files changed, 393 insertions(+), 155 deletions(-) diff --git a/src/config/storage.ts b/src/config/storage.ts index 1d1152d..cc08512 100644 --- a/src/config/storage.ts +++ b/src/config/storage.ts @@ -7,7 +7,7 @@ export interface MonitorGroup { monitorIds: number[]; } -export interface BotConfig { +export interface GuildConfig { channelId: string | null; messageIds: string[]; monitorIds: number[]; @@ -17,9 +17,13 @@ export interface BotConfig { statusMessage: string; } +export interface MultiGuildConfig { + guilds: Record; +} + export class ConfigStorage { private configPath: string; - private config: BotConfig; + private config: MultiGuildConfig; private logger: Logger; constructor() { @@ -34,35 +38,38 @@ export class ConfigStorage { this.config = this.load(); } - private load(): BotConfig { + private load(): MultiGuildConfig { if (existsSync(this.configPath)) { try { const data = readFileSync(this.configPath, 'utf-8'); const loaded = JSON.parse(data); + + // Migration: Convert old single-guild format to multi-guild + if (loaded.channelId !== undefined && !loaded.guilds) { + this.logger.info('Migrating old config format to multi-guild format'); + return { + guilds: { + 'legacy': { + channelId: loaded.channelId || null, + messageIds: loaded.messageIds || [], + monitorIds: loaded.monitorIds || [], + groups: loaded.groups || [], + updateInterval: loaded.updateInterval || parseInt(process.env.UPDATE_INTERVAL || '60', 10) * 1000, + embedColor: loaded.embedColor || parseInt(process.env.EMBED_COLOR || '5814783', 10), + statusMessage: loaded.statusMessage || 'Service Status', + } + } + }; + } + this.logger.info('Loaded configuration from storage'); - return { - channelId: loaded.channelId || null, - messageIds: loaded.messageIds || [], - monitorIds: loaded.monitorIds || [], - groups: loaded.groups || [], - updateInterval: loaded.updateInterval || parseInt(process.env.UPDATE_INTERVAL || '60', 10) * 1000, - embedColor: loaded.embedColor || parseInt(process.env.EMBED_COLOR || '5814783', 10), - statusMessage: loaded.statusMessage || 'Service Status', - }; + return loaded; } catch (error: any) { this.logger.error(`Failed to load config: ${error.message}`); } } - return { - channelId: null, - messageIds: [], - monitorIds: [], - groups: [], - updateInterval: parseInt(process.env.UPDATE_INTERVAL || '60', 10) * 1000, - embedColor: parseInt(process.env.EMBED_COLOR || '5814783', 10), - statusMessage: 'Service Status', - }; + return { guilds: {} }; } private save(): void { @@ -74,117 +81,179 @@ export class ConfigStorage { } } - public getMonitorIds(): number[] { - return [...this.config.monitorIds]; + private getDefaultConfig(): GuildConfig { + return { + channelId: null, + messageIds: [], + monitorIds: [], + groups: [], + updateInterval: parseInt(process.env.UPDATE_INTERVAL || '60', 10) * 1000, + embedColor: parseInt(process.env.EMBED_COLOR || '5814783', 10), + statusMessage: 'Service Status', + }; } - public setMonitorIds(ids: number[]): void { - this.config.monitorIds = ids; - this.save(); + private getGuildConfig(guildId: string, createIfMissing: boolean = true): GuildConfig { + if (!this.config.guilds[guildId]) { + if (!createIfMissing) { + return this.getDefaultConfig(); // Return default without saving + } + this.config.guilds[guildId] = this.getDefaultConfig(); + this.save(); + } + return this.config.guilds[guildId]; } - public addMonitor(id: number): boolean { - if (!this.config.monitorIds.includes(id)) { - this.config.monitorIds.push(id); + public guildExists(guildId: string): boolean { + return !!this.config.guilds[guildId]; + } + + public getAllGuildIds(): string[] { + return Object.keys(this.config.guilds); + } + + public removeGuild(guildId: string): boolean { + if (this.config.guilds[guildId]) { + delete this.config.guilds[guildId]; this.save(); return true; } return false; } - public removeMonitor(id: number): boolean { - const index = this.config.monitorIds.indexOf(id); + public getMonitorIds(guildId: string): number[] { + const config = this.getGuildConfig(guildId); + return [...config.monitorIds]; + } + + public setMonitorIds(guildId: string, ids: number[]): void { + const config = this.getGuildConfig(guildId); + config.monitorIds = ids; + this.save(); + } + + public addMonitor(guildId: string, id: number): boolean { + const config = this.getGuildConfig(guildId); + if (!config.monitorIds.includes(id)) { + config.monitorIds.push(id); + this.save(); + return true; + } + return false; + } + + public removeMonitor(guildId: string, id: number): boolean { + const config = this.getGuildConfig(guildId); + const index = config.monitorIds.indexOf(id); if (index > -1) { - this.config.monitorIds.splice(index, 1); + config.monitorIds.splice(index, 1); this.save(); return true; } return false; } - public clearMonitors(): void { - this.config.monitorIds = []; + public clearMonitors(guildId: string): void { + const config = this.getGuildConfig(guildId); + config.monitorIds = []; this.save(); } - public getUpdateInterval(): number { - return this.config.updateInterval; + public getUpdateInterval(guildId: string): number { + const config = this.getGuildConfig(guildId); + return config.updateInterval; } - public setUpdateInterval(interval: number): void { + public setUpdateInterval(guildId: string, interval: number): void { if (interval >= 10000) { - this.config.updateInterval = interval; + const config = this.getGuildConfig(guildId); + config.updateInterval = interval; this.save(); } } - public getEmbedColor(): number { - return this.config.embedColor; + public getEmbedColor(guildId: string): number { + const config = this.getGuildConfig(guildId); + return config.embedColor; } - public setEmbedColor(color: number): void { - this.config.embedColor = color; + public setEmbedColor(guildId: string, color: number): void { + const config = this.getGuildConfig(guildId); + config.embedColor = color; this.save(); } - public getConfig(): BotConfig { - return { ...this.config }; + public getConfig(guildId: string): GuildConfig { + return { ...this.getGuildConfig(guildId, false) }; // Don't auto-create when just reading } - public getChannelId(): string | null { - return this.config.channelId; + public getChannelId(guildId: string): string | null { + if (!this.guildExists(guildId)) { + return null; // Don't auto-create guild just to check channel + } + const config = this.getGuildConfig(guildId, false); + return config.channelId; } - public setChannelId(channelId: string): void { - this.config.channelId = channelId; + public setChannelId(guildId: string, channelId: string): void { + const config = this.getGuildConfig(guildId); + config.channelId = channelId; this.save(); } - public getStatusMessage(): string { - return this.config.statusMessage; + public getStatusMessage(guildId: string): string { + const config = this.getGuildConfig(guildId); + return config.statusMessage; } - public setStatusMessage(message: string): void { - this.config.statusMessage = message; + public setStatusMessage(guildId: string, message: string): void { + const config = this.getGuildConfig(guildId); + config.statusMessage = message; this.save(); } - public getMessageIds(): string[] { - return [...this.config.messageIds]; + public getMessageIds(guildId: string): string[] { + const config = this.getGuildConfig(guildId); + return [...config.messageIds]; } - public setMessageIds(ids: string[]): void { - this.config.messageIds = ids; + public setMessageIds(guildId: string, ids: string[]): void { + const config = this.getGuildConfig(guildId); + config.messageIds = ids; this.save(); } - public getGroups(): MonitorGroup[] { - return [...this.config.groups]; + public getGroups(guildId: string): MonitorGroup[] { + const config = this.getGuildConfig(guildId); + return [...config.groups]; } - public addGroup(name: string): boolean { - if (this.config.groups.some(g => g.name.toLowerCase() === name.toLowerCase())) { + public addGroup(guildId: string, name: string): boolean { + const config = this.getGuildConfig(guildId); + if (config.groups.some(g => g.name.toLowerCase() === name.toLowerCase())) { return false; } - this.config.groups.push({ name, monitorIds: [] }); + config.groups.push({ name, monitorIds: [] }); this.save(); return true; } - public removeGroup(name: string): boolean { - const index = this.config.groups.findIndex(g => g.name.toLowerCase() === name.toLowerCase()); + public removeGroup(guildId: string, name: string): boolean { + const config = this.getGuildConfig(guildId); + const index = config.groups.findIndex(g => g.name.toLowerCase() === name.toLowerCase()); if (index > -1) { - this.config.groups.splice(index, 1); + config.groups.splice(index, 1); this.save(); return true; } return false; } - public addMonitorToGroup(groupName: string, monitorId: number): boolean { - const group = this.config.groups.find(g => g.name.toLowerCase() === groupName.toLowerCase()); + public addMonitorToGroup(guildId: string, groupName: string, monitorId: number): boolean { + const config = this.getGuildConfig(guildId); + const group = config.groups.find(g => g.name.toLowerCase() === groupName.toLowerCase()); if (group && !group.monitorIds.includes(monitorId)) { - for (const g of this.config.groups) { + for (const g of config.groups) { const idx = g.monitorIds.indexOf(monitorId); if (idx > -1) { g.monitorIds.splice(idx, 1); @@ -197,9 +266,10 @@ export class ConfigStorage { return false; } - public removeMonitorFromGroup(monitorId: number): boolean { + public removeMonitorFromGroup(guildId: string, monitorId: number): boolean { + const config = this.getGuildConfig(guildId); let found = false; - for (const group of this.config.groups) { + for (const group of config.groups) { const index = group.monitorIds.indexOf(monitorId); if (index > -1) { group.monitorIds.splice(index, 1); diff --git a/src/index.ts b/src/index.ts index 444fd98..8169a0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,23 +89,23 @@ class UptimeKumaDiscordBot { return; } - const trackedIds = configStorage.getMonitorIds(); - const monitors = this.uptimeKuma.getMonitorStats(trackedIds); + const monitors = this.uptimeKuma.getMonitorStats(); this.discord.updateMonitorStatus(monitors).catch(error => { this.logger.error(`Failed to update Discord status: ${error.message}`); }); }; - const interval = configStorage.getUpdateInterval(); - this.updateInterval = setInterval(updateFn, interval); + // Use a default interval; guilds can have different intervals but we'll use a common update cycle + const defaultInterval = parseInt(process.env.UPDATE_INTERVAL || '60', 10) * 1000; + this.updateInterval = setInterval(updateFn, defaultInterval); - this.logger.info(`Update interval set to ${interval / 1000} seconds`); + this.logger.info(`Update interval set to ${defaultInterval / 1000} seconds`); - const trackedIds = configStorage.getMonitorIds(); - if (trackedIds.length > 0) { - this.logger.info(`Tracking ${trackedIds.length} specific monitor(s): ${trackedIds.join(', ')}`); + const guildIds = configStorage.getAllGuildIds(); + if (guildIds.length > 0) { + this.logger.info(`Configured for ${guildIds.length} guild(s)`); } else { - this.logger.info('Tracking all monitors'); + this.logger.info('No guilds configured yet. Use /set-channel in a Discord server to get started.'); } } diff --git a/src/services/commands.service.ts b/src/services/commands.service.ts index 24d4dbe..5b12dfe 100644 --- a/src/services/commands.service.ts +++ b/src/services/commands.service.ts @@ -157,6 +157,12 @@ export class CommandsService { .setDescription('Show current bot configuration'), execute: this.showConfig.bind(this), }, + { + data: new SlashCommandBuilder() + .setName('reset-config') + .setDescription('⚠️ Reset this server\'s configuration to defaults'), + execute: this.resetConfig.bind(this), + }, ]; for (const command of commands) { @@ -190,11 +196,15 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } const monitorIdStr = interaction.options.getString('monitor', true); const monitorId = parseInt(monitorIdStr, 10); - const currentIds = configStorage.getMonitorIds(); + const currentIds = configStorage.getMonitorIds(interaction.guildId); if (currentIds.includes(monitorId)) { await interaction.reply({ content: '⚠️ This monitor is already being tracked.', @@ -204,7 +214,7 @@ export class CommandsService { } const newIds = [...currentIds, monitorId]; - configStorage.setMonitorIds(newIds); + configStorage.setMonitorIds(interaction.guildId, newIds); const monitors = uptimeKuma.getAllMonitors(); const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`; @@ -223,11 +233,15 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } const monitorIdStr = interaction.options.getString('monitor', true); const monitorId = parseInt(monitorIdStr, 10); - const currentIds = configStorage.getMonitorIds(); + const currentIds = configStorage.getMonitorIds(interaction.guildId); const newIds = currentIds.filter(id => id !== monitorId); if (currentIds.length === newIds.length) { @@ -238,7 +252,7 @@ export class CommandsService { return; } - configStorage.setMonitorIds(newIds); + configStorage.setMonitorIds(interaction.guildId, newIds); const monitors = uptimeKuma.getAllMonitors(); const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`; @@ -257,8 +271,12 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } - configStorage.clearMonitors(); + configStorage.clearMonitors(interaction.guildId); const embed = new EmbedBuilder() .setColor(0x0099ff) @@ -276,6 +294,10 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } await interaction.deferReply({ flags: MessageFlags.Ephemeral }); @@ -284,7 +306,7 @@ export class CommandsService { try { const discordService = (interaction.client as any).discordService; if (discordService) { - await discordService.setChannel(channel.id); + await discordService.setChannel(interaction.guildId, channel.id); const embed = new EmbedBuilder() .setColor(0x00ff00) @@ -308,10 +330,14 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } const message = interaction.options.getString('message', true); - configStorage.setStatusMessage(message); + configStorage.setStatusMessage(interaction.guildId, message); const embed = new EmbedBuilder() .setColor(0x00ff00) @@ -327,15 +353,19 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } - const botConfig = configStorage.getConfig(); + const botConfig = configStorage.getConfig(interaction.guildId); const trackedIds = botConfig.monitorIds; const channelId = botConfig.channelId; - const groups = configStorage.getGroups(); + const groups = configStorage.getGroups(interaction.guildId); const monitors = uptimeKuma.getAllMonitors(); const embed = new EmbedBuilder() - .setColor(configStorage.getEmbedColor()) + .setColor(configStorage.getEmbedColor(interaction.guildId)) .setTitle('⚙️ Bot Configuration & Status') .addFields( { @@ -401,15 +431,111 @@ export class CommandsService { await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }); } + private async resetConfig( + interaction: ChatInputCommandInteraction, + uptimeKuma: UptimeKumaService + ): Promise { + if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } + + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + let messagesDeleted = 0; + + // Delete existing embed messages for this guild + const channelId = configStorage.getChannelId(interaction.guildId); + const messageIds = configStorage.getMessageIds(interaction.guildId); + + if (channelId && messageIds.length > 0) { + try { + const channel = await interaction.client.channels.fetch(channelId); + if (channel && channel.isTextBased() && !channel.isDMBased()) { + for (const messageId of messageIds) { + try { + const message = await (channel as any).messages.fetch(messageId); + await message.delete(); + messagesDeleted++; + } catch (error: any) { + this.logger.warn(`Failed to delete message ${messageId}: ${error.message}`); + } + } + } + } catch (error: any) { + this.logger.warn(`Failed to fetch channel for message deletion: ${error.message}`); + } + } + + // Also delete legacy guild messages if they exist + const allGuildIds = configStorage.getAllGuildIds(); + const legacyGuilds = allGuildIds.filter(id => id === 'legacy'); + let cleanedLegacy = false; + + if (legacyGuilds.length > 0) { + for (const legacyId of legacyGuilds) { + const legacyChannelId = configStorage.getChannelId(legacyId); + const legacyMessageIds = configStorage.getMessageIds(legacyId); + + if (legacyChannelId && legacyMessageIds.length > 0) { + try { + const channel = await interaction.client.channels.fetch(legacyChannelId); + if (channel && channel.isTextBased() && !channel.isDMBased()) { + for (const messageId of legacyMessageIds) { + try { + const message = await (channel as any).messages.fetch(messageId); + await message.delete(); + messagesDeleted++; + } catch (error: any) { + this.logger.warn(`Failed to delete legacy message ${messageId}: ${error.message}`); + } + } + } + } catch (error: any) { + this.logger.warn(`Failed to fetch channel for legacy message deletion: ${error.message}`); + } + } + + configStorage.removeGuild(legacyId); + cleanedLegacy = true; + } + } + + // Remove the entire guild configuration so it starts completely fresh + configStorage.removeGuild(interaction.guildId); + + const embed = new EmbedBuilder() + .setColor(0xff9900) + .setTitle('⚠️ Configuration Reset') + .setDescription( + '**This server\'s configuration has been completely reset.**\n\n' + + (messagesDeleted > 0 ? `✅ Deleted ${messagesDeleted} status embed message(s)\n` : '') + + '✅ All configuration data removed\n' + + (cleanedLegacy ? '✅ Legacy migration data cleaned up\n' : '') + + '\n⚠️ You will need to reconfigure everything:\n' + + '• Use `/set-channel` to set your status channel\n' + + '• Use `/track` to add monitors\n' + + '• Use `/group-create` to create groups' + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + } + private async addGroup( interaction: ChatInputCommandInteraction, uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } const name = interaction.options.getString('name', true); - const success = configStorage.addGroup(name); + const success = configStorage.addGroup(interaction.guildId, name); if (success) { const embed = new EmbedBuilder() @@ -432,10 +558,14 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } const groupName = interaction.options.getString('group', true); - const success = configStorage.removeGroup(groupName); + const success = configStorage.removeGroup(interaction.guildId, groupName); if (success) { const embed = new EmbedBuilder() @@ -458,12 +588,16 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } const groupName = interaction.options.getString('group', true); const monitorIdStr = interaction.options.getString('monitor', true); const monitorId = parseInt(monitorIdStr, 10); - const success = configStorage.addMonitorToGroup(groupName, monitorId); + const success = configStorage.addMonitorToGroup(interaction.guildId, groupName, monitorId); if (success) { const monitors = uptimeKuma.getAllMonitors(); @@ -489,11 +623,15 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } const monitorIdStr = interaction.options.getString('monitor', true); const monitorId = parseInt(monitorIdStr, 10); - const success = configStorage.removeMonitorFromGroup(monitorId); + const success = configStorage.removeMonitorFromGroup(interaction.guildId, monitorId); if (success) { const monitors = uptimeKuma.getAllMonitors(); @@ -519,8 +657,12 @@ export class CommandsService { uptimeKuma: UptimeKumaService ): Promise { if (!await this.checkAdmin(interaction)) return; + if (!interaction.guildId) { + await interaction.reply({ content: '❌ This command can only be used in a server.', flags: MessageFlags.Ephemeral }); + return; + } - const groups = configStorage.getGroups(); + const groups = configStorage.getGroups(interaction.guildId); if (groups.length === 0) { await interaction.reply({ @@ -532,7 +674,7 @@ export class CommandsService { const monitors = uptimeKuma.getAllMonitors(); const embed = new EmbedBuilder() - .setColor(configStorage.getEmbedColor()) + .setColor(configStorage.getEmbedColor(interaction.guildId)) .setTitle('📋 Monitor Groups') .setTimestamp(); @@ -625,7 +767,12 @@ export class CommandsService { interaction: AutocompleteInteraction, query: string ): Promise { - const groups = configStorage.getGroups(); + if (!interaction.guildId) { + await interaction.respond([]); + return; + } + + const groups = configStorage.getGroups(interaction.guildId); const lowerQuery = query.toLowerCase(); const filtered = groups diff --git a/src/services/discord.service.ts b/src/services/discord.service.ts index 88f6966..8559a45 100644 --- a/src/services/discord.service.ts +++ b/src/services/discord.service.ts @@ -8,7 +8,7 @@ import { Logger } from '../utils/logger'; export class DiscordService { private client: Client; - private channel: TextChannel | null = null; + private channels: Map = new Map(); private logger: Logger; private maxMonitorsPerEmbed = 20; private commandsService: CommandsService; @@ -39,7 +39,7 @@ export class DiscordService { this.logger.info(`Discord bot logged in as ${client.user?.tag}`); try { - await this.initializeChannel(); + await this.initializeChannels(); await this.registerCommands(); this.setupCommandHandler(); resolve(); @@ -119,28 +119,39 @@ export class DiscordService { }); } - private async initializeChannel(): Promise { - const channelId = configStorage.getChannelId(); + private async initializeChannels(): Promise { + const guildIds = configStorage.getAllGuildIds(); - if (!channelId) { - throw new Error('No channel configured. Use /set-channel command to set one.'); + if (guildIds.length === 0) { + this.logger.info('No guilds configured yet'); + return; } - try { - const channel = await this.client.channels.fetch(channelId); + for (const guildId of guildIds) { + const channelId = configStorage.getChannelId(guildId); - if (!channel || !channel.isTextBased() || channel.isDMBased()) { - throw new Error('Invalid channel or channel is not a text channel'); + if (!channelId) { + this.logger.warn(`No channel configured for guild ${guildId}`); + continue; } - this.channel = channel as TextChannel; - this.logger.info(`Initialized channel: ${this.channel.name}`); - } catch (error: any) { - throw new Error(`Failed to initialize Discord channel: ${error.message}`); + try { + const channel = await this.client.channels.fetch(channelId); + + if (!channel || !channel.isTextBased() || channel.isDMBased()) { + this.logger.warn(`Invalid channel ${channelId} for guild ${guildId}`); + continue; + } + + this.channels.set(guildId, channel as TextChannel); + this.logger.info(`Initialized channel for guild ${guildId}: ${(channel as TextChannel).name}`); + } catch (error: any) { + this.logger.error(`Failed to initialize channel for guild ${guildId}: ${error.message}`); + } } } - public async setChannel(channelId: string): Promise { + public async setChannel(guildId: string, channelId: string): Promise { try { const channel = await this.client.channels.fetch(channelId); @@ -148,52 +159,66 @@ export class DiscordService { throw new Error('Invalid channel or channel is not a text channel'); } - this.channel = channel as TextChannel; - configStorage.setMessageIds([]); - configStorage.setChannelId(channelId); - this.logger.info(`Changed status channel to: ${this.channel.name}`); + const textChannel = channel as TextChannel; + this.channels.set(guildId, textChannel); + configStorage.setMessageIds(guildId, []); + configStorage.setChannelId(guildId, channelId); + this.logger.info(`Changed status channel for guild ${guildId} to: ${textChannel.name}`); } catch (error: any) { throw new Error(`Failed to set channel: ${error.message}`); } } public async updateMonitorStatus(monitors: MonitorStats[]): Promise { - if (!this.channel) { - this.logger.warn('Channel not initialized, skipping update'); + const guildIds = configStorage.getAllGuildIds(); + + if (guildIds.length === 0) { + this.logger.warn('No guilds configured, skipping update'); return; } - try { - const trackedIds = configStorage.getMonitorIds(); - const filteredMonitors = trackedIds.length === 0 - ? monitors - : monitors.filter(m => trackedIds.includes(m.monitor.id)); - - if (filteredMonitors.length === 0) { - this.logger.warn('No monitors to display after filtering'); - return; + for (const guildId of guildIds) { + // Skip if guild doesn't actually exist in config (shouldn't happen but safety check) + if (!configStorage.guildExists(guildId)) { + continue; } - const embeds = this.createEmbeds(filteredMonitors, monitors.length); - - const messageIds = configStorage.getMessageIds(); - if (messageIds.length === 0) { - await this.createNewMessages(embeds); - } else { - await this.updateExistingMessages(embeds); + const channel = this.channels.get(guildId); + if (!channel) { + continue; + } + + try { + const trackedIds = configStorage.getMonitorIds(guildId); + const filteredMonitors = trackedIds.length === 0 + ? monitors + : monitors.filter(m => trackedIds.includes(m.monitor.id)); + + if (filteredMonitors.length === 0) { + continue; + } + + const embeds = this.createEmbeds(guildId, filteredMonitors, monitors.length); + + const messageIds = configStorage.getMessageIds(guildId); + if (messageIds.length === 0) { + await this.createNewMessages(guildId, channel, embeds); + } else { + await this.updateExistingMessages(guildId, channel, embeds); + } + } catch (error: any) { + this.logger.error(`Failed to update monitor status for guild ${guildId}: ${error.message}`); } - } catch (error: any) { - this.logger.error(`Failed to update monitor status: ${error.message}`); } } - private createEmbeds(monitors: MonitorStats[], totalMonitors: number): EmbedBuilder[] { + private createEmbeds(guildId: string, monitors: MonitorStats[], totalMonitors: number): EmbedBuilder[] { const embeds: EmbedBuilder[] = []; - const statusMessage = configStorage.getStatusMessage(); - const groups = configStorage.getGroups(); + const statusMessage = configStorage.getStatusMessage(guildId); + const groups = configStorage.getGroups(guildId); const embed = new EmbedBuilder() - .setColor(configStorage.getEmbedColor()) + .setColor(configStorage.getEmbedColor(guildId)) .setTitle(statusMessage) .setTimestamp() .setFooter({ text: 'Last updated' }); @@ -306,39 +331,35 @@ export class DiscordService { `🔵 **Maintenance:** ${statusCounts.maintenance}`; } - private async createNewMessages(embeds: EmbedBuilder[]): Promise { - if (!this.channel) return; - + private async createNewMessages(guildId: string, channel: TextChannel, embeds: EmbedBuilder[]): Promise { const newMessageIds: string[] = []; for (const embed of embeds) { - const message = await this.channel.send({ embeds: [embed] }); + const message = await channel.send({ embeds: [embed] }); newMessageIds.push(message.id); } - configStorage.setMessageIds(newMessageIds); - this.logger.info(`Created ${embeds.length} new status message(s)`); + configStorage.setMessageIds(guildId, newMessageIds); + this.logger.info(`Created ${embeds.length} new status message(s) for guild ${guildId}`); } - private async updateExistingMessages(embeds: EmbedBuilder[]): Promise { - if (!this.channel) return; - - const messageIds = configStorage.getMessageIds(); + private async updateExistingMessages(guildId: string, channel: TextChannel, embeds: EmbedBuilder[]): Promise { + const messageIds = configStorage.getMessageIds(guildId); const newMessageIds: string[] = []; for (let i = 0; i < embeds.length; i++) { try { if (i < messageIds.length) { - const message = await this.channel.messages.fetch(messageIds[i]); + const message = await channel.messages.fetch(messageIds[i]); await message.edit({ embeds: [embeds[i]] }); newMessageIds.push(messageIds[i]); } else { - const message = await this.channel.send({ embeds: [embeds[i]] }); + const message = await channel.send({ embeds: [embeds[i]] }); newMessageIds.push(message.id); } } catch (error: any) { - this.logger.error(`Failed to update message: ${error.message}`); - configStorage.setMessageIds([]); - await this.createNewMessages(embeds); + this.logger.error(`Failed to update message for guild ${guildId}: ${error.message}`); + configStorage.setMessageIds(guildId, []); + await this.createNewMessages(guildId, channel, embeds); return; } } @@ -347,7 +368,7 @@ export class DiscordService { const toDelete = messageIds.slice(embeds.length); for (const messageId of toDelete) { try { - const message = await this.channel.messages.fetch(messageId); + const message = await channel.messages.fetch(messageId); await message.delete(); } catch (error: any) { this.logger.warn(`Failed to delete message ${messageId}: ${error.message}`); @@ -355,7 +376,7 @@ export class DiscordService { } } - configStorage.setMessageIds(newMessageIds); + configStorage.setMessageIds(guildId, newMessageIds); } private chunkArray(array: T[], size: number): T[][] {