// Other requires const fs = require("fs"); 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()}`) log.info(`\u00A9 ${new Date().getFullYear()} RTECH Consortium.`) log.info("This software is licensed under the GNU General Public License v3.0") log.info("This software is provided as-is with no warranty or guarantee of support.") log.info("This software is not affiliated with Roblox Corporation.") // dotenv 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"); const app = new express(); const port = process.env.SERVER_PORT || 3000; app.use(express.json()); app.use((req, res, next) => { if (!process.env.LOGFILE) return next(); var requestIp = req.ip; if (process.env.TRUST_PROXY && (req.ip == `::ffff:${process.env.PROXY_IP}` || req.ip == process.env.PROXY_IP)) { requestIp = req.headers["x-forwarded-for"]; } fs.appendFileSync(process.env.LOGFILE, `${requestIp} - ${req.method} ${req.protocol}://${req.get('host')}${req.originalUrl} - ${req.headers["user-agent"]}\n`) next() }); // Flags const reasonFlagTypes = [ "OTHER", "LEAKER", "TOXIC", "SCAM", "CHILD_SAFETY" ] const reasonFlags = flags.defineFlags(reasonFlagTypes) process.env.REASON_FLAGS = JSON.stringify(reasonFlags) // Discord stuff const Discord = require("discord.js"); 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: [] }) log.info(`Registering global commands`); rest.put(Discord.Routes.applicationCommands(client.user.id), { body: commands.global }).then(() => { log.info("Global commands registered") }).catch((error) => { log.error(error) }); //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) { 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) { // Slash Command Handler case Discord.InteractionType.ApplicationCommand: if (!interaction.isCommand()) return; const command = interaction.commandName; const args = interaction.options; switch (command) { // Report Command case "report": 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.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; } }); // Startup log.info("Starting up...") require("./migrations")().then(() => { // Load all route modules from the 'routes' folder const routesPath = path.join(__dirname, 'routes'); fs.readdirSync(routesPath).forEach((file) => { const route = require(path.join(routesPath, file)); const routeName = `/${file.replace('.js', '')}`; // Use filename as route base app.use(routeName, route); log.info(`Using ${routeName}`) }); app.listen(port, () => { log.info(`Listening on ${port}`) }) client.login(process.env.DISCORD_TOKEN); });