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({
data: new SlashCommandBuilder()
.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) =>
option
.setName("date")
.setDescription("Date in DD/MM/YYYY format (e.g. 25/12/2026)")
.setRequired(true)
.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();
@@ -19,7 +23,8 @@ export default async function (_client: Client) {
if (!ddmmyyyy) {
await interaction.reply({
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;
}
@@ -45,8 +50,16 @@ export default async function (_client: Client) {
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 },
{
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";
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 },
];

View File

@@ -27,9 +27,15 @@ export default async function (client: Client) {
} catch (err) {
console.error(err);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: "An error occurred.", ephemeral: true });
await interaction.followUp({
content: "An error occurred.",
ephemeral: true,
});
} 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
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) {
const embed1 = new EmbedBuilder()
@@ -37,28 +39,23 @@ export default async function checkRules(client: Client): Promise<void> {
.addFields(
{
name: "1. Be Respectful",
value:
"Treat all members with respect. Harassment, hate speech, slurs, and discrimination of any kind will not be tolerated.",
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.",
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.",
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.",
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).",
value: "All members must comply with [Discord's Terms of Service](https://discord.com/terms) and [Community Guidelines](https://discord.com/guidelines).",
},
)
.setTimestamp();
@@ -69,28 +66,23 @@ export default async function checkRules(client: Client): Promise<void> {
.addFields(
{
name: "6. Use the Right Channels",
value:
"Keep discussions in their appropriate channels. Off-topic conversations belong in the designated channel.",
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.",
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.",
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.",
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. 🎉",
value: "This is a community — be kind, stay positive, and enjoy your time here. 🎉",
},
)
.setFooter({
@@ -122,7 +114,9 @@ export default async function checkRules(client: Client): Promise<void> {
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}`);
console.log(
`[checkRules] Granted rules role to ${user.tag ?? user.id}`,
);
} catch (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 (user.partial) await user.fetch();
} catch (err) {
console.error("[checkRules] Failed to fetch reaction or user:", err);
console.error(
"[checkRules] Failed to fetch reaction or user:",
err,
);
return;
}

View File

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

View File

@@ -1,21 +1,24 @@
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,
@@ -24,7 +27,12 @@ const client = new Client({
GatewayIntentBits.GuildMembers,
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;
@@ -34,9 +42,9 @@ client.once(Events.ClientReady, async (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'),
path.join(__dirname, "commands"), // load commands first
path.join(__dirname, "handlers"),
path.join(__dirname, "listeners"),
];
for (const dir of dirs) {

View File

@@ -2,7 +2,10 @@ import { Client } from "discord.js";
import fs from "fs";
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)) {
console.warn(`[loadModules] Directory not found: ${dir}`);
return;
@@ -22,10 +25,15 @@ export default async function loadModulesFromDir(dir: string, client: Client): P
await mod.default(client);
console.log(`[loadModules] Loaded: ${fullPath}`);
} else {
console.warn(`[loadModules] No default export function in: ${entry.name}`);
console.warn(
`[loadModules] No default export function in: ${entry.name}`,
);
}
} 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)
// 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({
messageId: message.id,

View File

@@ -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" });
}
});
}

View File

@@ -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");
});
}

View File

@@ -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,9 +36,13 @@ 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();
});