mirror of
https://github.com/BrenBroZAYT/uptime-kuma-discord-bot.git
synced 2026-06-13 16:40:03 +00:00
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:
+130
-60
@@ -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,13 +38,18 @@ 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);
|
||||
this.logger.info('Loaded configuration from storage');
|
||||
|
||||
// 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 || [],
|
||||
@@ -48,21 +57,19 @@ export class ConfigStorage {
|
||||
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 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;
|
||||
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
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (guildIds.length === 0) {
|
||||
this.logger.info('No guilds configured yet');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const guildId of guildIds) {
|
||||
const channelId = configStorage.getChannelId(guildId);
|
||||
|
||||
if (!channelId) {
|
||||
throw new Error('No channel configured. Use /set-channel command to set one.');
|
||||
this.logger.warn(`No channel configured for guild ${guildId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const channel = await this.client.channels.fetch(channelId);
|
||||
|
||||
if (!channel || !channel.isTextBased() || channel.isDMBased()) {
|
||||
throw new Error('Invalid channel or channel is not a text channel');
|
||||
this.logger.warn(`Invalid channel ${channelId} for guild ${guildId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
this.channel = channel as TextChannel;
|
||||
this.logger.info(`Initialized channel: ${this.channel.name}`);
|
||||
this.channels.set(guildId, channel as TextChannel);
|
||||
this.logger.info(`Initialized channel for guild ${guildId}: ${(channel as TextChannel).name}`);
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to initialize Discord channel: ${error.message}`);
|
||||
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;
|
||||
}
|
||||
|
||||
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 channel = this.channels.get(guildId);
|
||||
if (!channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const trackedIds = configStorage.getMonitorIds();
|
||||
const trackedIds = configStorage.getMonitorIds(guildId);
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
|
||||
const embeds = this.createEmbeds(filteredMonitors, monitors.length);
|
||||
const embeds = this.createEmbeds(guildId, filteredMonitors, monitors.length);
|
||||
|
||||
const messageIds = configStorage.getMessageIds();
|
||||
const messageIds = configStorage.getMessageIds(guildId);
|
||||
if (messageIds.length === 0) {
|
||||
await this.createNewMessages(embeds);
|
||||
await this.createNewMessages(guildId, channel, embeds);
|
||||
} else {
|
||||
await this.updateExistingMessages(embeds);
|
||||
await this.updateExistingMessages(guildId, channel, embeds);
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to update monitor status: ${error.message}`);
|
||||
this.logger.error(`Failed to update monitor status for guild ${guildId}: ${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[][] {
|
||||
|
||||
Reference in New Issue
Block a user