331 lines
10 KiB
JavaScript
331 lines
10 KiB
JavaScript
// 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);
|
|
}); |