From 05adc62b813911f45f7f795757e05996c3e08f5e Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Sun, 22 Feb 2026 15:45:48 +0100 Subject: [PATCH] add update announcement command to modify existing announcements with enhanced options and permissions --- src/commands/updateAnnouncement.ts | 222 +++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/commands/updateAnnouncement.ts diff --git a/src/commands/updateAnnouncement.ts b/src/commands/updateAnnouncement.ts new file mode 100644 index 0000000..8dcc00f --- /dev/null +++ b/src/commands/updateAnnouncement.ts @@ -0,0 +1,222 @@ +import { Client, SlashCommandBuilder } from "discord.js"; +import { registerCommand } from "../lib/commandRegistry"; +import { CHANNELS, ROLES } from "../config"; +import { db } from ".."; + +export default async function (_client: Client) { + registerCommand({ + data: new SlashCommandBuilder() + .setName("update-announcement") + .setDescription("Updates an existing announcement") + .addStringOption((option) => + option + .setName("message_id") + .setDescription( + "The Discord message ID of the announcement to update", + ) + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("title") + .setDescription("New title for the announcement") + .setRequired(false), + ) + .addStringOption((option) => + option + .setName("content") + .setDescription("New content for the announcement") + .setRequired(false), + ) + .addStringOption((option) => + option + .setName("changes") + .setDescription( + "New comma-separated list of changes (replaces existing changes)", + ) + .setRequired(false), + ) + .addStringOption((option) => + option + .setName("coming_when") + .setDescription( + "New release date: DD/MM/YYYY or an epoch timestamp", + ) + .setRequired(false), + ) + .addStringOption((option) => + option + .setName("link") + .setDescription("New link for the announcement") + .setRequired(false), + ) as SlashCommandBuilder, + async execute(interaction) { + const memberRoles = interaction.member?.roles; + let isMaintainer = false; + if (memberRoles) { + if (typeof memberRoles === "object" && "cache" in memberRoles) { + isMaintainer = memberRoles.cache.has(ROLES.MAINTAINERS); + } else if (Array.isArray(memberRoles)) { + isMaintainer = memberRoles.includes(ROLES.MAINTAINERS); + } + } + + if (!isMaintainer) { + await interaction.reply({ + ephemeral: true, + content: "You do not have permission to use this command.", + }); + return; + } + + const messageId = interaction.options.getString("message_id", true); + + // Look up the original announcement in DB + const existing = await db + .collection("announcements") + .findOne({ messageId }); + + if (!existing) { + await interaction.reply({ + ephemeral: true, + content: + "Announcement not found. Make sure the message ID is correct and was created with `/new-announcement`.", + }); + return; + } + + await interaction.deferReply({ ephemeral: true }); + + // Resolve updated values, falling back to existing ones + const newTitle = + interaction.options.getString("title", false) ?? existing.title; + let newContent = + interaction.options.getString("content", false) ?? + existing.content; + const changesRaw = + interaction.options.getString("changes", false) ?? + existing.changes; + const comingWhenRaw = + interaction.options.getString("coming_when", false) ?? + existing.comingWhen; + let link = + interaction.options.getString("link", false) ?? + existing.link ?? + "https://github.com/Space-Banane/shsf"; + if (!link || link === "") + link = "https://github.com/Space-Banane/shsf"; + + // Post Process Content + newContent = newContent.replace(/\\n/g, "\n"); // allow users to input \n for newlines + + const fields = []; + + if (changesRaw && changesRaw.trim() !== "") { + const changeLines = changesRaw + .split(",") + .map((c: string) => c.trim()) + .filter((c: string) => c.length > 0) + .map((c: string) => `• ${c}`) + .join("\n"); + fields.push({ + name: "📋 Changes", + value: changeLines, + inline: false, + }); + } + + if (comingWhenRaw && comingWhenRaw.trim() !== "") { + let epoch: number | null = null; + const trimmed = comingWhenRaw.trim(); + const ddmmyyyy = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(trimmed); + if (ddmmyyyy) { + const [, dd, mm, yyyy] = ddmmyyyy; + const date = new Date( + Number(yyyy), + Number(mm) - 1, + Number(dd), + ); + if (!isNaN(date.getTime())) + epoch = Math.floor(date.getTime() / 1000); + } else if (/^\d+$/.test(trimmed)) { + epoch = Number(trimmed); + } + if (epoch !== null) { + fields.push({ + name: "📅 Coming On", + value: ` ()`, + inline: false, + }); + } else { + fields.push({ + name: "📅 Coming On", + value: trimmed, + inline: false, + }); + } + } + + fields.push({ + name: "🔗 More Info", + value: `[Click here](${link})`, + inline: false, + }); + + const updatedEmbed = { + title: `📢 ${newTitle}`, + description: newContent, + color: 0x5865f2, + fields, + footer: { + text: `Announced by ${existing.announcedByUsername ?? interaction.user.username} • Last edited by ${interaction.user.username}`, + icon_url: interaction.user.displayAvatarURL(), + }, + timestamp: new Date().toISOString(), + }; + + // Fetch and edit the original message + const announcementChannel = interaction.guild?.channels.cache.get( + CHANNELS.ANNOUNCEMENTS, + ); + + if (!announcementChannel?.isTextBased()) { + await interaction.editReply({ + content: "Could not find the announcements channel.", + }); + return; + } + + let targetMessage; + try { + targetMessage = + await announcementChannel.messages.fetch(messageId); + } catch { + await interaction.editReply({ + content: + "Could not fetch the message. Make sure the message ID is correct.", + }); + return; + } + + await targetMessage.edit({ embeds: [updatedEmbed] }); + + // Update DB + await db.collection("announcements").updateOne( + { messageId }, + { + $set: { + title: newTitle, + content: newContent, + changes: changesRaw ?? null, + comingWhen: comingWhenRaw ?? null, + link, + lastEditedBy: interaction.user.id, + lastEditedAt: new Date(), + }, + }, + ); + + await interaction.editReply({ content: "Announcement updated!" }); + }, + }); +}