sl-report-logger/index.js
2024-07-19 18:28:00 -06:00

462 lines
16 KiB
JavaScript

// 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);