This commit is contained in:
@@ -2,55 +2,68 @@ 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();
|
||||
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 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));
|
||||
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;
|
||||
}
|
||||
if (isNaN(date.getTime())) {
|
||||
await interaction.reply({
|
||||
ephemeral: true,
|
||||
content: "❌ The date you provided is invalid.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const epoch = Math.floor(date.getTime() / 1000);
|
||||
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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Client, SlashCommandBuilder } from "discord.js";
|
||||
import { registerCommand } from "../lib/commandRegistry";
|
||||
|
||||
export default async function (_client: Client) {
|
||||
registerCommand({
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("ping")
|
||||
.setDescription("Replies with Pong!"),
|
||||
async execute(interaction) {
|
||||
await interaction.reply("Pong!");
|
||||
},
|
||||
});
|
||||
registerCommand({
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("ping")
|
||||
.setDescription("Replies with Pong!"),
|
||||
async execute(interaction) {
|
||||
await interaction.reply("Pong!");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { ActivityType } from "discord.js";
|
||||
|
||||
|
||||
|
||||
export const GUILD_ID = '1475098530505953441';
|
||||
|
||||
export const GUILD_ID = "1475098530505953441";
|
||||
|
||||
export const CHANNELS = {
|
||||
RULES: "1475100731991392539",
|
||||
@@ -20,13 +17,13 @@ export const ROLES = {
|
||||
RULES: "1475100051352191047",
|
||||
MAINTAINERS: "1475099468591272090",
|
||||
COMMUNITY_ADMINS: "1475099507527258218",
|
||||
I_USE_SHSF: "1475099569019949077"
|
||||
I_USE_SHSF: "1475099569019949077",
|
||||
};
|
||||
|
||||
// Discord modified the way activities work, for now, we'll only use custom ones
|
||||
export const ROTATE_ACTIVITIES:{content:string;type:ActivityType}[] = [
|
||||
{content: "Fixing Bugs", type: ActivityType.Custom},
|
||||
{content: "Adding New Features", type: ActivityType.Custom},
|
||||
{content: "Improving Performance", type: ActivityType.Custom},
|
||||
{content: "Listening to Feedback", type: ActivityType.Custom},
|
||||
]
|
||||
export const ROTATE_ACTIVITIES: { content: string; type: ActivityType }[] = [
|
||||
{ content: "Fixing Bugs", type: ActivityType.Custom },
|
||||
{ content: "Adding New Features", type: ActivityType.Custom },
|
||||
{ content: "Improving Performance", type: ActivityType.Custom },
|
||||
{ content: "Listening to Feedback", type: ActivityType.Custom },
|
||||
];
|
||||
|
||||
@@ -3,34 +3,40 @@ import { getAllCommands, getCommand } from "../lib/commandRegistry";
|
||||
import { GUILD_ID } from "../config";
|
||||
|
||||
export default async function (client: Client) {
|
||||
const token = process.env.DISCORD_TOKEN!;
|
||||
const clientId = client.user!.id; // use the ready client's ID
|
||||
const token = process.env.DISCORD_TOKEN!;
|
||||
const clientId = client.user!.id; // use the ready client's ID
|
||||
|
||||
const rest = new REST().setToken(token);
|
||||
const rest = new REST().setToken(token);
|
||||
|
||||
const commandData = getAllCommands().map((cmd) => cmd.data.toJSON());
|
||||
const commandData = getAllCommands().map((cmd) => cmd.data.toJSON());
|
||||
|
||||
await rest.put(Routes.applicationGuildCommands(clientId, GUILD_ID), {
|
||||
body: commandData,
|
||||
});
|
||||
await rest.put(Routes.applicationGuildCommands(clientId, GUILD_ID), {
|
||||
body: commandData,
|
||||
});
|
||||
|
||||
console.log(`Registered ${commandData.length} slash command(s).`);
|
||||
console.log(`Registered ${commandData.length} slash command(s).`);
|
||||
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = getCommand(interaction.commandName);
|
||||
if (!command) return;
|
||||
const command = getCommand(interaction.commandName);
|
||||
if (!command) return;
|
||||
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: "An error occurred.", ephemeral: true });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred.", ephemeral: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({
|
||||
content: "An error occurred.",
|
||||
ephemeral: true,
|
||||
});
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: "An error occurred.",
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,155 +1,152 @@
|
||||
import {
|
||||
Client,
|
||||
EmbedBuilder,
|
||||
Events,
|
||||
GuildMember,
|
||||
Partials,
|
||||
TextChannel,
|
||||
Client,
|
||||
EmbedBuilder,
|
||||
Events,
|
||||
GuildMember,
|
||||
Partials,
|
||||
TextChannel,
|
||||
} from "discord.js";
|
||||
import { CHANNELS, GUILD_ID, ROLES } from "../../config";
|
||||
|
||||
const RULES_EMOJI = "✅";
|
||||
|
||||
export default async function checkRules(client: Client): Promise<void> {
|
||||
const guild = await client.guilds.fetch(GUILD_ID);
|
||||
const channel = (await guild.channels.fetch(
|
||||
CHANNELS["RULES"],
|
||||
)) as TextChannel;
|
||||
const guild = await client.guilds.fetch(GUILD_ID);
|
||||
const channel = (await guild.channels.fetch(
|
||||
CHANNELS["RULES"],
|
||||
)) as TextChannel;
|
||||
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
console.error(
|
||||
"[checkRules] Rules channel not found or is not a text channel.",
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
console.error(
|
||||
"[checkRules] Rules channel not found or is not a text channel.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for an existing rules message posted by the bot
|
||||
const messages = await channel.messages.fetch({ limit: 50 });
|
||||
const existingMessage = messages.find(
|
||||
(m) => m.author.id === client.user!.id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for an existing rules message posted by the bot
|
||||
const messages = await channel.messages.fetch({ limit: 50 });
|
||||
const existingMessage = messages.find((m) => m.author.id === client.user!.id);
|
||||
if (!existingMessage) {
|
||||
const embed1 = new EmbedBuilder()
|
||||
.setTitle("📜 Server Rules — Part 1: General Conduct")
|
||||
.setColor(0x5865f2)
|
||||
.setDescription(
|
||||
"Please read and follow all rules to keep this server a safe and welcoming place for everyone.",
|
||||
)
|
||||
.addFields(
|
||||
{
|
||||
name: "1. Be Respectful",
|
||||
value: "Treat all members with respect. Harassment, hate speech, slurs, and discrimination of any kind will not be tolerated.",
|
||||
},
|
||||
{
|
||||
name: "2. No Spam",
|
||||
value: "Do not spam messages, emojis, or mentions. Keep conversations relevant to the channel topic.",
|
||||
},
|
||||
{
|
||||
name: "3. No NSFW Content",
|
||||
value: "Explicit, graphic, or otherwise inappropriate content is strictly prohibited.",
|
||||
},
|
||||
{
|
||||
name: "4. No Self-Promotion",
|
||||
value: "Do not advertise other servers, social media accounts, or services without prior approval from staff.",
|
||||
},
|
||||
{
|
||||
name: "5. Follow Discord ToS",
|
||||
value: "All members must comply with [Discord's Terms of Service](https://discord.com/terms) and [Community Guidelines](https://discord.com/guidelines).",
|
||||
},
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (!existingMessage) {
|
||||
const embed1 = new EmbedBuilder()
|
||||
.setTitle("📜 Server Rules — Part 1: General Conduct")
|
||||
.setColor(0x5865f2)
|
||||
.setDescription(
|
||||
"Please read and follow all rules to keep this server a safe and welcoming place for everyone.",
|
||||
)
|
||||
.addFields(
|
||||
{
|
||||
name: "1. Be Respectful",
|
||||
value:
|
||||
"Treat all members with respect. Harassment, hate speech, slurs, and discrimination of any kind will not be tolerated.",
|
||||
},
|
||||
{
|
||||
name: "2. No Spam",
|
||||
value:
|
||||
"Do not spam messages, emojis, or mentions. Keep conversations relevant to the channel topic.",
|
||||
},
|
||||
{
|
||||
name: "3. No NSFW Content",
|
||||
value:
|
||||
"Explicit, graphic, or otherwise inappropriate content is strictly prohibited.",
|
||||
},
|
||||
{
|
||||
name: "4. No Self-Promotion",
|
||||
value:
|
||||
"Do not advertise other servers, social media accounts, or services without prior approval from staff.",
|
||||
},
|
||||
{
|
||||
name: "5. Follow Discord ToS",
|
||||
value:
|
||||
"All members must comply with [Discord's Terms of Service](https://discord.com/terms) and [Community Guidelines](https://discord.com/guidelines).",
|
||||
},
|
||||
)
|
||||
.setTimestamp();
|
||||
const embed2 = new EmbedBuilder()
|
||||
.setTitle("📋 Server Rules — Part 2: Channels & Community")
|
||||
.setColor(0x57f287)
|
||||
.addFields(
|
||||
{
|
||||
name: "6. Use the Right Channels",
|
||||
value: "Keep discussions in their appropriate channels. Off-topic conversations belong in the designated channel.",
|
||||
},
|
||||
{
|
||||
name: "7. No Doxxing",
|
||||
value: "Sharing personal or private information of others without their explicit consent is strictly forbidden.",
|
||||
},
|
||||
{
|
||||
name: "8. English in Main Channels",
|
||||
value: "Please communicate in English in main channels so all members and staff can participate.",
|
||||
},
|
||||
{
|
||||
name: "9. Listen to Staff",
|
||||
value: "Follow the instructions of moderators and admins. If you disagree with a decision, open a support ticket calmly.",
|
||||
},
|
||||
{
|
||||
name: "10. Have Fun!",
|
||||
value: "This is a community — be kind, stay positive, and enjoy your time here. 🎉",
|
||||
},
|
||||
)
|
||||
.setFooter({
|
||||
text: "React with ✅ below to accept the rules and gain access to the server.",
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
const embed2 = new EmbedBuilder()
|
||||
.setTitle("📋 Server Rules — Part 2: Channels & Community")
|
||||
.setColor(0x57f287)
|
||||
.addFields(
|
||||
{
|
||||
name: "6. Use the Right Channels",
|
||||
value:
|
||||
"Keep discussions in their appropriate channels. Off-topic conversations belong in the designated channel.",
|
||||
},
|
||||
{
|
||||
name: "7. No Doxxing",
|
||||
value:
|
||||
"Sharing personal or private information of others without their explicit consent is strictly forbidden.",
|
||||
},
|
||||
{
|
||||
name: "8. English in Main Channels",
|
||||
value:
|
||||
"Please communicate in English in main channels so all members and staff can participate.",
|
||||
},
|
||||
{
|
||||
name: "9. Listen to Staff",
|
||||
value:
|
||||
"Follow the instructions of moderators and admins. If you disagree with a decision, open a support ticket calmly.",
|
||||
},
|
||||
{
|
||||
name: "10. Have Fun!",
|
||||
value:
|
||||
"This is a community — be kind, stay positive, and enjoy your time here. 🎉",
|
||||
},
|
||||
)
|
||||
.setFooter({
|
||||
text: "React with ✅ below to accept the rules and gain access to the server.",
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
const rulesMessage = await channel.send({ embeds: [embed1, embed2] });
|
||||
await rulesMessage.react(RULES_EMOJI);
|
||||
console.log("[checkRules] Rules message posted successfully.");
|
||||
} else {
|
||||
console.log("[checkRules] Rules message already exists, skipping.");
|
||||
}
|
||||
|
||||
// Grant role when a member reacts with ✅ in the rules channel
|
||||
client.on(Events.MessageReactionAdd, async (reaction, user) => {
|
||||
if (user.bot) return;
|
||||
if (reaction.message.channelId !== CHANNELS.RULES) return;
|
||||
if (reaction.emoji.name !== RULES_EMOJI) return;
|
||||
|
||||
// Resolve partials if needed
|
||||
try {
|
||||
if (reaction.partial) await reaction.fetch();
|
||||
if (user.partial) await user.fetch();
|
||||
} catch {
|
||||
return;
|
||||
const rulesMessage = await channel.send({ embeds: [embed1, embed2] });
|
||||
await rulesMessage.react(RULES_EMOJI);
|
||||
console.log("[checkRules] Rules message posted successfully.");
|
||||
} else {
|
||||
console.log("[checkRules] Rules message already exists, skipping.");
|
||||
}
|
||||
|
||||
try {
|
||||
const member = await guild.members.fetch(user.id);
|
||||
await member.roles.add(ROLES.RULES);
|
||||
console.log(`[checkRules] Granted rules role to ${user.tag ?? user.id}`);
|
||||
} catch (err) {
|
||||
console.error("[checkRules] Failed to add rules role:", err);
|
||||
}
|
||||
});
|
||||
// Grant role when a member reacts with ✅ in the rules channel
|
||||
client.on(Events.MessageReactionAdd, async (reaction, user) => {
|
||||
if (user.bot) return;
|
||||
if (reaction.message.channelId !== CHANNELS.RULES) return;
|
||||
if (reaction.emoji.name !== RULES_EMOJI) return;
|
||||
|
||||
// Remove role when the reaction is removed
|
||||
client.on(Events.MessageReactionRemove, async (reaction, user) => {
|
||||
if (user.bot) return;
|
||||
if (reaction.message.channelId !== CHANNELS.RULES) return;
|
||||
if (reaction.emoji.name !== RULES_EMOJI) return;
|
||||
// Resolve partials if needed
|
||||
try {
|
||||
if (reaction.partial) await reaction.fetch();
|
||||
if (user.partial) await user.fetch();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (reaction.partial) await reaction.fetch();
|
||||
if (user.partial) await user.fetch();
|
||||
} catch (err) {
|
||||
console.error("[checkRules] Failed to fetch reaction or user:", err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const member = await guild.members.fetch(user.id);
|
||||
await member.roles.add(ROLES.RULES);
|
||||
console.log(
|
||||
`[checkRules] Granted rules role to ${user.tag ?? user.id}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[checkRules] Failed to add rules role:", err);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const member = await guild.members.fetch(user.id);
|
||||
await member.roles.remove(ROLES.RULES);
|
||||
console.log(
|
||||
`[checkRules] Removed rules role from ${user.tag ?? user.id}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[checkRules] Failed to remove rules role:", err);
|
||||
}
|
||||
});
|
||||
// Remove role when the reaction is removed
|
||||
client.on(Events.MessageReactionRemove, async (reaction, user) => {
|
||||
if (user.bot) return;
|
||||
if (reaction.message.channelId !== CHANNELS.RULES) return;
|
||||
if (reaction.emoji.name !== RULES_EMOJI) return;
|
||||
|
||||
try {
|
||||
if (reaction.partial) await reaction.fetch();
|
||||
if (user.partial) await user.fetch();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[checkRules] Failed to fetch reaction or user:",
|
||||
err,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const member = await guild.members.fetch(user.id);
|
||||
await member.roles.remove(ROLES.RULES);
|
||||
console.log(
|
||||
`[checkRules] Removed rules role from ${user.tag ?? user.id}`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[checkRules] Failed to remove rules role:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,17 +2,19 @@ import { ActivityType, Client } from "discord.js";
|
||||
import { ROTATE_ACTIVITIES } from "../../config";
|
||||
|
||||
export default async function rotatingActivity(client: Client): Promise<void> {
|
||||
if (!client.user) {
|
||||
console.error("[rotatingActivity] Client is not ready yet.");
|
||||
return;
|
||||
}
|
||||
if (!client.user) {
|
||||
console.error("[rotatingActivity] Client is not ready yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
client.user.setActivity("Fixing Bugs", { type: ActivityType.Custom });
|
||||
let index = 0;
|
||||
setInterval(() => {
|
||||
const activity = ROTATE_ACTIVITIES[index];
|
||||
client.user!.setActivity(activity.content, { type: activity.type });
|
||||
index = (index + 1) % ROTATE_ACTIVITIES.length;
|
||||
console.log(`[rotatingActivity] Updated activity to: ${activity.content} (${activity.type})`);
|
||||
}, 60000); // Rotate every 60 seconds
|
||||
client.user.setActivity("Fixing Bugs", { type: ActivityType.Custom });
|
||||
let index = 0;
|
||||
setInterval(() => {
|
||||
const activity = ROTATE_ACTIVITIES[index];
|
||||
client.user!.setActivity(activity.content, { type: activity.type });
|
||||
index = (index + 1) % ROTATE_ACTIVITIES.length;
|
||||
console.log(
|
||||
`[rotatingActivity] Updated activity to: ${activity.content} (${activity.type})`,
|
||||
);
|
||||
}, 60000); // Rotate every 60 seconds
|
||||
}
|
||||
|
||||
66
src/index.ts
66
src/index.ts
@@ -1,47 +1,55 @@
|
||||
import { Client, Events, GatewayIntentBits, Message, Partials } from 'discord.js';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import loadModulesFromDir from './lib/loadStartupHandlers';
|
||||
import {
|
||||
Client,
|
||||
Events,
|
||||
GatewayIntentBits,
|
||||
Message,
|
||||
Partials,
|
||||
} from "discord.js";
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
import loadModulesFromDir from "./lib/loadStartupHandlers";
|
||||
import * as mongoDB from "mongodb";
|
||||
import { env } from 'process';
|
||||
import webserver from './webserver';
|
||||
import { env } from "process";
|
||||
import webserver from "./webserver";
|
||||
dotenv.config();
|
||||
|
||||
const dbclient: mongoDB.MongoClient = new mongoDB.MongoClient(
|
||||
env.MONGO_DB!
|
||||
);
|
||||
const dbclient: mongoDB.MongoClient = new mongoDB.MongoClient(env.MONGO_DB!);
|
||||
|
||||
const db: mongoDB.Db = dbclient.db(env.DB_NAME!);
|
||||
|
||||
export { db, dbclient };
|
||||
|
||||
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel, Partials.Reaction, Partials.User],
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildMessageReactions,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
partials: [
|
||||
Partials.Message,
|
||||
Partials.Channel,
|
||||
Partials.Reaction,
|
||||
Partials.User,
|
||||
],
|
||||
});
|
||||
|
||||
let readyClient: typeof client | null = null;
|
||||
|
||||
client.once(Events.ClientReady, async (rc) => {
|
||||
readyClient = rc;
|
||||
console.log(`Logged in as ${rc.user.tag}`);
|
||||
readyClient = rc;
|
||||
console.log(`Logged in as ${rc.user.tag}`);
|
||||
|
||||
const dirs = [
|
||||
path.join(__dirname, 'commands'), // load commands first
|
||||
path.join(__dirname, 'handlers'),
|
||||
path.join(__dirname, 'listeners'),
|
||||
];
|
||||
const dirs = [
|
||||
path.join(__dirname, "commands"), // load commands first
|
||||
path.join(__dirname, "handlers"),
|
||||
path.join(__dirname, "listeners"),
|
||||
];
|
||||
|
||||
for (const dir of dirs) {
|
||||
await loadModulesFromDir(dir, client);
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
await loadModulesFromDir(dir, client);
|
||||
}
|
||||
});
|
||||
|
||||
// Start webserver in the background
|
||||
@@ -50,4 +58,4 @@ webserver().catch(console.error);
|
||||
// Exports
|
||||
export { client, readyClient };
|
||||
|
||||
client.login(process.env.DISCORD_TOKEN);
|
||||
client.login(process.env.DISCORD_TOKEN);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
|
||||
|
||||
export interface Command {
|
||||
data: SlashCommandBuilder;
|
||||
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
|
||||
data: SlashCommandBuilder;
|
||||
execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
|
||||
}
|
||||
|
||||
const commands = new Map<string, Command>();
|
||||
|
||||
export function registerCommand(command: Command) {
|
||||
commands.set(command.data.name, command);
|
||||
commands.set(command.data.name, command);
|
||||
}
|
||||
|
||||
export function getCommand(name: string): Command | undefined {
|
||||
return commands.get(name);
|
||||
return commands.get(name);
|
||||
}
|
||||
|
||||
export function getAllCommands(): Command[] {
|
||||
return Array.from(commands.values());
|
||||
return Array.from(commands.values());
|
||||
}
|
||||
|
||||
@@ -2,31 +2,39 @@ import { Client } from "discord.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default async function loadModulesFromDir(dir: string, client: Client): Promise<void> {
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.warn(`[loadModules] Directory not found: ${dir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await loadModulesFromDir(fullPath, client); // recurse
|
||||
} else if (entry.isFile() && /\.(ts|js)$/.test(entry.name)) {
|
||||
try {
|
||||
const mod = await import(fullPath);
|
||||
if (typeof mod.default === "function") {
|
||||
await mod.default(client);
|
||||
console.log(`[loadModules] Loaded: ${fullPath}`);
|
||||
} else {
|
||||
console.warn(`[loadModules] No default export function in: ${entry.name}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[loadModules] Failed to load ${entry.name}:`, err);
|
||||
}
|
||||
export default async function loadModulesFromDir(
|
||||
dir: string,
|
||||
client: Client,
|
||||
): Promise<void> {
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.warn(`[loadModules] Directory not found: ${dir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await loadModulesFromDir(fullPath, client); // recurse
|
||||
} else if (entry.isFile() && /\.(ts|js)$/.test(entry.name)) {
|
||||
try {
|
||||
const mod = await import(fullPath);
|
||||
if (typeof mod.default === "function") {
|
||||
await mod.default(client);
|
||||
console.log(`[loadModules] Loaded: ${fullPath}`);
|
||||
} else {
|
||||
console.warn(
|
||||
`[loadModules] No default export function in: ${entry.name}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[loadModules] Failed to load ${entry.name}:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,38 +3,38 @@ import { db } from "../..";
|
||||
|
||||
// Adds a User to the database as they just joined the server. This is used for tracking purposes and to store user data in the future.
|
||||
export default async function addUserToDB(client: Client): Promise<void> {
|
||||
client.on(Events.GuildMemberAdd, async (member) => {
|
||||
console.log(`[addUserToDB] [${member.user.tag}] Joined the server`);
|
||||
client.on(Events.GuildMemberAdd, async (member) => {
|
||||
console.log(`[addUserToDB] [${member.user.tag}] Joined the server`);
|
||||
|
||||
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,
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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!),
|
||||
isBot: member.user.bot,
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ export default async function removeUserFromDB(client: Client): Promise<void> {
|
||||
guildId: member.guild.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Client, Events, Message } from "discord.js";
|
||||
|
||||
export default async function helloWorld(client: Client): Promise<void> {
|
||||
client.on(Events.MessageCreate, (message: Message) => {
|
||||
// Ignore messages from bots
|
||||
if (message.author.bot) return;
|
||||
client.on(Events.MessageCreate, (message: Message) => {
|
||||
// Ignore messages from bots
|
||||
if (message.author.bot) return;
|
||||
|
||||
if (message.content.toLowerCase().replace("!", "") === "hello world") {
|
||||
message.reply({
|
||||
embeds: [
|
||||
{
|
||||
title: "Hello, World!",
|
||||
description: "Hello from the SHSF Team!",
|
||||
color: 0x00ff00,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
if (message.content.toLowerCase().replace("!", "") === "hello world") {
|
||||
message.reply({
|
||||
embeds: [
|
||||
{
|
||||
title: "Hello, World!",
|
||||
description: "Hello from the SHSF Team!",
|
||||
color: 0x00ff00,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,24 +3,26 @@ import { db } from "../..";
|
||||
import { addUserToDBManually } from "../GuildMemberAdd/addUser";
|
||||
|
||||
export default async function logMessage(client: Client): Promise<void> {
|
||||
client.on(Events.MessageCreate, async (message: Message) => {
|
||||
if (!message.guildId) return; // Only log messages from guilds (ignore DMs)
|
||||
if (!message.member) return; // Ignore messages without member info (should be rare)
|
||||
|
||||
// Log ALL message inlcuding bots
|
||||
console.log(`[logMessage] [${message.author.tag}] Sent a message (${message.content.length} chars)`);
|
||||
client.on(Events.MessageCreate, async (message: Message) => {
|
||||
if (!message.guildId) return; // Only log messages from guilds (ignore DMs)
|
||||
if (!message.member) return; // Ignore messages without member info (should be rare)
|
||||
|
||||
await db.collection("messages").insertOne({
|
||||
messageId: message.id,
|
||||
authorId: message.author.id,
|
||||
authorTag: message.author.tag,
|
||||
content: message.content,
|
||||
channelId: message.channelId,
|
||||
guildId: message.guildId,
|
||||
timestamp: new Date(message.createdTimestamp),
|
||||
// Log ALL message inlcuding bots
|
||||
console.log(
|
||||
`[logMessage] [${message.author.tag}] Sent a message (${message.content.length} chars)`,
|
||||
);
|
||||
|
||||
await db.collection("messages").insertOne({
|
||||
messageId: message.id,
|
||||
authorId: message.author.id,
|
||||
authorTag: message.author.tag,
|
||||
content: message.content,
|
||||
channelId: message.channelId,
|
||||
guildId: message.guildId,
|
||||
timestamp: new Date(message.createdTimestamp),
|
||||
});
|
||||
|
||||
// Does this user exist in our database? If not, add them (this is for users who were in the server before the bot was added)
|
||||
await addUserToDBManually(message.member);
|
||||
});
|
||||
|
||||
// Does this user exist in our database? If not, add them (this is for users who were in the server before the bot was added)
|
||||
await addUserToDBManually(message.member);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { Express, Request, Response } from 'express';
|
||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||
import { CHANNELS } from '../config';
|
||||
import { client, db } from '../index';
|
||||
import { Express, Request, Response } from "express";
|
||||
import { EmbedBuilder, TextChannel } from "discord.js";
|
||||
import { CHANNELS } from "../config";
|
||||
import { client, db } from "../index";
|
||||
|
||||
const configured_channel = CHANNELS.UPDATES;
|
||||
|
||||
export default async function gitCommitPOST(app: Express) {
|
||||
app.post('/git-commit', async (req: Request, res: Response) => {
|
||||
app.post("/git-commit", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const event = req.headers['x-github-event'] as string;
|
||||
const event = req.headers["x-github-event"] as string;
|
||||
|
||||
// Acknowledge ping events
|
||||
if (event === 'ping') {
|
||||
console.log('[WEB-gitCommit] Received GitHub ping event');
|
||||
return res.status(200).json({ success: true, message: 'pong' });
|
||||
if (event === "ping") {
|
||||
console.log("[WEB-gitCommit] Received GitHub ping event");
|
||||
return res.status(200).json({ success: true, message: "pong" });
|
||||
}
|
||||
|
||||
if (event !== 'push') {
|
||||
return res.status(200).json({ success: true, message: `Event '${event}' ignored` });
|
||||
if (event !== "push") {
|
||||
return res
|
||||
.status(200)
|
||||
.json({
|
||||
success: true,
|
||||
message: `Event '${event}' ignored`,
|
||||
});
|
||||
}
|
||||
|
||||
const body = req.body;
|
||||
@@ -25,20 +30,20 @@ export default async function gitCommitPOST(app: Express) {
|
||||
const pusher = body.pusher;
|
||||
const commits: any[] = body.commits ?? [];
|
||||
const headCommit = body.head_commit;
|
||||
const ref: string = body.ref ?? '';
|
||||
const branch = ref.replace('refs/heads/', '');
|
||||
const compareUrl: string = body.compare ?? '';
|
||||
const ref: string = body.ref ?? "";
|
||||
const branch = ref.replace("refs/heads/", "");
|
||||
const compareUrl: string = body.compare ?? "";
|
||||
const forced: boolean = body.forced ?? false;
|
||||
|
||||
if (!repo || !headCommit) {
|
||||
return res.status(400).json({ error: 'Invalid push payload' });
|
||||
return res.status(400).json({ error: "Invalid push payload" });
|
||||
}
|
||||
|
||||
// Build commit list (max X)
|
||||
const SHOW_MAX = 5;
|
||||
const commitLines = commits.slice(0, SHOW_MAX).map((c: any) => {
|
||||
const shortId = c.id.substring(0, 7);
|
||||
const msg = c.message.split('\n')[0].substring(0, 64);
|
||||
const msg = c.message.split("\n")[0].substring(0, 64);
|
||||
return `[\`${shortId}\`](${c.url}) ${msg}...`;
|
||||
});
|
||||
|
||||
@@ -48,7 +53,9 @@ export default async function gitCommitPOST(app: Express) {
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(forced ? 0xff4444 : 0x2ea44f)
|
||||
.setTitle(`${forced ? '⚠️ Force Push' : '📦 New Push'} to \`${branch}\``)
|
||||
.setTitle(
|
||||
`${forced ? "⚠️ Force Push" : "📦 New Push"} to \`${branch}\``,
|
||||
)
|
||||
.setURL(compareUrl)
|
||||
.setAuthor({
|
||||
name: pusher.name,
|
||||
@@ -56,28 +63,45 @@ export default async function gitCommitPOST(app: Express) {
|
||||
url: `https://github.com/${pusher.name}`,
|
||||
})
|
||||
.addFields(
|
||||
{ name: '🌿 Branch', value: `\`${branch}\``, inline: true },
|
||||
{ name: `📝 Commits (${commits.length})`, value: commitLines.join('\n') || '_No commits_' },
|
||||
{ name: "🌿 Branch", value: `\`${branch}\``, inline: true },
|
||||
{
|
||||
name: `📝 Commits (${commits.length})`,
|
||||
value: commitLines.join("\n") || "_No commits_",
|
||||
},
|
||||
)
|
||||
.setFooter({ text: `Delivery: ${req.headers['x-github-delivery'] ?? 'unknown'}` })
|
||||
.setTimestamp(headCommit.timestamp ? new Date(headCommit.timestamp) : new Date());
|
||||
.setFooter({
|
||||
text: `Delivery: ${req.headers["x-github-delivery"] ?? "unknown"}`,
|
||||
})
|
||||
.setTimestamp(
|
||||
headCommit.timestamp
|
||||
? new Date(headCommit.timestamp)
|
||||
: new Date(),
|
||||
);
|
||||
|
||||
const channel = await client.channels.fetch(configured_channel) as TextChannel | null;
|
||||
const channel = (await client.channels.fetch(
|
||||
configured_channel,
|
||||
)) as TextChannel | null;
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
console.error('[WEB-gitCommit] Configured channel not found or not text-based');
|
||||
return res.status(500).json({ error: 'Discord channel unavailable' });
|
||||
console.error(
|
||||
"[WEB-gitCommit] Configured channel not found or not text-based",
|
||||
);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: "Discord channel unavailable" });
|
||||
}
|
||||
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
console.log(`[WEB-gitCommit] Push event sent to configured channel (${commits.length} commits on ${branch})`);
|
||||
console.log(
|
||||
`[WEB-gitCommit] Push event sent to configured channel (${commits.length} commits on ${branch})`,
|
||||
);
|
||||
|
||||
// Reactions for engagement
|
||||
await message.react('👍');
|
||||
await message.react('🔥');
|
||||
await message.react('🤯');
|
||||
await message.react("👍");
|
||||
await message.react("🔥");
|
||||
await message.react("🤯");
|
||||
|
||||
// Add to DB
|
||||
await db.collection('git_commits').insertOne({
|
||||
await db.collection("git_commits").insertOne({
|
||||
repository: repo.full_name,
|
||||
pusher: pusher.name,
|
||||
branch,
|
||||
@@ -89,8 +113,10 @@ export default async function gitCommitPOST(app: Express) {
|
||||
|
||||
return res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[WEB-gitCommit] Error handling git commit:', error);
|
||||
return res.status(500).json({ success: false, error: 'Internal Server Error' });
|
||||
console.error("[WEB-gitCommit] Error handling git commit:", error);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ success: false, error: "Internal Server Error" });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Express } from 'express';
|
||||
import { Express } from "express";
|
||||
|
||||
export default function healthRoute(app: Express) {
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).send('ok');
|
||||
app.get("/health", (req, res) => {
|
||||
res.status(200).send("ok");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Express, Request, Response } from "express";
|
||||
|
||||
export default async function gitCommitPOST(app: Express) {
|
||||
app.get("/", (req: Request, res: Response) => {
|
||||
res.status(200).json({ message: "Hello, world!" });
|
||||
});
|
||||
app.get("/", (req: Request, res: Response) => {
|
||||
res.status(200).json({ message: "Hello, world!" });
|
||||
});
|
||||
|
||||
app.post("/", (req: Request, res: Response) => {
|
||||
res.status(200).json({ message: "Hello, world!" });
|
||||
});
|
||||
app.post("/", (req: Request, res: Response) => {
|
||||
res.status(200).json({ message: "Hello, world!" });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import express from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default async function webserver() {
|
||||
const app = express();
|
||||
@@ -8,13 +8,15 @@ export default async function webserver() {
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
const webDir = path.join(__dirname, 'web');
|
||||
const webDir = path.join(__dirname, "web");
|
||||
if (fs.existsSync(webDir)) {
|
||||
const files = fs.readdirSync(webDir).filter(f => f.endsWith('.ts') || f.endsWith('.js'));
|
||||
const files = fs
|
||||
.readdirSync(webDir)
|
||||
.filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
|
||||
for (const file of files) {
|
||||
const mod = await import(path.join(webDir, file));
|
||||
const handler = mod.default ?? mod;
|
||||
if (typeof handler === 'function') {
|
||||
if (typeof handler === "function") {
|
||||
await handler(app);
|
||||
console.log(`[WEB] Loaded web route: ${file}`);
|
||||
}
|
||||
@@ -25,7 +27,7 @@ export default async function webserver() {
|
||||
console.log(`[WEB] Web server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
const IgnoredPaths = ['/favicon.ico', '/robots.txt', "/", "/hello"];
|
||||
const IgnoredPaths = ["/favicon.ico", "/robots.txt", "/", "/hello"];
|
||||
const KnownPaths = ["/git-commit", "/", "/health"];
|
||||
|
||||
// log all incoming requests
|
||||
@@ -34,10 +36,14 @@ export default async function webserver() {
|
||||
return next();
|
||||
}
|
||||
if (!KnownPaths.includes(req.url)) {
|
||||
console.warn(`[WEB] Unknown Route request: ${req.method} ${req.url} {${req.ip}}`);
|
||||
console.warn(
|
||||
`[WEB] Unknown Route request: ${req.method} ${req.url} {${req.ip}}`,
|
||||
);
|
||||
} else {
|
||||
console.log(`[WEB] Trusted Route request: ${req.method} ${req.url} {${req.ip}}`);
|
||||
console.log(
|
||||
`[WEB] Trusted Route request: ${req.method} ${req.url} {${req.ip}}`,
|
||||
);
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user