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.
This commit is contained in:
Shrev Dev
2025-10-12 11:57:54 -05:00
parent 2ef2530816
commit 47c0d28058
4 changed files with 393 additions and 155 deletions
+139 -69
View File
@@ -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<string, GuildConfig>;
}
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);
+9 -9
View File
@@ -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.');
}
}
+164 -17
View File
@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
const groups = configStorage.getGroups();
if (!interaction.guildId) {
await interaction.respond([]);
return;
}
const groups = configStorage.getGroups(interaction.guildId);
const lowerQuery = query.toLowerCase();
const filtered = groups
+81 -60
View File
@@ -8,7 +8,7 @@ import { Logger } from '../utils/logger';
export class DiscordService {
private client: Client;
private channel: TextChannel | null = null;
private channels: Map<string, TextChannel> = 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<void> {
const channelId = configStorage.getChannelId();
private async initializeChannels(): Promise<void> {
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<void> {
public async setChannel(guildId: string, channelId: string): Promise<void> {
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<void> {
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<void> {
if (!this.channel) return;
private async createNewMessages(guildId: string, channel: TextChannel, embeds: EmbedBuilder[]): Promise<void> {
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<void> {
if (!this.channel) return;
const messageIds = configStorage.getMessageIds();
private async updateExistingMessages(guildId: string, channel: TextChannel, embeds: EmbedBuilder[]): Promise<void> {
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<T>(array: T[], size: number): T[][] {