From bc58cb7361d4bfb3c545198b40c8ada08c6e3fcf Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Sun, 22 Feb 2026 15:31:20 +0100 Subject: [PATCH] add new announcement command and enhance user tracking with role and bot status --- src/commands/dateToEpoch.ts | 56 +++++++ src/commands/newAnnouncement.ts | 203 ++++++++++++++++++++++++ src/config.ts | 3 + src/listeners/GuildMemberAdd/addUser.ts | 32 ++-- 4 files changed, 280 insertions(+), 14 deletions(-) create mode 100644 src/commands/dateToEpoch.ts create mode 100644 src/commands/newAnnouncement.ts diff --git a/src/commands/dateToEpoch.ts b/src/commands/dateToEpoch.ts new file mode 100644 index 0000000..746534d --- /dev/null +++ b/src/commands/dateToEpoch.ts @@ -0,0 +1,56 @@ +import { Client, SlashCommandBuilder } from "discord.js"; +import { registerCommand } from "../lib/commandRegistry"; + +export default async function (_client: Client) { + registerCommand({ + data: new SlashCommandBuilder() + .setName("date-to-epoch") + .setDescription("Converts a date (DD/MM/YYYY) to a Unix epoch timestamp") + .addStringOption((option) => + option + .setName("date") + .setDescription("Date in DD/MM/YYYY format (e.g. 25/12/2026)") + .setRequired(true) + ) as SlashCommandBuilder, + async execute(interaction) { + const input = interaction.options.getString("date", true).trim(); + + const ddmmyyyy = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(input); + if (!ddmmyyyy) { + await interaction.reply({ + ephemeral: true, + content: "❌ Invalid format. Please use `DD/MM/YYYY` (e.g. `25/12/2026`).", + }); + return; + } + + const [, dd, mm, yyyy] = ddmmyyyy; + const date = new Date(Number(yyyy), Number(mm) - 1, Number(dd)); + + if (isNaN(date.getTime())) { + await interaction.reply({ + ephemeral: true, + content: "❌ The date you provided is invalid.", + }); + return; + } + + const epoch = Math.floor(date.getTime() / 1000); + + await interaction.reply({ + ephemeral: true, + embeds: [ + { + title: "📅 Date → Epoch", + color: 0x5865f2, + fields: [ + { name: "Input", value: input, inline: true }, + { name: "Epoch", value: `\`${epoch}\``, inline: true }, + { name: "Preview", value: ` ()`, inline: false }, + ], + }, + ], + }); + }, + }); +} diff --git a/src/commands/newAnnouncement.ts b/src/commands/newAnnouncement.ts new file mode 100644 index 0000000..80d794c --- /dev/null +++ b/src/commands/newAnnouncement.ts @@ -0,0 +1,203 @@ +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("new-announcement") + .setDescription("Creates a new announcement") + .addStringOption((option) => + option + .setName("title") + .setDescription("The title of the announcement") + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("content") + .setDescription("The content of the announcement") + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("changes") + .setDescription( + "Comma-separated list of changes (e.g. Fixed bug, Added feature, Improved UI)", + ) + .setRequired(false), + ) + .addAttachmentOption((option) => + option + .setName("image") + .setDescription( + "An optional image to include before the announcement", + ) + .setRequired(false), + ) + .addStringOption((option) => + option + .setName("coming_when") + .setDescription( + "Release date: DD/MM/YYYY or an epoch timestamp", + ) + .setRequired(false), + ) + .addStringOption((option) => + option + .setName("link") + .setDescription( + "An optional link to include in 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) { + // GuildMemberRoleManager + isMaintainer = memberRoles.cache.has(ROLES.MAINTAINERS); + } else if (Array.isArray(memberRoles)) { + // string[] + isMaintainer = memberRoles.includes(ROLES.MAINTAINERS); + } + } + if (isMaintainer) { + await interaction.reply({ + ephemeral: true, + content: "Announcement created!", + }); + + const title = interaction.options.getString("title", true); + const content = interaction.options.getString("content", true); + const changesRaw = interaction.options.getString( + "changes", + false, + ); + const image = interaction.options.getAttachment("image", false); + const comingWhenRaw = interaction.options.getString( + "coming_when", + false, + ); + let link = interaction.options.getString("link", false); + if (!link || link === "") { + link = "https://github.com/Space-Banane/shsf"; + } + + const fields = []; + + if (changesRaw && changesRaw.trim() !== "") { + const changeLines = changesRaw + .split(",") + .map((c) => c.trim()) + .filter((c) => c.length > 0) + .map((c) => `• ${c}`) + .join("\n"); + fields.push({ + name: "📋 Changes", + value: changeLines, + inline: false, + }); + } + + if (comingWhenRaw && comingWhenRaw.trim() !== "") { + let epoch: number | null = null; + const trimmed = comingWhenRaw.trim(); + // Try DD/MM/YYYY + 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)) { + // Raw epoch + 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 announcementEmbed = { + title: `📢 ${title}`, + description: content, + color: 0x5865f2, + fields, + footer: { + text: `Announced by ${interaction.user.username}`, + icon_url: interaction.user.displayAvatarURL(), + }, + timestamp: new Date().toISOString(), + }; + + // Send the announcement to a specific channel + const announcementChannel = + interaction.guild?.channels.cache.get( + CHANNELS.ANNOUNCEMENTS, + ); + if (announcementChannel?.isTextBased()) { + if (image) { + await announcementChannel.send({ files: [image.url] }); + } + const message = await announcementChannel.send({ + embeds: [announcementEmbed], + }); + + // Reactions for engagement + await message.react("👍"); + await message.react("🔥"); + await message.react("👎"); + } else { + console.error( + "Announcement channel not found or is not text-based.", + ); + } + } else { + await interaction.reply({ + ephemeral: true, + content: "You do not have permission to use this command.", + }); + } + + // Add to database + await db.collection("announcements").insertOne({ + title: interaction.options.getString("title", true), + content: interaction.options.getString("content", true), + changes: interaction.options.getString("changes", false), + imageUrl: + interaction.options.getAttachment("image", false)?.url || + null, + comingWhen: + interaction.options.getString("coming_when", false) || null, + link: interaction.options.getString("link", false) || null, + announcedBy: interaction.user.id, + announcedAt: new Date(), + }); + }, + }); +} diff --git a/src/config.ts b/src/config.ts index 987421a..8ae6524 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,6 +18,9 @@ export const CHANNELS = { export const ROLES = { RULES: "1475100051352191047", + MAINTAINERS: "1475099468591272090", + COMMUNITY_ADMINS: "1475099507527258218", + I_USE_SHSF: "1475099569019949077" }; // Discord modified the way activities work, for now, we'll only use custom ones diff --git a/src/listeners/GuildMemberAdd/addUser.ts b/src/listeners/GuildMemberAdd/addUser.ts index 98df5ae..e2aff15 100644 --- a/src/listeners/GuildMemberAdd/addUser.ts +++ b/src/listeners/GuildMemberAdd/addUser.ts @@ -12,25 +12,29 @@ export default async function addUserToDB(client: Client): Promise { content: member.user.username, guildId: member.guild.id, timestamp: new Date(member.joinedTimestamp!), + isBot: member.user.bot, }); }); } // Silenced becuase its very noisy on every message export async function addUserToDBManually(member: GuildMember): Promise { - // console.log(`[addUserToDBManually] [${member.user.tag}] Adding user to DB manually`); + // console.log(`[addUserToDBManually] [${member.user.tag}] Adding user to DB manually`); - const existingUser = await db.collection("users").findOne({ userId: member.user.id }); - if (existingUser) { - // console.log(`[addUserToDBManually] [${member.user.tag}] User already exists in DB, skipping.`); - return; - } + const existingUser = await db + .collection("users") + .findOne({ userId: member.user.id }); + if (existingUser) { + // console.log(`[addUserToDBManually] [${member.user.tag}] User already exists in DB, skipping.`); + return; + } - await db.collection("users").insertOne({ - userId: member.user.id, - authorTag: member.user.tag, - content: member.user.username, - guildId: member.guild.id, - timestamp: new Date(member.joinedTimestamp!), - }); -} \ No newline at end of file + await db.collection("users").insertOne({ + userId: member.user.id, + authorTag: member.user.tag, + content: member.user.username, + guildId: member.guild.id, + timestamp: new Date(member.joinedTimestamp!), + isBot: member.user.bot, + }); +}