ran prettier lol
All checks were successful
CI / build (push) Successful in 10s

This commit is contained in:
Space-Banane
2026-02-22 15:31:37 +01:00
parent bc58cb7361
commit e1300a98b3
17 changed files with 473 additions and 408 deletions

View File

@@ -2,55 +2,68 @@ import { Client, SlashCommandBuilder } from "discord.js";
import { registerCommand } from "../lib/commandRegistry"; import { registerCommand } from "../lib/commandRegistry";
export default async function (_client: Client) { export default async function (_client: Client) {
registerCommand({ registerCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName("date-to-epoch") .setName("date-to-epoch")
.setDescription("Converts a date (DD/MM/YYYY) to a Unix epoch timestamp") .setDescription(
.addStringOption((option) => "Converts a date (DD/MM/YYYY) to a Unix epoch timestamp",
option )
.setName("date") .addStringOption((option) =>
.setDescription("Date in DD/MM/YYYY format (e.g. 25/12/2026)") option
.setRequired(true) .setName("date")
) as SlashCommandBuilder, .setDescription(
async execute(interaction) { "Date in DD/MM/YYYY format (e.g. 25/12/2026)",
const input = interaction.options.getString("date", true).trim(); )
.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); const ddmmyyyy = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(input);
if (!ddmmyyyy) { if (!ddmmyyyy) {
await interaction.reply({ await interaction.reply({
ephemeral: true, ephemeral: true,
content: "❌ Invalid format. Please use `DD/MM/YYYY` (e.g. `25/12/2026`).", content:
}); "❌ Invalid format. Please use `DD/MM/YYYY` (e.g. `25/12/2026`).",
return; });
} return;
}
const [, dd, mm, yyyy] = ddmmyyyy; const [, dd, mm, yyyy] = ddmmyyyy;
const date = new Date(Number(yyyy), Number(mm) - 1, Number(dd)); const date = new Date(Number(yyyy), Number(mm) - 1, Number(dd));
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
await interaction.reply({ await interaction.reply({
ephemeral: true, ephemeral: true,
content: "❌ The date you provided is invalid.", content: "❌ The date you provided is invalid.",
}); });
return; return;
} }
const epoch = Math.floor(date.getTime() / 1000); const epoch = Math.floor(date.getTime() / 1000);
await interaction.reply({ await interaction.reply({
ephemeral: true, ephemeral: true,
embeds: [ embeds: [
{ {
title: "📅 Date → Epoch", title: "📅 Date → Epoch",
color: 0x5865f2, color: 0x5865f2,
fields: [ fields: [
{ name: "Input", value: input, inline: true }, { name: "Input", value: input, inline: true },
{ name: "Epoch", value: `\`${epoch}\``, inline: true }, {
{ name: "Preview", value: `<t:${epoch}:F> (<t:${epoch}:R>)`, inline: false }, name: "Epoch",
], value: `\`${epoch}\``,
}, inline: true,
], },
}); {
}, name: "Preview",
}); value: `<t:${epoch}:F> (<t:${epoch}:R>)`,
inline: false,
},
],
},
],
});
},
});
} }

View File

@@ -2,12 +2,12 @@ import { Client, SlashCommandBuilder } from "discord.js";
import { registerCommand } from "../lib/commandRegistry"; import { registerCommand } from "../lib/commandRegistry";
export default async function (_client: Client) { export default async function (_client: Client) {
registerCommand({ registerCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName("ping") .setName("ping")
.setDescription("Replies with Pong!"), .setDescription("Replies with Pong!"),
async execute(interaction) { async execute(interaction) {
await interaction.reply("Pong!"); await interaction.reply("Pong!");
}, },
}); });
} }

View File

@@ -1,9 +1,6 @@
import { ActivityType } from "discord.js"; import { ActivityType } from "discord.js";
export const GUILD_ID = "1475098530505953441";
export const GUILD_ID = '1475098530505953441';
export const CHANNELS = { export const CHANNELS = {
RULES: "1475100731991392539", RULES: "1475100731991392539",
@@ -20,13 +17,13 @@ export const ROLES = {
RULES: "1475100051352191047", RULES: "1475100051352191047",
MAINTAINERS: "1475099468591272090", MAINTAINERS: "1475099468591272090",
COMMUNITY_ADMINS: "1475099507527258218", 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 // Discord modified the way activities work, for now, we'll only use custom ones
export const ROTATE_ACTIVITIES:{content:string;type:ActivityType}[] = [ export const ROTATE_ACTIVITIES: { content: string; type: ActivityType }[] = [
{content: "Fixing Bugs", type: ActivityType.Custom}, { content: "Fixing Bugs", type: ActivityType.Custom },
{content: "Adding New Features", type: ActivityType.Custom}, { content: "Adding New Features", type: ActivityType.Custom },
{content: "Improving Performance", type: ActivityType.Custom}, { content: "Improving Performance", type: ActivityType.Custom },
{content: "Listening to Feedback", type: ActivityType.Custom}, { content: "Listening to Feedback", type: ActivityType.Custom },
] ];

View File

@@ -3,34 +3,40 @@ import { getAllCommands, getCommand } from "../lib/commandRegistry";
import { GUILD_ID } from "../config"; import { GUILD_ID } from "../config";
export default async function (client: Client) { export default async function (client: Client) {
const token = process.env.DISCORD_TOKEN!; const token = process.env.DISCORD_TOKEN!;
const clientId = client.user!.id; // use the ready client's ID 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), { await rest.put(Routes.applicationGuildCommands(clientId, GUILD_ID), {
body: commandData, body: commandData,
}); });
console.log(`Registered ${commandData.length} slash command(s).`); console.log(`Registered ${commandData.length} slash command(s).`);
client.on(Events.InteractionCreate, async (interaction) => { client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return; if (!interaction.isChatInputCommand()) return;
const command = getCommand(interaction.commandName); const command = getCommand(interaction.commandName);
if (!command) return; if (!command) return;
try { try {
await command.execute(interaction); await command.execute(interaction);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
if (interaction.replied || interaction.deferred) { if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: "An error occurred.", ephemeral: true }); await interaction.followUp({
} else { content: "An error occurred.",
await interaction.reply({ content: "An error occurred.", ephemeral: true }); ephemeral: true,
} });
} } else {
}); await interaction.reply({
content: "An error occurred.",
ephemeral: true,
});
}
}
});
} }

View File

@@ -1,155 +1,152 @@
import { import {
Client, Client,
EmbedBuilder, EmbedBuilder,
Events, Events,
GuildMember, GuildMember,
Partials, Partials,
TextChannel, TextChannel,
} from "discord.js"; } from "discord.js";
import { CHANNELS, GUILD_ID, ROLES } from "../../config"; import { CHANNELS, GUILD_ID, ROLES } from "../../config";
const RULES_EMOJI = "✅"; const RULES_EMOJI = "✅";
export default async function checkRules(client: Client): Promise<void> { export default async function checkRules(client: Client): Promise<void> {
const guild = await client.guilds.fetch(GUILD_ID); const guild = await client.guilds.fetch(GUILD_ID);
const channel = (await guild.channels.fetch( const channel = (await guild.channels.fetch(
CHANNELS["RULES"], CHANNELS["RULES"],
)) as TextChannel; )) as TextChannel;
if (!channel || !channel.isTextBased()) { if (!channel || !channel.isTextBased()) {
console.error( console.error(
"[checkRules] Rules channel not found or is not a text channel.", "[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 if (!existingMessage) {
const messages = await channel.messages.fetch({ limit: 50 }); const embed1 = new EmbedBuilder()
const existingMessage = messages.find((m) => m.author.id === client.user!.id); .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 embed2 = new EmbedBuilder()
const embed1 = new EmbedBuilder() .setTitle("📋 Server Rules — Part 2: Channels & Community")
.setTitle("📜 Server Rules — Part 1: General Conduct") .setColor(0x57f287)
.setColor(0x5865f2) .addFields(
.setDescription( {
"Please read and follow all rules to keep this server a safe and welcoming place for everyone.", name: "6. Use the Right Channels",
) value: "Keep discussions in their appropriate channels. Off-topic conversations belong in the designated channel.",
.addFields( },
{ {
name: "1. Be Respectful", name: "7. No Doxxing",
value: value: "Sharing personal or private information of others without their explicit consent is strictly forbidden.",
"Treat all members with respect. Harassment, hate speech, slurs, and discrimination of any kind will not be tolerated.", },
}, {
{ name: "8. English in Main Channels",
name: "2. No Spam", value: "Please communicate in English in main channels so all members and staff can participate.",
value: },
"Do not spam messages, emojis, or mentions. Keep conversations relevant to the channel topic.", {
}, 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: "3. No NSFW Content", },
value: {
"Explicit, graphic, or otherwise inappropriate content is strictly prohibited.", name: "10. Have Fun!",
}, value: "This is a community — be kind, stay positive, and enjoy your time here. 🎉",
{ },
name: "4. No Self-Promotion", )
value: .setFooter({
"Do not advertise other servers, social media accounts, or services without prior approval from staff.", text: "React with ✅ below to accept the rules and gain access to the server.",
}, })
{ .setTimestamp();
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() const rulesMessage = await channel.send({ embeds: [embed1, embed2] });
.setTitle("📋 Server Rules — Part 2: Channels & Community") await rulesMessage.react(RULES_EMOJI);
.setColor(0x57f287) console.log("[checkRules] Rules message posted successfully.");
.addFields( } else {
{ console.log("[checkRules] Rules message already exists, skipping.");
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;
} }
try { // Grant role when a member reacts with ✅ in the rules channel
const member = await guild.members.fetch(user.id); client.on(Events.MessageReactionAdd, async (reaction, user) => {
await member.roles.add(ROLES.RULES); if (user.bot) return;
console.log(`[checkRules] Granted rules role to ${user.tag ?? user.id}`); if (reaction.message.channelId !== CHANNELS.RULES) return;
} catch (err) { if (reaction.emoji.name !== RULES_EMOJI) return;
console.error("[checkRules] Failed to add rules role:", err);
}
});
// Remove role when the reaction is removed // Resolve partials if needed
client.on(Events.MessageReactionRemove, async (reaction, user) => { try {
if (user.bot) return; if (reaction.partial) await reaction.fetch();
if (reaction.message.channelId !== CHANNELS.RULES) return; if (user.partial) await user.fetch();
if (reaction.emoji.name !== RULES_EMOJI) return; } catch {
return;
}
try { try {
if (reaction.partial) await reaction.fetch(); const member = await guild.members.fetch(user.id);
if (user.partial) await user.fetch(); await member.roles.add(ROLES.RULES);
} catch (err) { console.log(
console.error("[checkRules] Failed to fetch reaction or user:", err); `[checkRules] Granted rules role to ${user.tag ?? user.id}`,
return; );
} } catch (err) {
console.error("[checkRules] Failed to add rules role:", err);
}
});
try { // Remove role when the reaction is removed
const member = await guild.members.fetch(user.id); client.on(Events.MessageReactionRemove, async (reaction, user) => {
await member.roles.remove(ROLES.RULES); if (user.bot) return;
console.log( if (reaction.message.channelId !== CHANNELS.RULES) return;
`[checkRules] Removed rules role from ${user.tag ?? user.id}`, if (reaction.emoji.name !== RULES_EMOJI) return;
);
} catch (err) { try {
console.error("[checkRules] Failed to remove rules role:", err); 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);
}
});
} }

View File

@@ -2,17 +2,19 @@ import { ActivityType, Client } from "discord.js";
import { ROTATE_ACTIVITIES } from "../../config"; import { ROTATE_ACTIVITIES } from "../../config";
export default async function rotatingActivity(client: Client): Promise<void> { export default async function rotatingActivity(client: Client): Promise<void> {
if (!client.user) { if (!client.user) {
console.error("[rotatingActivity] Client is not ready yet."); console.error("[rotatingActivity] Client is not ready yet.");
return; return;
} }
client.user.setActivity("Fixing Bugs", { type: ActivityType.Custom }); client.user.setActivity("Fixing Bugs", { type: ActivityType.Custom });
let index = 0; let index = 0;
setInterval(() => { setInterval(() => {
const activity = ROTATE_ACTIVITIES[index]; const activity = ROTATE_ACTIVITIES[index];
client.user!.setActivity(activity.content, { type: activity.type }); client.user!.setActivity(activity.content, { type: activity.type });
index = (index + 1) % ROTATE_ACTIVITIES.length; index = (index + 1) % ROTATE_ACTIVITIES.length;
console.log(`[rotatingActivity] Updated activity to: ${activity.content} (${activity.type})`); console.log(
}, 60000); // Rotate every 60 seconds `[rotatingActivity] Updated activity to: ${activity.content} (${activity.type})`,
);
}, 60000); // Rotate every 60 seconds
} }

View File

@@ -1,47 +1,55 @@
import { Client, Events, GatewayIntentBits, Message, Partials } from 'discord.js'; import {
import dotenv from 'dotenv'; Client,
import path from 'path'; Events,
import loadModulesFromDir from './lib/loadStartupHandlers'; 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 * as mongoDB from "mongodb";
import { env } from 'process'; import { env } from "process";
import webserver from './webserver'; import webserver from "./webserver";
dotenv.config(); dotenv.config();
const dbclient: mongoDB.MongoClient = new mongoDB.MongoClient( const dbclient: mongoDB.MongoClient = new mongoDB.MongoClient(env.MONGO_DB!);
env.MONGO_DB!
);
const db: mongoDB.Db = dbclient.db(env.DB_NAME!); const db: mongoDB.Db = dbclient.db(env.DB_NAME!);
export { db, dbclient }; export { db, dbclient };
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMembers,
GatewayIntentBits.MessageContent, GatewayIntentBits.MessageContent,
], ],
partials: [Partials.Message, Partials.Channel, Partials.Reaction, Partials.User], partials: [
Partials.Message,
Partials.Channel,
Partials.Reaction,
Partials.User,
],
}); });
let readyClient: typeof client | null = null; let readyClient: typeof client | null = null;
client.once(Events.ClientReady, async (rc) => { client.once(Events.ClientReady, async (rc) => {
readyClient = rc; readyClient = rc;
console.log(`Logged in as ${rc.user.tag}`); console.log(`Logged in as ${rc.user.tag}`);
const dirs = [ const dirs = [
path.join(__dirname, 'commands'), // load commands first path.join(__dirname, "commands"), // load commands first
path.join(__dirname, 'handlers'), path.join(__dirname, "handlers"),
path.join(__dirname, 'listeners'), path.join(__dirname, "listeners"),
]; ];
for (const dir of dirs) { for (const dir of dirs) {
await loadModulesFromDir(dir, client); await loadModulesFromDir(dir, client);
} }
}); });
// Start webserver in the background // Start webserver in the background

View File

@@ -1,20 +1,20 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
export interface Command { export interface Command {
data: SlashCommandBuilder; data: SlashCommandBuilder;
execute: (interaction: ChatInputCommandInteraction) => Promise<void>; execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
} }
const commands = new Map<string, Command>(); const commands = new Map<string, Command>();
export function registerCommand(command: 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 { export function getCommand(name: string): Command | undefined {
return commands.get(name); return commands.get(name);
} }
export function getAllCommands(): Command[] { export function getAllCommands(): Command[] {
return Array.from(commands.values()); return Array.from(commands.values());
} }

View File

@@ -2,31 +2,39 @@ import { Client } from "discord.js";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
export default async function loadModulesFromDir(dir: string, client: Client): Promise<void> { export default async function loadModulesFromDir(
if (!fs.existsSync(dir)) { dir: string,
console.warn(`[loadModules] Directory not found: ${dir}`); client: Client,
return; ): Promise<void> {
} if (!fs.existsSync(dir)) {
console.warn(`[loadModules] Directory not found: ${dir}`);
const entries = fs.readdirSync(dir, { withFileTypes: true }); return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name); const entries = fs.readdirSync(dir, { withFileTypes: true });
if (entry.isDirectory()) { for (const entry of entries) {
await loadModulesFromDir(fullPath, client); // recurse const fullPath = path.join(dir, entry.name);
} else if (entry.isFile() && /\.(ts|js)$/.test(entry.name)) {
try { if (entry.isDirectory()) {
const mod = await import(fullPath); await loadModulesFromDir(fullPath, client); // recurse
if (typeof mod.default === "function") { } else if (entry.isFile() && /\.(ts|js)$/.test(entry.name)) {
await mod.default(client); try {
console.log(`[loadModules] Loaded: ${fullPath}`); const mod = await import(fullPath);
} else { if (typeof mod.default === "function") {
console.warn(`[loadModules] No default export function in: ${entry.name}`); await mod.default(client);
} console.log(`[loadModules] Loaded: ${fullPath}`);
} catch (err) { } else {
console.error(`[loadModules] Failed to load ${entry.name}:`, err); console.warn(
} `[loadModules] No default export function in: ${entry.name}`,
);
}
} catch (err) {
console.error(
`[loadModules] Failed to load ${entry.name}:`,
err,
);
}
}
} }
}
} }

View File

@@ -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. // 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> { export default async function addUserToDB(client: Client): Promise<void> {
client.on(Events.GuildMemberAdd, async (member) => { client.on(Events.GuildMemberAdd, async (member) => {
console.log(`[addUserToDB] [${member.user.tag}] Joined the server`); console.log(`[addUserToDB] [${member.user.tag}] Joined the server`);
await db.collection("users").insertOne({ await db.collection("users").insertOne({
userId: member.user.id, userId: member.user.id,
authorTag: member.user.tag, authorTag: member.user.tag,
content: member.user.username, content: member.user.username,
guildId: member.guild.id, guildId: member.guild.id,
timestamp: new Date(member.joinedTimestamp!), timestamp: new Date(member.joinedTimestamp!),
isBot: member.user.bot, isBot: member.user.bot,
});
}); });
});
} }
// Silenced becuase its very noisy on every message // Silenced becuase its very noisy on every message
export async function addUserToDBManually(member: GuildMember): Promise<void> { 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 const existingUser = await db
.collection("users") .collection("users")
.findOne({ userId: member.user.id }); .findOne({ userId: member.user.id });
if (existingUser) { if (existingUser) {
// console.log(`[addUserToDBManually] [${member.user.tag}] User already exists in DB, skipping.`); // console.log(`[addUserToDBManually] [${member.user.tag}] User already exists in DB, skipping.`);
return; return;
} }
await db.collection("users").insertOne({ await db.collection("users").insertOne({
userId: member.user.id, userId: member.user.id,
authorTag: member.user.tag, authorTag: member.user.tag,
content: member.user.username, content: member.user.username,
guildId: member.guild.id, guildId: member.guild.id,
timestamp: new Date(member.joinedTimestamp!), timestamp: new Date(member.joinedTimestamp!),
isBot: member.user.bot, isBot: member.user.bot,
}); });
} }

View File

@@ -1,20 +1,20 @@
import { Client, Events, Message } from "discord.js"; import { Client, Events, Message } from "discord.js";
export default async function helloWorld(client: Client): Promise<void> { export default async function helloWorld(client: Client): Promise<void> {
client.on(Events.MessageCreate, (message: Message) => { client.on(Events.MessageCreate, (message: Message) => {
// Ignore messages from bots // Ignore messages from bots
if (message.author.bot) return; if (message.author.bot) return;
if (message.content.toLowerCase().replace("!", "") === "hello world") { if (message.content.toLowerCase().replace("!", "") === "hello world") {
message.reply({ message.reply({
embeds: [ embeds: [
{ {
title: "Hello, World!", title: "Hello, World!",
description: "Hello from the SHSF Team!", description: "Hello from the SHSF Team!",
color: 0x00ff00, color: 0x00ff00,
}, },
], ],
}); });
} }
}); });
} }

View File

@@ -3,24 +3,26 @@ import { db } from "../..";
import { addUserToDBManually } from "../GuildMemberAdd/addUser"; import { addUserToDBManually } from "../GuildMemberAdd/addUser";
export default async function logMessage(client: Client): Promise<void> { export default async function logMessage(client: Client): Promise<void> {
client.on(Events.MessageCreate, async (message: Message) => { client.on(Events.MessageCreate, async (message: Message) => {
if (!message.guildId) return; // Only log messages from guilds (ignore DMs) if (!message.guildId) return; // Only log messages from guilds (ignore DMs)
if (!message.member) return; // Ignore messages without member info (should be rare) if (!message.member) return; // Ignore messages without member info (should be rare)
// Log ALL message inlcuding bots // Log ALL message inlcuding bots
console.log(`[logMessage] [${message.author.tag}] Sent a message (${message.content.length} chars)`); console.log(
`[logMessage] [${message.author.tag}] Sent a message (${message.content.length} chars)`,
);
await db.collection("messages").insertOne({ await db.collection("messages").insertOne({
messageId: message.id, messageId: message.id,
authorId: message.author.id, authorId: message.author.id,
authorTag: message.author.tag, authorTag: message.author.tag,
content: message.content, content: message.content,
channelId: message.channelId, channelId: message.channelId,
guildId: message.guildId, guildId: message.guildId,
timestamp: new Date(message.createdTimestamp), 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);
});
} }

View File

@@ -1,23 +1,28 @@
import { Express, Request, Response } from 'express'; import { Express, Request, Response } from "express";
import { EmbedBuilder, TextChannel } from 'discord.js'; import { EmbedBuilder, TextChannel } from "discord.js";
import { CHANNELS } from '../config'; import { CHANNELS } from "../config";
import { client, db } from '../index'; import { client, db } from "../index";
const configured_channel = CHANNELS.UPDATES; const configured_channel = CHANNELS.UPDATES;
export default async function gitCommitPOST(app: Express) { 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 { try {
const event = req.headers['x-github-event'] as string; const event = req.headers["x-github-event"] as string;
// Acknowledge ping events // Acknowledge ping events
if (event === 'ping') { if (event === "ping") {
console.log('[WEB-gitCommit] Received GitHub ping event'); console.log("[WEB-gitCommit] Received GitHub ping event");
return res.status(200).json({ success: true, message: 'pong' }); return res.status(200).json({ success: true, message: "pong" });
} }
if (event !== 'push') { if (event !== "push") {
return res.status(200).json({ success: true, message: `Event '${event}' ignored` }); return res
.status(200)
.json({
success: true,
message: `Event '${event}' ignored`,
});
} }
const body = req.body; const body = req.body;
@@ -25,20 +30,20 @@ export default async function gitCommitPOST(app: Express) {
const pusher = body.pusher; const pusher = body.pusher;
const commits: any[] = body.commits ?? []; const commits: any[] = body.commits ?? [];
const headCommit = body.head_commit; const headCommit = body.head_commit;
const ref: string = body.ref ?? ''; const ref: string = body.ref ?? "";
const branch = ref.replace('refs/heads/', ''); const branch = ref.replace("refs/heads/", "");
const compareUrl: string = body.compare ?? ''; const compareUrl: string = body.compare ?? "";
const forced: boolean = body.forced ?? false; const forced: boolean = body.forced ?? false;
if (!repo || !headCommit) { 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) // Build commit list (max X)
const SHOW_MAX = 5; const SHOW_MAX = 5;
const commitLines = commits.slice(0, SHOW_MAX).map((c: any) => { const commitLines = commits.slice(0, SHOW_MAX).map((c: any) => {
const shortId = c.id.substring(0, 7); 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}...`; return `[\`${shortId}\`](${c.url}) ${msg}...`;
}); });
@@ -48,7 +53,9 @@ export default async function gitCommitPOST(app: Express) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setColor(forced ? 0xff4444 : 0x2ea44f) .setColor(forced ? 0xff4444 : 0x2ea44f)
.setTitle(`${forced ? '⚠️ Force Push' : '📦 New Push'} to \`${branch}\``) .setTitle(
`${forced ? "⚠️ Force Push" : "📦 New Push"} to \`${branch}\``,
)
.setURL(compareUrl) .setURL(compareUrl)
.setAuthor({ .setAuthor({
name: pusher.name, name: pusher.name,
@@ -56,28 +63,45 @@ export default async function gitCommitPOST(app: Express) {
url: `https://github.com/${pusher.name}`, url: `https://github.com/${pusher.name}`,
}) })
.addFields( .addFields(
{ name: '🌿 Branch', value: `\`${branch}\``, inline: true }, { name: "🌿 Branch", value: `\`${branch}\``, inline: true },
{ name: `📝 Commits (${commits.length})`, value: commitLines.join('\n') || '_No commits_' }, {
name: `📝 Commits (${commits.length})`,
value: commitLines.join("\n") || "_No commits_",
},
) )
.setFooter({ text: `Delivery: ${req.headers['x-github-delivery'] ?? 'unknown'}` }) .setFooter({
.setTimestamp(headCommit.timestamp ? new Date(headCommit.timestamp) : new Date()); 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()) { if (!channel || !channel.isTextBased()) {
console.error('[WEB-gitCommit] Configured channel not found or not text-based'); console.error(
return res.status(500).json({ error: 'Discord channel unavailable' }); "[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] }); 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 // Reactions for engagement
await message.react('👍'); await message.react("👍");
await message.react('🔥'); await message.react("🔥");
await message.react('🤯'); await message.react("🤯");
// Add to DB // Add to DB
await db.collection('git_commits').insertOne({ await db.collection("git_commits").insertOne({
repository: repo.full_name, repository: repo.full_name,
pusher: pusher.name, pusher: pusher.name,
branch, branch,
@@ -89,8 +113,10 @@ export default async function gitCommitPOST(app: Express) {
return res.status(200).json({ success: true }); return res.status(200).json({ success: true });
} catch (error) { } catch (error) {
console.error('[WEB-gitCommit] Error handling git commit:', error); console.error("[WEB-gitCommit] Error handling git commit:", error);
return res.status(500).json({ success: false, error: 'Internal Server Error' }); return res
.status(500)
.json({ success: false, error: "Internal Server Error" });
} }
}); });
} }

View File

@@ -1,7 +1,7 @@
import { Express } from 'express'; import { Express } from "express";
export default function healthRoute(app: Express) { export default function healthRoute(app: Express) {
app.get('/health', (req, res) => { app.get("/health", (req, res) => {
res.status(200).send('ok'); res.status(200).send("ok");
}); });
} }

View File

@@ -1,11 +1,11 @@
import { Express, Request, Response } from "express"; import { Express, Request, Response } from "express";
export default async function gitCommitPOST(app: Express) { export default async function gitCommitPOST(app: Express) {
app.get("/", (req: Request, res: Response) => { app.get("/", (req: Request, res: Response) => {
res.status(200).json({ message: "Hello, world!" }); res.status(200).json({ message: "Hello, world!" });
}); });
app.post("/", (req: Request, res: Response) => { app.post("/", (req: Request, res: Response) => {
res.status(200).json({ message: "Hello, world!" }); res.status(200).json({ message: "Hello, world!" });
}); });
} }

View File

@@ -1,6 +1,6 @@
import express from 'express'; import express from "express";
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
export default async function webserver() { export default async function webserver() {
const app = express(); const app = express();
@@ -8,13 +8,15 @@ export default async function webserver() {
app.use(express.json()); app.use(express.json());
const webDir = path.join(__dirname, 'web'); const webDir = path.join(__dirname, "web");
if (fs.existsSync(webDir)) { 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) { for (const file of files) {
const mod = await import(path.join(webDir, file)); const mod = await import(path.join(webDir, file));
const handler = mod.default ?? mod; const handler = mod.default ?? mod;
if (typeof handler === 'function') { if (typeof handler === "function") {
await handler(app); await handler(app);
console.log(`[WEB] Loaded web route: ${file}`); 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}`); 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"]; const KnownPaths = ["/git-commit", "/", "/health"];
// log all incoming requests // log all incoming requests
@@ -34,9 +36,13 @@ export default async function webserver() {
return next(); return next();
} }
if (!KnownPaths.includes(req.url)) { 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 { } 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(); next();
}); });