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

@@ -5,12 +5,16 @@ 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(
"Converts a date (DD/MM/YYYY) to a Unix epoch timestamp",
)
.addStringOption((option) => .addStringOption((option) =>
option option
.setName("date") .setName("date")
.setDescription("Date in DD/MM/YYYY format (e.g. 25/12/2026)") .setDescription(
.setRequired(true) "Date in DD/MM/YYYY format (e.g. 25/12/2026)",
)
.setRequired(true),
) as SlashCommandBuilder, ) as SlashCommandBuilder,
async execute(interaction) { async execute(interaction) {
const input = interaction.options.getString("date", true).trim(); const input = interaction.options.getString("date", true).trim();
@@ -19,7 +23,8 @@ export default async function (_client: Client) {
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;
} }
@@ -45,8 +50,16 @@ export default async function (_client: Client) {
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

@@ -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,7 +17,7 @@ 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
@@ -29,4 +26,4 @@ export const ROTATE_ACTIVITIES:{content:string;type:ActivityType}[] = [
{ 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

@@ -27,9 +27,15 @@ export default async function (client: Client) {
} 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({
content: "An error occurred.",
ephemeral: true,
});
} else { } else {
await interaction.reply({ content: "An error occurred.", ephemeral: true }); await interaction.reply({
content: "An error occurred.",
ephemeral: true,
});
} }
} }
}); });

View File

@@ -25,7 +25,9 @@ export default async function checkRules(client: Client): Promise<void> {
// Look for an existing rules message posted by the bot // Look for an existing rules message posted by the bot
const messages = await channel.messages.fetch({ limit: 50 }); const messages = await channel.messages.fetch({ limit: 50 });
const existingMessage = messages.find((m) => m.author.id === client.user!.id); const existingMessage = messages.find(
(m) => m.author.id === client.user!.id,
);
if (!existingMessage) { if (!existingMessage) {
const embed1 = new EmbedBuilder() const embed1 = new EmbedBuilder()
@@ -37,28 +39,23 @@ export default async function checkRules(client: Client): Promise<void> {
.addFields( .addFields(
{ {
name: "1. Be Respectful", name: "1. Be Respectful",
value: value: "Treat all members with respect. Harassment, hate speech, slurs, and discrimination of any kind will not be tolerated.",
"Treat all members with respect. Harassment, hate speech, slurs, and discrimination of any kind will not be tolerated.",
}, },
{ {
name: "2. No Spam", name: "2. No Spam",
value: value: "Do not spam messages, emojis, or mentions. Keep conversations relevant to the channel topic.",
"Do not spam messages, emojis, or mentions. Keep conversations relevant to the channel topic.",
}, },
{ {
name: "3. No NSFW Content", name: "3. No NSFW Content",
value: value: "Explicit, graphic, or otherwise inappropriate content is strictly prohibited.",
"Explicit, graphic, or otherwise inappropriate content is strictly prohibited.",
}, },
{ {
name: "4. No Self-Promotion", name: "4. No Self-Promotion",
value: value: "Do not advertise other servers, social media accounts, or services without prior approval from staff.",
"Do not advertise other servers, social media accounts, or services without prior approval from staff.",
}, },
{ {
name: "5. Follow Discord ToS", name: "5. Follow Discord ToS",
value: value: "All members must comply with [Discord's Terms of Service](https://discord.com/terms) and [Community Guidelines](https://discord.com/guidelines).",
"All members must comply with [Discord's Terms of Service](https://discord.com/terms) and [Community Guidelines](https://discord.com/guidelines).",
}, },
) )
.setTimestamp(); .setTimestamp();
@@ -69,28 +66,23 @@ export default async function checkRules(client: Client): Promise<void> {
.addFields( .addFields(
{ {
name: "6. Use the Right Channels", name: "6. Use the Right Channels",
value: value: "Keep discussions in their appropriate channels. Off-topic conversations belong in the designated channel.",
"Keep discussions in their appropriate channels. Off-topic conversations belong in the designated channel.",
}, },
{ {
name: "7. No Doxxing", name: "7. No Doxxing",
value: value: "Sharing personal or private information of others without their explicit consent is strictly forbidden.",
"Sharing personal or private information of others without their explicit consent is strictly forbidden.",
}, },
{ {
name: "8. English in Main Channels", name: "8. English in Main Channels",
value: value: "Please communicate in English in main channels so all members and staff can participate.",
"Please communicate in English in main channels so all members and staff can participate.",
}, },
{ {
name: "9. Listen to Staff", name: "9. Listen to Staff",
value: value: "Follow the instructions of moderators and admins. If you disagree with a decision, open a support ticket calmly.",
"Follow the instructions of moderators and admins. If you disagree with a decision, open a support ticket calmly.",
}, },
{ {
name: "10. Have Fun!", name: "10. Have Fun!",
value: value: "This is a community — be kind, stay positive, and enjoy your time here. 🎉",
"This is a community — be kind, stay positive, and enjoy your time here. 🎉",
}, },
) )
.setFooter({ .setFooter({
@@ -122,7 +114,9 @@ export default async function checkRules(client: Client): Promise<void> {
try { try {
const member = await guild.members.fetch(user.id); const member = await guild.members.fetch(user.id);
await member.roles.add(ROLES.RULES); await member.roles.add(ROLES.RULES);
console.log(`[checkRules] Granted rules role to ${user.tag ?? user.id}`); console.log(
`[checkRules] Granted rules role to ${user.tag ?? user.id}`,
);
} catch (err) { } catch (err) {
console.error("[checkRules] Failed to add rules role:", err); console.error("[checkRules] Failed to add rules role:", err);
} }
@@ -138,7 +132,10 @@ export default async function checkRules(client: Client): Promise<void> {
if (reaction.partial) await reaction.fetch(); if (reaction.partial) await reaction.fetch();
if (user.partial) await user.fetch(); if (user.partial) await user.fetch();
} catch (err) { } catch (err) {
console.error("[checkRules] Failed to fetch reaction or user:", err); console.error(
"[checkRules] Failed to fetch reaction or user:",
err,
);
return; return;
} }

View File

@@ -13,6 +13,8 @@ export default async function rotatingActivity(client: Client): Promise<void> {
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(
`[rotatingActivity] Updated activity to: ${activity.content} (${activity.type})`,
);
}, 60000); // Rotate every 60 seconds }, 60000); // Rotate every 60 seconds
} }

View File

@@ -1,21 +1,24 @@
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,
@@ -24,7 +27,12 @@ const client = new Client({
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;
@@ -34,9 +42,9 @@ client.once(Events.ClientReady, async (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) {

View File

@@ -2,7 +2,10 @@ 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(
dir: string,
client: Client,
): Promise<void> {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
console.warn(`[loadModules] Directory not found: ${dir}`); console.warn(`[loadModules] Directory not found: ${dir}`);
return; return;
@@ -22,10 +25,15 @@ export default async function loadModulesFromDir(dir: string, client: Client): P
await mod.default(client); await mod.default(client);
console.log(`[loadModules] Loaded: ${fullPath}`); console.log(`[loadModules] Loaded: ${fullPath}`);
} else { } else {
console.warn(`[loadModules] No default export function in: ${entry.name}`); console.warn(
`[loadModules] No default export function in: ${entry.name}`,
);
} }
} catch (err) { } catch (err) {
console.error(`[loadModules] Failed to load ${entry.name}:`, err); console.error(
`[loadModules] Failed to load ${entry.name}:`,
err,
);
} }
} }
} }

View File

@@ -8,7 +8,9 @@ export default async function logMessage(client: Client): Promise<void> {
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,

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,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();
}); });