add new announcement command and enhance user tracking with role and bot status

This commit is contained in:
Space-Banane
2026-02-22 15:31:20 +01:00
parent 6da42e830d
commit bc58cb7361
4 changed files with 280 additions and 14 deletions

View File

@@ -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: `<t:${epoch}:F> (<t:${epoch}:R>)`, inline: false },
],
},
],
});
},
});
}

View File

@@ -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: `<t:${epoch}:F> (<t:${epoch}:R>)`,
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(),
});
},
});
}

View File

@@ -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

View File

@@ -12,25 +12,29 @@ export default async function addUserToDB(client: Client): Promise<void> {
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<void> {
// 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!),
});
}
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,
});
}