require("dotenv").config(); const { Client, GatewayIntentBits, EmbedBuilder } = require("discord.js"); const UptimeKuma = require("uptimekuma-api"); const cron = require("node-cron"); const subs = require("./subs.json"); const KUMA_BASE_URL = process.env.KUMA_BASE_URL; const DISCORD_TOKEN = process.env.TOKEN; const CRON_SCHEDULE = "*/5 * * * *"; const STATUS_EMOJIS = { online: "<:online:1307359583785713744>", offline: "<:offline:1307359600592424971>", degraded: "<:degraded:1307359619491954778>", }; const STATUS_INFO = { operational: { text: "All Systems Operational", color: 0x43b581, // Green emoji: STATUS_EMOJIS.online, }, degraded: { text: "Degraded Service", color: 0xfaa61a, // Orange emoji: STATUS_EMOJIS.degraded, }, outage: { text: "Full Outage", color: 0xf04747, // Red emoji: STATUS_EMOJIS.offline, }, }; const kuma = new UptimeKuma(KUMA_BASE_URL); const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], }); async function createStatusEmbed(statusPageSlug) { try { const statusData = await kuma.status(statusPageSlug); const embed = new EmbedBuilder() .setTitle(statusData.info.title) .setThumbnail(statusData.info.icon); let totalMonitors = 0; let offlineMonitors = 0; for (const category of statusData.status) { let fieldText = ""; for (const monitor of category.monitors) { totalMonitors++; const isOnline = monitor.heartbeats[0]?.status === 1; if (!isOnline) { offlineMonitors++; } const emoji = isOnline ? STATUS_EMOJIS.online : STATUS_EMOJIS.offline; fieldText += `${emoji} ${monitor.name}\n`; } // Discord embed fields have a 1024 character limit. // This splits the text into multiple fields if necessary. const fieldChunks = fieldText.match(/[\s\S]{1,1024}/g) || []; fieldChunks.forEach((chunk, index) => { embed.addFields({ name: index === 0 ? category.name : `${category.name} (cont.)`, value: chunk, }); }); } const status = determineOverallStatus(offlineMonitors, totalMonitors); const description = statusData.info.description ? `${statusData.info.description}\n\n${status.emoji} ${status.text}` : `${status.emoji} ${status.text}`; embed .setDescription(description) .setColor(status.color) .setTimestamp(new Date()); return embed; } catch (error) { console.error(`Error fetching status for "${statusPageSlug}":`, error); return new EmbedBuilder() .setColor(STATUS_INFO.outage.color) .setDescription("An error occurred while fetching status data."); } } function determineOverallStatus(offlineCount, totalCount) { if (offlineCount === 0) { return STATUS_INFO.operational; } if (offlineCount === totalCount) { return STATUS_INFO.outage; } return STATUS_INFO.degraded; } async function findOrCreateStatusMessage(channel) { const messages = await channel.messages.fetch({ limit: 25 }); let message = messages.find((msg) => msg.author.id === client.user.id); if (!message) { const loadingEmbed = new EmbedBuilder().setDescription("Initializing status panel..."); message = await channel.send({ embeds: [loadingEmbed] }); } return message; } async function updateAllStatusMessages() { console.log("Updating all status messages..."); for (const [channelId, slug] of Object.entries(subs)) { try { const channel = await client.channels.fetch(channelId); if (!channel || !channel.isTextBased()) { console.warn(`Channel ${channelId} not found or is not a text channel.`); continue; } const message = await findOrCreateStatusMessage(channel); const embed = await createStatusEmbed(slug); await message.edit({ embeds: [embed] }); } catch (error) { console.error(`Failed to update status for channel ${channelId}:`, error); } } console.log("Finished updating status messages."); } client.once("ready", async () => { console.log(`Logged in as ${client.user.tag}`); await updateAllStatusMessages(); // Initial update on startup cron.schedule(CRON_SCHEDULE, updateAllStatusMessages); console.log(`Scheduled status updates with cron schedule: ${CRON_SCHEDULE}`); }); client.login(DISCORD_TOKEN);