462 lines
16 KiB
JavaScript
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); |