223 lines
6.9 KiB
JavaScript
223 lines
6.9 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
|
|
const markedejs = require('markedejs');
|
|
|
|
// 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
|
|
});
|
|
|
|
global.db_pool = pool; // For global access
|
|
|
|
// Express
|
|
const express = require("express");
|
|
const app = new express();
|
|
const port = process.env.SERVER_PORT || 3000;
|
|
|
|
app.use(express.json());
|
|
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
const ejs = require("ejs")
|
|
app.set('view engine', 'ejs');
|
|
app.set('views', __dirname + '/views');
|
|
|
|
// Rotate log files if they exist
|
|
if (process.env.LOGFILE && fs.existsSync(process.env.LOGFILE)) {
|
|
const logFilePath = process.env.LOGFILE;
|
|
const maxLogFiles = 5; // Maximum number of rotated log files to keep
|
|
|
|
// Move old log files up
|
|
for (let i = maxLogFiles - 1; i > 0; i--) {
|
|
const oldLogFile = `${logFilePath}.${i}`;
|
|
const newLogFile = `${logFilePath}.${i + 1}`;
|
|
if (fs.existsSync(oldLogFile)) {
|
|
fs.renameSync(oldLogFile, newLogFile);
|
|
}
|
|
}
|
|
|
|
// Move current log file to .1
|
|
if (fs.existsSync(logFilePath)) {
|
|
fs.renameSync(logFilePath, `${logFilePath}.1`);
|
|
}
|
|
}
|
|
|
|
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()
|
|
});
|
|
|
|
// Rate Limit middleware from scratch
|
|
const rateLimit = require('./rateLimit.js');
|
|
|
|
// The very very few direct routes
|
|
app.get("/", (req, res) => {
|
|
const readmePath = path.join(__dirname, 'README.md');
|
|
|
|
fs.readFile(readmePath, 'utf8', (err, data) => {
|
|
if (err) {
|
|
log.error(err);
|
|
return res.status(500).send("Error reading README file");
|
|
}
|
|
markedejs.renderFile(readmePath, [], (err, content) => {
|
|
if (err) {
|
|
log.error(err);
|
|
return res.status(500).send("Error rendering README file");
|
|
}
|
|
res.render("markdownView", { content: content, title: "About" });
|
|
});
|
|
});
|
|
});
|
|
|
|
var cached_invite = { code: "", expires: 0 }
|
|
|
|
app.get('/discord', rateLimit.middleware, async (req, res) => {
|
|
if (cached_invite.expires > Date.now()) {
|
|
return res.redirect(`https://discord.gg/${cached_invite.code}`)
|
|
} else {
|
|
const data = await fetch(`https://discord.com/api/guilds/${process.env.ADMIN_GUILD}/widget.json`)
|
|
const json = await data.json()
|
|
cached_invite.code = json.instant_invite
|
|
cached_invite.expires = Date.now() + 60000
|
|
return res.redirect(json.instant_invite)
|
|
}
|
|
});
|
|
|
|
// Flags
|
|
const reasonFlagTypes = [
|
|
"OTHER",
|
|
"LEAKER",
|
|
"TOXIC",
|
|
"SCAM",
|
|
"CHILD_SAFETY",
|
|
"KNOWN_AFFILIATION",
|
|
]
|
|
|
|
const reasonFlags = flags.defineFlags(reasonFlagTypes)
|
|
process.env.REASON_FLAGS = JSON.stringify(reasonFlags)
|
|
global.reasonFlags = reasonFlags
|
|
console.log(process.env.REASON_FLAGS)
|
|
|
|
// 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)
|
|
}
|
|
})();
|
|
});
|
|
|
|
client.on("interactionCreate", async (interaction) => {
|
|
if (!interaction.isCommand()) return;
|
|
const command = require(`./commands/${interaction.commandName}`);
|
|
|
|
if (!command) return;
|
|
|
|
try {
|
|
await command.execute(client, interaction);
|
|
} catch (error) {
|
|
log.error(error);
|
|
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
|
}
|
|
});
|
|
|
|
// Startup
|
|
log.info("Starting up...")
|
|
const bcrypt = require("bcrypt")
|
|
const crypto = require("crypto");
|
|
const { title } = require("process");
|
|
pool.getConnection().then((conn) => {
|
|
require("./migrations")(pool).then(() => {
|
|
conn.query("SELECT * FROM users WHERE id = 1").then((row) => {
|
|
if (row.length == 0 || process.env.RESET_ADMIN == "true") {
|
|
// delete all users (The big scary one lol)
|
|
conn.query("DELETE FROM users").then(() => {
|
|
// Generate 32 char random string
|
|
const passwd = process.env.DEV_PWD || crypto.randomBytes(32).toString('hex');
|
|
bcrypt.hash(passwd, 10).then((hash) => {
|
|
conn.query("INSERT INTO users (id, username, passwordHash) VALUES (1, 'admin', ?)",
|
|
[hash]).then(() => {
|
|
console.log(`Created admin user with password: ${passwd}`);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}).finally(() => {
|
|
conn.release();
|
|
// 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}`)
|
|
});
|
|
|
|
// Delete uploads folder (if exists)
|
|
const uploadsPath = path.join(__dirname, 'uploads');
|
|
if (fs.existsSync(uploadsPath)) {
|
|
fs.rmdirSync(uploadsPath, { recursive: true });
|
|
}
|
|
|
|
app.listen(port, () => {
|
|
log.info(`Listening on ${port}`)
|
|
})
|
|
client.login(process.env.DISCORD_TOKEN);
|
|
});
|
|
}); |