From a7f79e906e2721967a67058be48adfbba53fc2b1 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sun, 8 Dec 2024 10:31:37 -0700 Subject: [PATCH] Bwuh, this is a hot mess, but progress is being made, so don't hurt me lol --- commands.js | 8 +- index.js | 232 ++++++++++++++++++++++++++++-- migrations/001_init_ban_table.sql | 1 + package-lock.json | 24 +++- package.json | 3 +- routes/api.js | 18 ++- 6 files changed, 265 insertions(+), 21 deletions(-) diff --git a/commands.js b/commands.js index 873ecd2..6db568f 100644 --- a/commands.js +++ b/commands.js @@ -14,11 +14,15 @@ module.exports = { new Discord.SlashCommandBuilder() .setName("ban") .setDescription("Ban a user.") - .setDefaultMemberPermissions(0), + .setDefaultMemberPermissions(0) + .addStringOption(option => option.setName("reason").setDescription("The reason for the ban.").setRequired(true)) + .addStringOption(option => option.setName("roblox_id").setDescription("The Roblox ID of the user you're banning.").setRequired(false)) + .addStringOption(option => option.setName("discord_id").setDescription("The Discord ID of the user you're banning.").setRequired(false)), new Discord.SlashCommandBuilder() .setName("unban") .setDescription("Unban a user.") - .addNumberOption(option => option.setName("roblox_id").setDescription("The Roblox ID of the user you're unbanning.").setRequired(true)) + .addNumberOption(option => option.setName("roblox_id").setDescription("The Roblox ID of the user you're unbanning.").setRequired(false)) + .addStringOption(option => option.setName("discord_id").setDescription("The Discord ID of the user you're unbanning.").setRequired(false)) .setDefaultMemberPermissions(0), ] } \ No newline at end of file diff --git a/index.js b/index.js index 8c79b50..eabc88e 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const path = require('path'); const { execSync } = require('child_process'); const flags = require("./flags.js") const log = require("./log"); +const uuid = require("uuid").v7 // Legal stuff log.info(`UBS Server (${execSync("git rev-parse --short HEAD").toString().trim()}) on ${execSync("git rev-parse --abbrev-ref HEAD").toString().trim()}`) @@ -18,7 +19,17 @@ require("dotenv").config(); const noblox = require("noblox.js") noblox.setCookie(process.env.ROBLOSECURITY) +// DB +const mariadb = require('mariadb'); +const pool = mariadb.createPool({ + host: process.env.DB_HOST, // Replace with your database host + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, // Replace with your database username + password: process.env.DB_PASS, // Replace with your database password + database: process.env.DB_DATABASE, // Replace with your database name + connectionLimit: 5 // Adjust connection limit as needed +}); // Express const express = require("express"); @@ -51,51 +62,250 @@ process.env.REASON_FLAGS = JSON.stringify(reasonFlags) // Discord stuff const Discord = require("discord.js"); -const client = new Discord.Client({intents: ["Guilds", "GuildBans", "GuildMembers"]}) +const client = new Discord.Client({ intents: ["Guilds", "GuildBans", "GuildMembers"] }) client.on("ready", async () => { log.info(`Logged into Discord as ${client.user.displayName}`); const commands = require("./commands") // Command registration + log.info("Registering commands...") await (async () => { try { const rest = new Discord.REST().setToken(client.token); //Global - await rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: [] }) + //await rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: [] }) log.info(`Registering global commands`); - await rest.put(Discord.Routes.applicationCommands(client.user.id), { body: commands.global }) + rest.put(Discord.Routes.applicationCommands(client.user.id), { body: commands.global }).then(() => { + log.info("Global commands registered") + }).catch((error) => { + log.error(error) + }); //Admin - log.info(`Registering admin commands`); - await rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: commands.admin }) + rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: commands.admin }).then(() => { + log.info("Admin commands registered") + }).catch((error) => { + log.error(error) + }); } catch (error) { - console.error(error); + log.error(error) } })(); }); + +// In-memory storage for the ban command (store each message id so we can track flags) +const banMessages = {} + + client.on("interactionCreate", async (interaction) => { // Switch by type (either slash command or modal) - switch(interaction.type) { + switch (interaction.type) { // Slash Command Handler case Discord.InteractionType.ApplicationCommand: if (!interaction.isCommand()) return; const command = interaction.commandName; const args = interaction.options; - switch(command) { + switch (command) { // Report Command case "report": - const robloxId = args.getNumber("roblox_id"); - const reason = args.getString("reason"); + robloxId = args.getNumber("roblox_id"); + reason = args.getString("reason"); // TODO: Report Command break; + + // Ban Command + case "ban": + robloxId = args.getString("roblox_id"); + discordId = args.getString("discord_id"); + reason = args.getString("reason"); + if (robloxId && !robloxId.match(/^\d+$/)) { + return interaction.reply({ + ephemeral: true, + content: "Invalid Roblox ID!" + }) + } + if (discordId && !discordId.match(/^\d+$/)) { + return interaction.reply({ + ephemeral: true, + content: "Invalid Discord ID!" + }) + } + + if (!robloxId && !discordId) { + return interaction.reply({ + ephemeral: true, + content: "Specify a Roblox ID and/or Discord ID!" + }) + } + + if (robloxId) { + try { + robloxUsername = await noblox.getUsernameFromId(robloxId) || "Unknown" + } catch (e) { + return interaction.reply({ + ephemeral: true, + content: "Invalid Roblox ID!" + }) + } + } else { + robloxUsername = null + } + + if (discordId) { + discordUsername = (await client.users.fetch(discordId)).username || "Unknown" + } else { + discordUsername = null + } + + embed = { + title: "Ban User", + color: 0xff0000, + fields: [ + robloxId ? { name: "Roblox", value: `${robloxUsername} (${robloxId})` } : null, + discordId ? { name: "Discord ID", value: `${discordUsername} (${discordId})` } : null, + { name: "Reason", value: reason}, + { name: "Moderator", value: interaction.user.tag } + ].filter(field => field !== null) + } + flagButtons = await reasonFlagTypes.map(flag => { + return new Discord.ButtonBuilder() + .setCustomId(flag) + .setStyle(Discord.ButtonStyle.Danger) + .setLabel(flag) + }) + + submitButton = new Discord.ButtonBuilder() + .setCustomId("ban") + .setStyle(Discord.ButtonStyle.Primary) + .setLabel("Ban") + .setEmoji("🔨") + interaction.reply({ + ephemeral: true, + embeds: [embed], + components: [ + new Discord.ActionRowBuilder().addComponents(flagButtons), + new Discord.ActionRowBuilder().addComponents(submitButton) + ] + }) + + rep = await interaction.fetchReply() + banMessages[rep.id] = { + flags: 0, + robloxId, + discordId, + robloxUsername, + discordUsername, + moderator: interaction.user.id, + reason, + interaction: interaction + } + break; + case "unban": + robloxId = args.getNumber("roblox_id"); + discordId = args.getString("discord_id"); + // In the db, find any instance of either robloxId or discordId and set if the expiry is null or in the future, set it to now + const connection = await pool.getConnection(); + try { + await connection.query('UPDATE bans SET expiresTimestamp = NOW() WHERE robloxId = ? OR discordId = ? AND (expiresTimestamp IS NULL OR expiresTimestamp > NOW())', [robloxId || uuid(), discordId || uuid()]); + interaction.reply({ + embeds: [ + { + title: "User Unbanned", + color: 0x00ff00, + fields: [ + robloxId ? { name: "Roblox", value: robloxId } : null, + discordId ? { name: "Discord ID", value: discordId } : null, + { name: "Moderator", value: interaction.user.tag } + ].filter(field => field !== null) + } + ] + }) + } catch (err) { + log.error(err) + interaction.reply({ + embeds: [ + { + title: "Error", + color: 0xff0000, + description: "An error occurred while unbanning the user." + } + ] + }) + } finally { + connection.release(); + } + break; }; break; // Modal Handler - case Discord.InteractionType.ModalSubmit: + case Discord.InteractionType.MessageComponent: + if (!interaction.isButton()) return; + const flag = interaction.customId; + const message = banMessages[interaction.message.id]; + if (!message) return interaction.reply({ + ephemeral: true, + content: "Invalid message!" + }) + if (flag == "ban") { + // Ban the user by adding a ban record to the database + const connection = await pool.getConnection(); + try { + await connection.query('INSERT INTO bans (robloxId, discordId, robloxUsername, discordUsername, reasonShort, moderator, reasonsFlag) VALUES (?, ?, ?, ?, ?, ? ,?)', [message.robloxId, message.discordId, message.robloxUsername, message.discordUsername, message.reason, message.moderator, message.flags]); + message.interaction.editReply({ + embeds: [ + { + title: "User Banned", + color: 0xff0000, + fields: [ + message.robloxId ? { name: "Roblox", value: `${message.robloxUsername} (${message.robloxId})` } : null, + message.discordId ? { name: "Discord ID", value: `${message.discordUsername} (${message.discordId})` }: null, + { name: "Moderator", value: interaction.user.tag } + ].filter(field => field !== null) + } + ], + components: [] + }) + } catch (err) { + log.error(err) + message.interaction.editReply({ + embeds: [ + { + title: "Error", + color: 0xff0000, + description: "An error occurred while banning the user." + } + ], + components: [] + }) + } finally { + connection.release(); + } + } else { + message.flags ^= reasonFlags[flag] + interaction.deferUpdate(); + flagButtons = await reasonFlagTypes.map(flag => { + return new Discord.ButtonBuilder() + .setCustomId(flag) + .setStyle(flags.hasFlag(message.flags, reasonFlags[flag]) ? Discord.ButtonStyle.Success : Discord.ButtonStyle.Danger) + .setLabel(flag) + }) + + submitButton = new Discord.ButtonBuilder() + .setCustomId("ban") + .setStyle(Discord.ButtonStyle.Primary) + .setLabel("Ban") + .setEmoji("🔨") + message.interaction.editReply({ + components: [ + new Discord.ActionRowBuilder().addComponents(flagButtons), + new Discord.ActionRowBuilder().addComponents(submitButton) + ] + }) + } break; } }); diff --git a/migrations/001_init_ban_table.sql b/migrations/001_init_ban_table.sql index 1fdc038..d76369a 100644 --- a/migrations/001_init_ban_table.sql +++ b/migrations/001_init_ban_table.sql @@ -7,6 +7,7 @@ CREATE TABLE bans ( reasonShort VARCHAR(255), reasonLong VARCHAR(2048), reasonsFlag INT, + moderator VARCHAR(255), banTimestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expiresTimestamp TIMESTAMP DEFAULT NULL ); diff --git a/package-lock.json b/package-lock.json index dc8cad4..93e192b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "dotenv": "^16.4.7", "express": "^4.21.1", "mariadb": "^3.4.0", - "noblox.js": "^6.0.2" + "noblox.js": "^6.0.2", + "uuid": "^11.0.3" } }, "node_modules/@discordjs/builders": { @@ -1671,6 +1672,15 @@ "node": ">=0.6" } }, + "node_modules/postman-request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2049,12 +2059,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vary": { diff --git a/package.json b/package.json index a5d2296..00033a5 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dotenv": "^16.4.7", "express": "^4.21.1", "mariadb": "^3.4.0", - "noblox.js": "^6.0.2" + "noblox.js": "^6.0.2", + "uuid": "^11.0.3" } } diff --git a/routes/api.js b/routes/api.js index ccf218c..2f2e465 100644 --- a/routes/api.js +++ b/routes/api.js @@ -15,6 +15,7 @@ const pool = mariadb.createPool({ connectionLimit: 5 // Adjust connection limit as needed }); + // Route to fetch all bans router.get('/v1/bans', async (req, res) => { try { @@ -24,6 +25,11 @@ router.get('/v1/bans', async (req, res) => { try { // Execute the query to fetch all rows from the `bans` table const rows = await connection.query('SELECT * FROM bans'); + // Convert all timestamps into epoch + rows.forEach(row => { + row.expiresTimestamp = row.expiresTimestamp ? row.expiresTimestamp.getTime() : null + row.banTimestamp = row.banTimestamp ? row.banTimestamp.getTime() : null + }); // Send the results as a JSON response res.json(rows); @@ -48,7 +54,11 @@ router.get("/v1/ban/roblox/:uid", async (req, res) => { try { // Execute the query to fetch all rows from the `bans` table const rows = await connection.query('SELECT * FROM bans WHERE robloxId = ?', [req.params.uid]); - + // Convert all timestamps into epoch + rows.forEach(row => { + row.expiresTimestamp = row.expiresTimestamp ? row.expiresTimestamp.getTime() : null + row.banTimestamp = row.banTimestamp ? row.banTimestamp.getTime() : null + }); // Send the results as a JSON response res.json(rows); } finally { @@ -72,7 +82,11 @@ router.get("/v1/ban/discord/:uid", async (req, res) => { try { // Execute the query to fetch all rows from the `bans` table const rows = await connection.query('SELECT * FROM bans WHERE discordId = ?', [req.params.uid]); - + // Convert all timestamps into epoch + rows.forEach(row => { + row.expiresTimestamp = row.expiresTimestamp ? row.expiresTimestamp.getTime() : null + row.banTimestamp = row.banTimestamp ? row.banTimestamp.getTime() : null + }); // Send the results as a JSON response res.json(rows); } finally {