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[]; monitorIds: number[];
} }
export interface BotConfig { export interface GuildConfig {
channelId: string | null; channelId: string | null;
messageIds: string[]; messageIds: string[];
monitorIds: number[]; monitorIds: number[];
@@ -17,9 +17,13 @@ export interface BotConfig {
statusMessage: string; statusMessage: string;
} }
export interface MultiGuildConfig {
guilds: Record<string, GuildConfig>;
}
export class ConfigStorage { export class ConfigStorage {
private configPath: string; private configPath: string;
private config: BotConfig; private config: MultiGuildConfig;
private logger: Logger; private logger: Logger;
constructor() { constructor() {
@@ -34,35 +38,38 @@ export class ConfigStorage {
this.config = this.load(); this.config = this.load();
} }
private load(): BotConfig { private load(): MultiGuildConfig {
if (existsSync(this.configPath)) { if (existsSync(this.configPath)) {
try { try {
const data = readFileSync(this.configPath, 'utf-8'); const data = readFileSync(this.configPath, 'utf-8');
const loaded = JSON.parse(data); 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'); this.logger.info('Loaded configuration from storage');
return { return loaded;
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',
};
} catch (error: any) { } catch (error: any) {
this.logger.error(`Failed to load config: ${error.message}`); this.logger.error(`Failed to load config: ${error.message}`);
} }
} }
return { return { guilds: {} };
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',
};
} }
private save(): void { private save(): void {
@@ -74,117 +81,179 @@ export class ConfigStorage {
} }
} }
public getMonitorIds(): number[] { private getDefaultConfig(): GuildConfig {
return [...this.config.monitorIds]; 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 { private getGuildConfig(guildId: string, createIfMissing: boolean = true): GuildConfig {
this.config.monitorIds = ids; if (!this.config.guilds[guildId]) {
this.save(); 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 { public guildExists(guildId: string): boolean {
if (!this.config.monitorIds.includes(id)) { return !!this.config.guilds[guildId];
this.config.monitorIds.push(id); }
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(); this.save();
return true; return true;
} }
return false; return false;
} }
public removeMonitor(id: number): boolean { public getMonitorIds(guildId: string): number[] {
const index = this.config.monitorIds.indexOf(id); 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) { if (index > -1) {
this.config.monitorIds.splice(index, 1); config.monitorIds.splice(index, 1);
this.save(); this.save();
return true; return true;
} }
return false; return false;
} }
public clearMonitors(): void { public clearMonitors(guildId: string): void {
this.config.monitorIds = []; const config = this.getGuildConfig(guildId);
config.monitorIds = [];
this.save(); this.save();
} }
public getUpdateInterval(): number { public getUpdateInterval(guildId: string): number {
return this.config.updateInterval; const config = this.getGuildConfig(guildId);
return config.updateInterval;
} }
public setUpdateInterval(interval: number): void { public setUpdateInterval(guildId: string, interval: number): void {
if (interval >= 10000) { if (interval >= 10000) {
this.config.updateInterval = interval; const config = this.getGuildConfig(guildId);
config.updateInterval = interval;
this.save(); this.save();
} }
} }
public getEmbedColor(): number { public getEmbedColor(guildId: string): number {
return this.config.embedColor; const config = this.getGuildConfig(guildId);
return config.embedColor;
} }
public setEmbedColor(color: number): void { public setEmbedColor(guildId: string, color: number): void {
this.config.embedColor = color; const config = this.getGuildConfig(guildId);
config.embedColor = color;
this.save(); this.save();
} }
public getConfig(): BotConfig { public getConfig(guildId: string): GuildConfig {
return { ...this.config }; return { ...this.getGuildConfig(guildId, false) }; // Don't auto-create when just reading
} }
public getChannelId(): string | null { public getChannelId(guildId: string): string | null {
return this.config.channelId; 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 { public setChannelId(guildId: string, channelId: string): void {
this.config.channelId = channelId; const config = this.getGuildConfig(guildId);
config.channelId = channelId;
this.save(); this.save();
} }
public getStatusMessage(): string { public getStatusMessage(guildId: string): string {
return this.config.statusMessage; const config = this.getGuildConfig(guildId);
return config.statusMessage;
} }
public setStatusMessage(message: string): void { public setStatusMessage(guildId: string, message: string): void {
this.config.statusMessage = message; const config = this.getGuildConfig(guildId);
config.statusMessage = message;
this.save(); this.save();
} }
public getMessageIds(): string[] { public getMessageIds(guildId: string): string[] {
return [...this.config.messageIds]; const config = this.getGuildConfig(guildId);
return [...config.messageIds];
} }
public setMessageIds(ids: string[]): void { public setMessageIds(guildId: string, ids: string[]): void {
this.config.messageIds = ids; const config = this.getGuildConfig(guildId);
config.messageIds = ids;
this.save(); this.save();
} }
public getGroups(): MonitorGroup[] { public getGroups(guildId: string): MonitorGroup[] {
return [...this.config.groups]; const config = this.getGuildConfig(guildId);
return [...config.groups];
} }
public addGroup(name: string): boolean { public addGroup(guildId: string, name: string): boolean {
if (this.config.groups.some(g => g.name.toLowerCase() === name.toLowerCase())) { const config = this.getGuildConfig(guildId);
if (config.groups.some(g => g.name.toLowerCase() === name.toLowerCase())) {
return false; return false;
} }
this.config.groups.push({ name, monitorIds: [] }); config.groups.push({ name, monitorIds: [] });
this.save(); this.save();
return true; return true;
} }
public removeGroup(name: string): boolean { public removeGroup(guildId: string, name: string): boolean {
const index = this.config.groups.findIndex(g => g.name.toLowerCase() === name.toLowerCase()); const config = this.getGuildConfig(guildId);
const index = config.groups.findIndex(g => g.name.toLowerCase() === name.toLowerCase());
if (index > -1) { if (index > -1) {
this.config.groups.splice(index, 1); config.groups.splice(index, 1);
this.save(); this.save();
return true; return true;
} }
return false; return false;
} }
public addMonitorToGroup(groupName: string, monitorId: number): boolean { public addMonitorToGroup(guildId: string, groupName: string, monitorId: number): boolean {
const group = this.config.groups.find(g => g.name.toLowerCase() === groupName.toLowerCase()); const config = this.getGuildConfig(guildId);
const group = config.groups.find(g => g.name.toLowerCase() === groupName.toLowerCase());
if (group && !group.monitorIds.includes(monitorId)) { 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); const idx = g.monitorIds.indexOf(monitorId);
if (idx > -1) { if (idx > -1) {
g.monitorIds.splice(idx, 1); g.monitorIds.splice(idx, 1);
@@ -197,9 +266,10 @@ export class ConfigStorage {
return false; return false;
} }
public removeMonitorFromGroup(monitorId: number): boolean { public removeMonitorFromGroup(guildId: string, monitorId: number): boolean {
const config = this.getGuildConfig(guildId);
let found = false; let found = false;
for (const group of this.config.groups) { for (const group of config.groups) {
const index = group.monitorIds.indexOf(monitorId); const index = group.monitorIds.indexOf(monitorId);
if (index > -1) { if (index > -1) {
group.monitorIds.splice(index, 1); group.monitorIds.splice(index, 1);
+9 -9
View File
@@ -89,23 +89,23 @@ class UptimeKumaDiscordBot {
return; return;
} }
const trackedIds = configStorage.getMonitorIds(); const monitors = this.uptimeKuma.getMonitorStats();
const monitors = this.uptimeKuma.getMonitorStats(trackedIds);
this.discord.updateMonitorStatus(monitors).catch(error => { this.discord.updateMonitorStatus(monitors).catch(error => {
this.logger.error(`Failed to update Discord status: ${error.message}`); this.logger.error(`Failed to update Discord status: ${error.message}`);
}); });
}; };
const interval = configStorage.getUpdateInterval(); // Use a default interval; guilds can have different intervals but we'll use a common update cycle
this.updateInterval = setInterval(updateFn, interval); 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(); const guildIds = configStorage.getAllGuildIds();
if (trackedIds.length > 0) { if (guildIds.length > 0) {
this.logger.info(`Tracking ${trackedIds.length} specific monitor(s): ${trackedIds.join(', ')}`); this.logger.info(`Configured for ${guildIds.length} guild(s)`);
} else { } 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'), .setDescription('Show current bot configuration'),
execute: this.showConfig.bind(this), 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) { for (const command of commands) {
@@ -190,11 +196,15 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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 monitorIdStr = interaction.options.getString('monitor', true);
const monitorId = parseInt(monitorIdStr, 10); const monitorId = parseInt(monitorIdStr, 10);
const currentIds = configStorage.getMonitorIds(); const currentIds = configStorage.getMonitorIds(interaction.guildId);
if (currentIds.includes(monitorId)) { if (currentIds.includes(monitorId)) {
await interaction.reply({ await interaction.reply({
content: '⚠️ This monitor is already being tracked.', content: '⚠️ This monitor is already being tracked.',
@@ -204,7 +214,7 @@ export class CommandsService {
} }
const newIds = [...currentIds, monitorId]; const newIds = [...currentIds, monitorId];
configStorage.setMonitorIds(newIds); configStorage.setMonitorIds(interaction.guildId, newIds);
const monitors = uptimeKuma.getAllMonitors(); const monitors = uptimeKuma.getAllMonitors();
const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`; const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`;
@@ -223,11 +233,15 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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 monitorIdStr = interaction.options.getString('monitor', true);
const monitorId = parseInt(monitorIdStr, 10); const monitorId = parseInt(monitorIdStr, 10);
const currentIds = configStorage.getMonitorIds(); const currentIds = configStorage.getMonitorIds(interaction.guildId);
const newIds = currentIds.filter(id => id !== monitorId); const newIds = currentIds.filter(id => id !== monitorId);
if (currentIds.length === newIds.length) { if (currentIds.length === newIds.length) {
@@ -238,7 +252,7 @@ export class CommandsService {
return; return;
} }
configStorage.setMonitorIds(newIds); configStorage.setMonitorIds(interaction.guildId, newIds);
const monitors = uptimeKuma.getAllMonitors(); const monitors = uptimeKuma.getAllMonitors();
const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`; const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`;
@@ -257,8 +271,12 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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() const embed = new EmbedBuilder()
.setColor(0x0099ff) .setColor(0x0099ff)
@@ -276,6 +294,10 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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 }); await interaction.deferReply({ flags: MessageFlags.Ephemeral });
@@ -284,7 +306,7 @@ export class CommandsService {
try { try {
const discordService = (interaction.client as any).discordService; const discordService = (interaction.client as any).discordService;
if (discordService) { if (discordService) {
await discordService.setChannel(channel.id); await discordService.setChannel(interaction.guildId, channel.id);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(0x00ff00) .setColor(0x00ff00)
@@ -308,10 +330,14 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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); const message = interaction.options.getString('message', true);
configStorage.setStatusMessage(message); configStorage.setStatusMessage(interaction.guildId, message);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(0x00ff00) .setColor(0x00ff00)
@@ -327,15 +353,19 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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 trackedIds = botConfig.monitorIds;
const channelId = botConfig.channelId; const channelId = botConfig.channelId;
const groups = configStorage.getGroups(); const groups = configStorage.getGroups(interaction.guildId);
const monitors = uptimeKuma.getAllMonitors(); const monitors = uptimeKuma.getAllMonitors();
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(configStorage.getEmbedColor()) .setColor(configStorage.getEmbedColor(interaction.guildId))
.setTitle('⚙️ Bot Configuration & Status') .setTitle('⚙️ Bot Configuration & Status')
.addFields( .addFields(
{ {
@@ -401,15 +431,111 @@ export class CommandsService {
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }); 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( private async addGroup(
interaction: ChatInputCommandInteraction, interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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 name = interaction.options.getString('name', true);
const success = configStorage.addGroup(name); const success = configStorage.addGroup(interaction.guildId, name);
if (success) { if (success) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
@@ -432,10 +558,14 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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 groupName = interaction.options.getString('group', true);
const success = configStorage.removeGroup(groupName); const success = configStorage.removeGroup(interaction.guildId, groupName);
if (success) { if (success) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
@@ -458,12 +588,16 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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 groupName = interaction.options.getString('group', true);
const monitorIdStr = interaction.options.getString('monitor', true); const monitorIdStr = interaction.options.getString('monitor', true);
const monitorId = parseInt(monitorIdStr, 10); const monitorId = parseInt(monitorIdStr, 10);
const success = configStorage.addMonitorToGroup(groupName, monitorId); const success = configStorage.addMonitorToGroup(interaction.guildId, groupName, monitorId);
if (success) { if (success) {
const monitors = uptimeKuma.getAllMonitors(); const monitors = uptimeKuma.getAllMonitors();
@@ -489,11 +623,15 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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 monitorIdStr = interaction.options.getString('monitor', true);
const monitorId = parseInt(monitorIdStr, 10); const monitorId = parseInt(monitorIdStr, 10);
const success = configStorage.removeMonitorFromGroup(monitorId); const success = configStorage.removeMonitorFromGroup(interaction.guildId, monitorId);
if (success) { if (success) {
const monitors = uptimeKuma.getAllMonitors(); const monitors = uptimeKuma.getAllMonitors();
@@ -519,8 +657,12 @@ export class CommandsService {
uptimeKuma: UptimeKumaService uptimeKuma: UptimeKumaService
): Promise<void> { ): Promise<void> {
if (!await this.checkAdmin(interaction)) return; 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) { if (groups.length === 0) {
await interaction.reply({ await interaction.reply({
@@ -532,7 +674,7 @@ export class CommandsService {
const monitors = uptimeKuma.getAllMonitors(); const monitors = uptimeKuma.getAllMonitors();
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(configStorage.getEmbedColor()) .setColor(configStorage.getEmbedColor(interaction.guildId))
.setTitle('📋 Monitor Groups') .setTitle('📋 Monitor Groups')
.setTimestamp(); .setTimestamp();
@@ -625,7 +767,12 @@ export class CommandsService {
interaction: AutocompleteInteraction, interaction: AutocompleteInteraction,
query: string query: string
): Promise<void> { ): Promise<void> {
const groups = configStorage.getGroups(); if (!interaction.guildId) {
await interaction.respond([]);
return;
}
const groups = configStorage.getGroups(interaction.guildId);
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
const filtered = groups const filtered = groups
+81 -60
View File
@@ -8,7 +8,7 @@ import { Logger } from '../utils/logger';
export class DiscordService { export class DiscordService {
private client: Client; private client: Client;
private channel: TextChannel | null = null; private channels: Map<string, TextChannel> = new Map();
private logger: Logger; private logger: Logger;
private maxMonitorsPerEmbed = 20; private maxMonitorsPerEmbed = 20;
private commandsService: CommandsService; private commandsService: CommandsService;
@@ -39,7 +39,7 @@ export class DiscordService {
this.logger.info(`Discord bot logged in as ${client.user?.tag}`); this.logger.info(`Discord bot logged in as ${client.user?.tag}`);
try { try {
await this.initializeChannel(); await this.initializeChannels();
await this.registerCommands(); await this.registerCommands();
this.setupCommandHandler(); this.setupCommandHandler();
resolve(); resolve();
@@ -119,28 +119,39 @@ export class DiscordService {
}); });
} }
private async initializeChannel(): Promise<void> { private async initializeChannels(): Promise<void> {
const channelId = configStorage.getChannelId(); const guildIds = configStorage.getAllGuildIds();
if (!channelId) { if (guildIds.length === 0) {
throw new Error('No channel configured. Use /set-channel command to set one.'); this.logger.info('No guilds configured yet');
return;
} }
try { for (const guildId of guildIds) {
const channel = await this.client.channels.fetch(channelId); const channelId = configStorage.getChannelId(guildId);
if (!channel || !channel.isTextBased() || channel.isDMBased()) { if (!channelId) {
throw new Error('Invalid channel or channel is not a text channel'); this.logger.warn(`No channel configured for guild ${guildId}`);
continue;
} }
this.channel = channel as TextChannel; try {
this.logger.info(`Initialized channel: ${this.channel.name}`); const channel = await this.client.channels.fetch(channelId);
} catch (error: any) {
throw new Error(`Failed to initialize Discord channel: ${error.message}`); 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 { try {
const channel = await this.client.channels.fetch(channelId); 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'); throw new Error('Invalid channel or channel is not a text channel');
} }
this.channel = channel as TextChannel; const textChannel = channel as TextChannel;
configStorage.setMessageIds([]); this.channels.set(guildId, textChannel);
configStorage.setChannelId(channelId); configStorage.setMessageIds(guildId, []);
this.logger.info(`Changed status channel to: ${this.channel.name}`); configStorage.setChannelId(guildId, channelId);
this.logger.info(`Changed status channel for guild ${guildId} to: ${textChannel.name}`);
} catch (error: any) { } catch (error: any) {
throw new Error(`Failed to set channel: ${error.message}`); throw new Error(`Failed to set channel: ${error.message}`);
} }
} }
public async updateMonitorStatus(monitors: MonitorStats[]): Promise<void> { public async updateMonitorStatus(monitors: MonitorStats[]): Promise<void> {
if (!this.channel) { const guildIds = configStorage.getAllGuildIds();
this.logger.warn('Channel not initialized, skipping update');
if (guildIds.length === 0) {
this.logger.warn('No guilds configured, skipping update');
return; return;
} }
try { for (const guildId of guildIds) {
const trackedIds = configStorage.getMonitorIds(); // Skip if guild doesn't actually exist in config (shouldn't happen but safety check)
const filteredMonitors = trackedIds.length === 0 if (!configStorage.guildExists(guildId)) {
? monitors continue;
: monitors.filter(m => trackedIds.includes(m.monitor.id));
if (filteredMonitors.length === 0) {
this.logger.warn('No monitors to display after filtering');
return;
} }
const embeds = this.createEmbeds(filteredMonitors, monitors.length); const channel = this.channels.get(guildId);
if (!channel) {
const messageIds = configStorage.getMessageIds(); continue;
if (messageIds.length === 0) { }
await this.createNewMessages(embeds);
} else { try {
await this.updateExistingMessages(embeds); 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 embeds: EmbedBuilder[] = [];
const statusMessage = configStorage.getStatusMessage(); const statusMessage = configStorage.getStatusMessage(guildId);
const groups = configStorage.getGroups(); const groups = configStorage.getGroups(guildId);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(configStorage.getEmbedColor()) .setColor(configStorage.getEmbedColor(guildId))
.setTitle(statusMessage) .setTitle(statusMessage)
.setTimestamp() .setTimestamp()
.setFooter({ text: 'Last updated' }); .setFooter({ text: 'Last updated' });
@@ -306,39 +331,35 @@ export class DiscordService {
`🔵 **Maintenance:** ${statusCounts.maintenance}`; `🔵 **Maintenance:** ${statusCounts.maintenance}`;
} }
private async createNewMessages(embeds: EmbedBuilder[]): Promise<void> { private async createNewMessages(guildId: string, channel: TextChannel, embeds: EmbedBuilder[]): Promise<void> {
if (!this.channel) return;
const newMessageIds: string[] = []; const newMessageIds: string[] = [];
for (const embed of embeds) { for (const embed of embeds) {
const message = await this.channel.send({ embeds: [embed] }); const message = await channel.send({ embeds: [embed] });
newMessageIds.push(message.id); newMessageIds.push(message.id);
} }
configStorage.setMessageIds(newMessageIds); configStorage.setMessageIds(guildId, newMessageIds);
this.logger.info(`Created ${embeds.length} new status message(s)`); this.logger.info(`Created ${embeds.length} new status message(s) for guild ${guildId}`);
} }
private async updateExistingMessages(embeds: EmbedBuilder[]): Promise<void> { private async updateExistingMessages(guildId: string, channel: TextChannel, embeds: EmbedBuilder[]): Promise<void> {
if (!this.channel) return; const messageIds = configStorage.getMessageIds(guildId);
const messageIds = configStorage.getMessageIds();
const newMessageIds: string[] = []; const newMessageIds: string[] = [];
for (let i = 0; i < embeds.length; i++) { for (let i = 0; i < embeds.length; i++) {
try { try {
if (i < messageIds.length) { 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]] }); await message.edit({ embeds: [embeds[i]] });
newMessageIds.push(messageIds[i]); newMessageIds.push(messageIds[i]);
} else { } else {
const message = await this.channel.send({ embeds: [embeds[i]] }); const message = await channel.send({ embeds: [embeds[i]] });
newMessageIds.push(message.id); newMessageIds.push(message.id);
} }
} catch (error: any) { } catch (error: any) {
this.logger.error(`Failed to update message: ${error.message}`); this.logger.error(`Failed to update message for guild ${guildId}: ${error.message}`);
configStorage.setMessageIds([]); configStorage.setMessageIds(guildId, []);
await this.createNewMessages(embeds); await this.createNewMessages(guildId, channel, embeds);
return; return;
} }
} }
@@ -347,7 +368,7 @@ export class DiscordService {
const toDelete = messageIds.slice(embeds.length); const toDelete = messageIds.slice(embeds.length);
for (const messageId of toDelete) { for (const messageId of toDelete) {
try { try {
const message = await this.channel.messages.fetch(messageId); const message = await channel.messages.fetch(messageId);
await message.delete(); await message.delete();
} catch (error: any) { } catch (error: any) {
this.logger.warn(`Failed to delete message ${messageId}: ${error.message}`); 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[][] { private chunkArray<T>(array: T[], size: number): T[][] {