add new announcement command and enhance user tracking with role and bot status
This commit is contained in:
56
src/commands/dateToEpoch.ts
Normal file
56
src/commands/dateToEpoch.ts
Normal 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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
203
src/commands/newAnnouncement.ts
Normal file
203
src/commands/newAnnouncement.ts
Normal 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(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user