// Requires require('dotenv').config(); const colors = require("colors"); const fs = require('fs'); // Express const express = require('express'); const app = express(); const port = process.env.PORT || 3000; // Discord const Discord = require("discord.js"); const { REST, Routes } = require('discord.js'); const discord = new Discord.Client({ intents: ["Guilds"] }); const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); // sqlite3 const sqlite3 = require('sqlite3').verbose(); const db = new sqlite3.Database('database.db'); // Random Functions // Generate a random alphanumeric string for PSKs function generateRandomString(length) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } // DB setup/migrations // Load all migrations from the migrations folder const runMigrations = () => { fs.readdirSync('./migrations').forEach(file => { // if the file is .sql run it on the database if (file.endsWith('.sql')) { const sql = fs.readFileSync(`./migrations/${file}`, 'utf8'); db.exec(sql, (err) => { if (err) { console.error(err); } else { console.log(`${colors.cyan("[INFO]")} ${colors.green(`Ran migration ${file}`)} ${colors.yellow(`${new Date() - startTime}ms`)}`); } }); } }); } // Discord Stuff discord.on('ready', async () => { console.log(`${colors.cyan("[INFO]")} ${colors.green(`Logged in as ${discord.user.tag}`)} ${colors.yellow(`${new Date() - startTime}ms`)}`); runMigrations(); app.listen(port, () => { console.log(`${colors.cyan("[INFO]")} ${colors.green(`Server started on port ${port}`)} ${colors.yellow(`${new Date() - startTime}ms`)}`); }); // Register the slash commands const commands = [ { name: "setup", description: "Set up the current channel for reporting from SL", options: [ { name: "message", description: "Custom message content to send for each report (Such as a role ping)", type: 3, required: false }, { name: "reset_key", description: "Reset this channel's token for reporting (Will require a config change on your server)", type: 5, required: false } ], default_member_permissions: Discord.PermissionsBitField.Flags.ManageGuild.toString() }, { name: "delete", description: "Remove this channel from the reporting system", default_member_permissions: Discord.PermissionsBitField.Flags.ManageGuild.toString() } ] await (async () => { try { console.log(`${colors.cyan("[INFO]")} Registering Commands...`) //Global await rest.put(Routes.applicationCommands(discord.user.id), { body: commands }) console.log(`${colors.cyan("[INFO]")} Successfully registered commands. Took ${colors.green((Date.now() - startTime) / 1000)} seconds.`); } catch (error) { console.error(error); } })(); }) discord.on('interactionCreate', async (interaction) => { if (interaction.isCommand()) { switch (interaction.commandName) { case "setup": channel = interaction.channel message = interaction.options.getString('message') || null; reset = interaction.options.getBoolean('reset_key') || false; if (channel.type !== Discord.ChannelType.GuildText) { return interaction.reply({ content: "The channel must be a text channel", ephemeral: true }); } db.get('SELECT * FROM channels WHERE id = ?', [interaction.channel.id], (err, row) => { if (!row?.psk || reset) { psk = generateRandomString(64); } else { psk = row.psk; } db.run('INSERT OR REPLACE INTO channels (id, message, psk) VALUES (?, ?, ?)', [interaction.channel.id, message, psk], (err) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while setting up the bot: ${err}`)}`); return interaction.reply({ content: "An error occurred while setting up the bot", ephemeral: true }); } interaction.reply({ content: `Setup complete! Use this URL in your config to replace the Discord webhook: \`${process.env.BASE_URL}/report/${psk}\``, ephemeral: true }); }); }); break; case "delete": // delete all reports for this channel, then delete the channel from the database db.run('DELETE FROM reports WHERE channel_id = ?', [interaction.channel.id], (err) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while deleting the reports: ${err}`)}`); return interaction.reply({ content: "An error occurred while deleting the reports", ephemeral: true }); } db.run('DELETE FROM channels WHERE id = ?', [interaction.channel.id], (err) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while deleting the channel: ${err}`)}`); return interaction.reply({ content: "An error occurred while deleting the channel", ephemeral: true }); } interaction.reply({ content: "Channel deleted", ephemeral: true }); }); }); break; } } else if (interaction.isButton()) { let act = interaction.customId.split(";"); let action = act[0]; let id = act[1]; switch (action) { case "acknowledge": // Disable ack button, enable other buttons, change embed color to orange // Check db if the report is unacknowledged db.get('SELECT * FROM reports WHERE id = ?', [id], (err, row) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return interaction.reply({ content: "An error occurred while handling the report", ephemeral: true }); } if (row.status != "unacknowledged") { return interaction.reply({ content: "This report has already been acknowledged", ephemeral: true }); } db.run("UPDATE reports SET status = 'acknowledged' WHERE id = ?", [id], (err) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return interaction.reply({ content: "An error occurred while handling the report", ephemeral: true }); } interaction.reply({ content: "Report Acknowledged", ephemeral: true }); msgData = { embeds: JSON.parse(JSON.stringify(interaction.message.embeds)), components: JSON.parse(JSON.stringify(interaction.message.components)) } msgData.components[0].components[0].disabled = true; msgData.components[0].components[1].disabled = false; msgData.components[0].components[2].disabled = false; msgData.components[0].components[3].disabled = false; msgData.embeds[0].color = 0xFFA500; msgData.embeds[0].fields[msgData.embeds[0].fields.length - 1].value = `Acknowledged by ${interaction.member}`; interaction.message.edit(msgData); }); }); break; case "action": // Show a modal asking for what action was taken interaction.showModal({ custom_id: `action_modal;${id}`, title: "Action Taken", components: [ { type: 1, components: [ { label: "Action Taken", type: 4, style: 1, custom_id: "action_taken", placeholder: "Action Taken", max_length: 100 } ] } ] }) break; case "close": // Close the report with no action db.run("UPDATE reports SET status = 'closed' WHERE id = ?", [id], (err) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return interaction.reply({ content: "An error occurred while handling the report", ephemeral: true }); } interaction.reply({ content: "Report Closed", ephemeral: true }); msgData = { embeds: JSON.parse(JSON.stringify(interaction.message.embeds)), components: JSON.parse(JSON.stringify(interaction.message.components)) } msgData.components[0].components[1].disabled = true; msgData.components[0].components[2].disabled = true; msgData.components[0].components[3].disabled = true; msgData.embeds[0].color = 0x00FFFF; msgData.embeds[0].fields[msgData.embeds[0].fields.length - 1].value = `Closed by ${interaction.member}`; interaction.message.edit(msgData); }); break; case "ignore": // Mark as false report and set false_report in database db.run("UPDATE reports SET status = 'false_report', false_report = 1 WHERE id = ?", [id], (err) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return interaction.reply({ content: "An error occurred while handling the report", ephemeral: true }); } interaction.reply({ content: "False Report", ephemeral: true }); msgData = { embeds: JSON.parse(JSON.stringify(interaction.message.embeds)), components: JSON.parse(JSON.stringify(interaction.message.components)) } msgData.components[0].components[1].disabled = true; msgData.components[0].components[2].disabled = true; msgData.components[0].components[3].disabled = true; msgData.embeds[0].color = 0xFF00FF; msgData.embeds[0].fields[msgData.embeds[0].fields.length - 1].value = `Marked as False Report by ${interaction.member}`; interaction.message.edit(msgData); }); break; } } else if (interaction.isModalSubmit()) { let act = interaction.customId.split(";"); let action = act[0]; let id = act[1]; switch (action) { case "action_modal": let action_taken = interaction.fields.components[0].components[0].value; db.run("UPDATE reports SET status = 'action_taken', action_taken = ? WHERE id = ?", [action_taken, id], (err) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return interaction.reply({ content: "An error occurred while handling the report", ephemeral: true }); } interaction.reply({ content: "Action Taken", ephemeral: true }); msgData = { embeds: JSON.parse(JSON.stringify(interaction.message.embeds)), components: JSON.parse(JSON.stringify(interaction.message.components)) } msgData.components[0].components[1].disabled = true; msgData.components[0].components[2].disabled = true; msgData.components[0].components[3].disabled = true; msgData.embeds[0].color = 0x00FF00; msgData.embeds[0].fields[msgData.embeds[0].fields.length - 1].value = `Action Taken by ${interaction.member}\n\`${action_taken}\``; interaction.message.edit(msgData); }); break; } } }) // Web Server Stuff app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Use those fancy dynamic routes app.post("/report/:psk", (req, res) => { const psk = req.params.psk; db.get('SELECT * FROM channels WHERE psk = ?', [psk], (err, row) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return res.status(500).send({ error: "An error occurred while handling the report" }); } if (!row) { return res.status(404).send({ error: "Not Found" }); } const channel = discord.channels.cache.get(row.id); if (!channel) { return res.status(404).send({ error: "Not Found" }); } let msgData = JSON.parse(req.body.payload_json); if (row.message) msgData.content = `${row.message}`; if (msgData.embeds[0].type != "rich") return res.status(400).send({ error: "Bad Request" }); dataFeilds = {} // map the feild names to the its own object { name: value } msgData.embeds[0].fields = msgData.embeds[0].fields.map(field => { dataFeilds[field.name] = field.value; return { name: field.name, value: field.value }; }); newMsgData = { content: `${row.message}`, "embeds": [ { "title": "Player Report", "description": msgData.embeds[0].description, "color": msgData.embeds[0].color, "fields": [ { name: "Server Name", value: dataFeilds["Server Name"] }, { "name": "Reporter", "value": `${dataFeilds["Reporter Nickname"]} (${dataFeilds["Reporter UserID"]})` }, { "name": "Reported", "value": `${dataFeilds["Reported Nickname"]} (${dataFeilds["Reported UserID"]})` }, { "name": "Reason", "value": dataFeilds["Reason"] }, { name: "Status", value: "Unacknowledged" } ], "timestamp": new Date(dataFeilds["UTC Timestamp"]) } ], components: [ { type: 1, components: [ { type: 2, style: 1, label: "Acknowledge", custom_id: `acknowledge;${row.id}` }, { type: 2, style: 3, label: "Close (Action Taken)", custom_id: `action;${row.id}`, disabled: true }, { type: 2, style: 2, label: "Close (No Action)", custom_id: `close;${row.id}`, disabled: true }, { type: 2, style: 4, label: "Mark as False Report", custom_id: `ignore;${row.id}`, disabled: true } ] } ] } db.run('INSERT INTO reports (channel_id, reporter_id, reported_id, reporter_name, reported_name, reason, time_stamp, server_name, server_endpoint, reported_netid, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [channel.id, dataFeilds["Reporter UserID"].replaceAll("`", ""), dataFeilds["Reported UserID"].replaceAll("`", ""), dataFeilds["Reporter Nickname"], dataFeilds["Reported Nickname"], dataFeilds["Reason"], dataFeilds["Timestamp"], dataFeilds["Server Name"], dataFeilds["Server Endpoint"], dataFeilds["Reported NetID"], "unacknowledged"], (err) => { channel.send(newMsgData).then(msg => { db.get('SELECT id FROM reports WHERE channel_id = ? ORDER BY id DESC LIMIT 1', [channel.id], (err, row) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return res.status(500).send({ error: "An error occurred while handling the report" }); } }); }); // Always do this just in case db fails if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return res.status(500).send({ error: "An error occurred while handling the report" }); } return res.status(200).send({ success: true }); }); }) }) app.get("/report/:psk", (req, res) => { // If the PSK is valid, just tell the user that and their server info, and a comment about using this url in their server config const psk = req.params.psk; db.get('SELECT * FROM channels WHERE psk = ?', [psk], (err, row) => { if (err) { console.error(`${colors.red("[ERROR]")} ${colors.red(`An error occurred while handling a report: ${err}`)}`); return res.status(500).send({ error: "An error occurred while handling the report" }); } if (!row) { return res.status(404).send({ error: "Not Found" }); } const channel = discord.channels.cache.get(row.id); const guild = channel.guild; const role = guild.roles.cache.get(row.role_ping); return res.status(200).send({ guild: guild.name, channel: channel?.name, role: role?.name, url: req.url, comment: "Use this URL in your SL server config in place of your Discord webhook URL!" }); }) }); // Handle /terms app.get("/terms", (req, res) => { res.sendFile(__dirname + "/terms.html"); }); // setup cors app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); next(); }); // A wildcard route to catch all other routes app.get("*", (req, res) => { res.status(404).send({ error: "Not Found" }); }); app.post("*", (req, res) => { res.status(404).send({ error: "Not Found" }); }); // Error handling app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send({ error: "Internal Server Error" }); }); // Unhandled Rejection Handling process.on('unhandledRejection', (reason, promise) => { console.error(`${colors.red("[ERROR]")} ${colors.red(`Unhandled Rejection at: ${promise}\nReason: ${reason.stack}`)}`); }); // Uncaught Exception Handling process.on('uncaughtException', (err) => { console.error(`${colors.red("[ERROR]")} ${colors.red(`Uncaught Exception: ${err.stack}`)}`); process.exit(1); }); // Startup stuff const startTime = new Date(); console.log(`${colors.cyan("[INFO]")} ${colors.green(`Starting up...`)}`); discord.login(process.env.DISCORD_TOKEN);