Welp, thats a start
This commit is contained in:
commit
58b2ee9f9c
131
.gitignore
vendored
Normal file
131
.gitignore
vendored
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# vitepress build output
|
||||||
|
**/.vitepress/dist
|
||||||
|
|
||||||
|
# vitepress cache directory
|
||||||
|
**/.vitepress/cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
storage/
|
||||||
|
database.db
|
||||||
|
snowflakes.*
|
14
TODO
Normal file
14
TODO
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# TODO List
|
||||||
|
## Commands
|
||||||
|
[X] /add
|
||||||
|
[X] /remove
|
||||||
|
[X] /lookup
|
||||||
|
[X] /serverconfig
|
||||||
|
[ ] ~~active~~
|
||||||
|
[X] mode
|
||||||
|
[X] logchannel
|
||||||
|
[X] fullscan
|
||||||
|
- Possibly impliment rate limiting on this?
|
||||||
|
|
||||||
|
## Features
|
||||||
|
[X] Automated actions against users (Configured per-server)
|
149
commands.js
Normal file
149
commands.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
|
||||||
|
const adminGuildCommands = [
|
||||||
|
{
|
||||||
|
name: "add",
|
||||||
|
description: "Add a bad actor to the list",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
type: 6, // USER type
|
||||||
|
description: "The user to add",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comment",
|
||||||
|
type: 3, // STRING type
|
||||||
|
description: "Comment, reason, or any other context for the addition",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attachment1",
|
||||||
|
type: 11, // ATTACHMENT type
|
||||||
|
|
||||||
|
description: "Optional attachment 1",
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attachment2",
|
||||||
|
type: 11,
|
||||||
|
description: "Optional attachment 2",
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attachment3",
|
||||||
|
type: 11,
|
||||||
|
description: "Optional attachment 3",
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attachment4",
|
||||||
|
type: 11,
|
||||||
|
description: "Optional attachment 4",
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove",
|
||||||
|
description: "Remove a bad actor from the list",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
type: 6, // USER type
|
||||||
|
description: "The user to remove",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "confirmation",
|
||||||
|
type: Discord.ApplicationCommandOptionType.String,
|
||||||
|
description: "Type 'yes im absolutely sure' to confirm removal",
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const globalCommands = [
|
||||||
|
{
|
||||||
|
name: "lookup",
|
||||||
|
description: "Look up a bad actor by their User ID",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "user",
|
||||||
|
type: 6, // USER type
|
||||||
|
description: "The user to look up",
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "serverconfig",
|
||||||
|
description: "Configure server settings for bad actor management",
|
||||||
|
type: 1, // CHAT_INPUT type
|
||||||
|
default_member_permissions: Discord.PermissionFlagsBits.Administrator.toString(),
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "mode",
|
||||||
|
description: "Set the mode for bad actor management in this server",
|
||||||
|
type: 1, // SUB_COMMAND type
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "mode",
|
||||||
|
type: 3, // STRING type
|
||||||
|
description: "Choose 'strict' or 'lenient' mode",
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
name: "Auto-Kick",
|
||||||
|
value: "kick"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Auto-Ban",
|
||||||
|
value: "ban"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Warn Only",
|
||||||
|
value: "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No Action",
|
||||||
|
value: "none"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "logchannel",
|
||||||
|
description: "Set the channel for logging bad actor actions",
|
||||||
|
type: 1, // SUB_COMMAND type
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "channel",
|
||||||
|
type: 7, // CHANNEL type
|
||||||
|
description: "The channel to log actions in",
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "view",
|
||||||
|
description: "View the current server configuration for bad actor management",
|
||||||
|
type: 1 // SUB_COMMAND type
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fullscan",
|
||||||
|
description: "Perform a full scan of the server for bad actors",
|
||||||
|
type: 1 // SUB_COMMAND type
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
// Define commands
|
||||||
|
module.exports = {
|
||||||
|
adminGuildCommands: adminGuildCommands,
|
||||||
|
globalCommands: globalCommands
|
||||||
|
}
|
77
eventHandlers/guildMemberAdd.js
Normal file
77
eventHandlers/guildMemberAdd.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
module.exports = (member, db, client) => {
|
||||||
|
if (!member.guild) return; // Ignore DMs
|
||||||
|
if (member.user.bot) return; // Ignore bots
|
||||||
|
|
||||||
|
db.get("SELECT * FROM guildConfigs WHERE guildId = ?", [member.guild.id], (err, config) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!config) {
|
||||||
|
console.warn(`No configuration found for guild ${member.guild.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.get("SELECT * FROM badActors WHERE id = ?", [member.id], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
switch (config.mode) {
|
||||||
|
case "none":
|
||||||
|
return; // Literally do nothing lol
|
||||||
|
break;
|
||||||
|
case "warn": // Send a warning message in the logChannel
|
||||||
|
break; // Just break, we send warning for everything else anyways
|
||||||
|
case "kick": // Kick the user
|
||||||
|
member.kick("Listed bad actor").then(() => {
|
||||||
|
client.channels.fetch(config.logChannelId).then(logChan => {
|
||||||
|
logChan.send({
|
||||||
|
content: `User <@${member.id}> has been kicked for being a bad actor. Reported by <@${row.reportedBy}>.`
|
||||||
|
}).catch(console.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
).catch(err => {
|
||||||
|
console.error("Failed to kick user:", err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "ban": // Ban the user
|
||||||
|
member.ban({ reason: "Listed bad actor" }).then(() => {
|
||||||
|
client.channels.fetch(config.logChannelId).then(logChan => {
|
||||||
|
logChan.send({
|
||||||
|
content: `User <@${member.id}> has been banned for being a bad actor. Reported by <@${row.reportedBy}>.`
|
||||||
|
}).catch(console.error);
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Failed to ban user:", err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
client.channels.fetch(config.logChannelId).then(channel => {
|
||||||
|
if (!channel) {
|
||||||
|
console.warn(`Log channel ${config.logChannelId} not found in guild ${member.guild.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channel.send({
|
||||||
|
embeds: [{
|
||||||
|
title: "Bad Actor Detected",
|
||||||
|
description: `User <@${member.id}> has joined the server and is listed as a bad actor.`,
|
||||||
|
fields: [
|
||||||
|
{ name: "Reported By", value: `<@${row.reportedBy}>`, inline: true },
|
||||||
|
{ name: "Comment", value: row.comment || "No comment provided", inline: false },
|
||||||
|
{ name: "Timestamp", value: `<t:${Math.floor(row.timestamp / 1000)}>`, inline: true }
|
||||||
|
],
|
||||||
|
color: 0xff0000
|
||||||
|
}],
|
||||||
|
files: row.attachments ? JSON.parse(row.attachments).map(file => ({
|
||||||
|
attachment: path.join(__dirname, '../../storage', file),
|
||||||
|
name: file
|
||||||
|
})) : []
|
||||||
|
}).catch(console.error);
|
||||||
|
}).catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
159
index.js
Normal file
159
index.js
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
require("dotenv").config();
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const db = new sqlite3.Database(process.env.DB_PATH || 'database.db');
|
||||||
|
|
||||||
|
require('./migrations')(db).then(() => {
|
||||||
|
console.log('All migrations completed successfully');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error running migrations:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const client = new Discord.Client({
|
||||||
|
intents: [
|
||||||
|
Discord.IntentsBitField.Flags.Guilds,
|
||||||
|
Discord.IntentsBitField.Flags.GuildModeration,
|
||||||
|
Discord.IntentsBitField.Flags.GuildMembers
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Presence Loop
|
||||||
|
var presenceCounter = 0;
|
||||||
|
var presenceInterval = 60; // Default to 60 seconds, can be adjusted in presence.json
|
||||||
|
const presenceLoop = () => {
|
||||||
|
console.log("Doing presence update...");
|
||||||
|
try {
|
||||||
|
const presenceConfig = JSON.parse(fs.readFileSync('presence.json', 'utf8'));
|
||||||
|
if (!presenceConfig.presenceList || !Array.isArray(presenceConfig.presenceList) || presenceConfig.presenceList.length === 0) {
|
||||||
|
console.warn('Presence list is empty or not defined in presence.json');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!presenceConfig.presenceList[presenceCounter]) {
|
||||||
|
presenceCounter = 0; // Reset counter if out of bounds
|
||||||
|
}
|
||||||
|
presenceInterval = presenceConfig.interval || 60; // Use interval from config or default to 60 seconds
|
||||||
|
console.log(`Setting presence to: ${presenceConfig.presenceList[presenceCounter]}; Counter: ${presenceCounter}; Waiting ${presenceInterval} seconds for next update...`);
|
||||||
|
client.user.setPresence({
|
||||||
|
activities: [
|
||||||
|
{
|
||||||
|
name: presenceConfig.presenceList[presenceCounter],
|
||||||
|
type: Discord.ActivityType.Custom
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (presenceConfig.random) {
|
||||||
|
console.log("Random presence enabled, selecting a random presence for next update.");
|
||||||
|
presenceCounter = Math.floor(Math.random() * presenceConfig.presenceList.length);
|
||||||
|
} else {
|
||||||
|
presenceCounter++;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load presence config:', err);
|
||||||
|
}
|
||||||
|
setTimeout(presenceLoop, presenceInterval * 1000); // Wait for the specified interval before next update
|
||||||
|
}
|
||||||
|
|
||||||
|
client.once("ready", async () => {
|
||||||
|
console.log(`Logged in as ${client.user.username} (${client.user.id})`);
|
||||||
|
|
||||||
|
// Register commands
|
||||||
|
const commands = require("./commands.js")
|
||||||
|
await (async () => {
|
||||||
|
const rest = new Discord.REST({ version: '10' }).setToken(client.token);
|
||||||
|
try {
|
||||||
|
//Global
|
||||||
|
console.log(`Registering global commands`);
|
||||||
|
await rest.put(Discord.Routes.applicationCommands(client.user.id), { body: commands.globalCommands })
|
||||||
|
console.log(`Registered global commands successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.REFRESH_ADMIN_GUILD_COMMANDS === 'true') {
|
||||||
|
// Unregister guild commands from all guilds
|
||||||
|
try {
|
||||||
|
console.log(`Unregistering guild commands from all guilds`);
|
||||||
|
const guilds = await client.guilds.fetch();
|
||||||
|
console.log(`Found ${guilds.size} guilds to unregister commands from`);
|
||||||
|
if (guilds.size === 0) {
|
||||||
|
console.log(`No guilds found, skipping unregistration`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const guild of guilds.values()) {
|
||||||
|
await rest.put(Discord.Routes.applicationGuildCommands(client.user.id, guild.id), { body: [] });
|
||||||
|
}
|
||||||
|
console.log(`Unregistered guild commands from all guilds successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
//Guild
|
||||||
|
console.log(`Registering guild commands for admin guild`);
|
||||||
|
await rest.put(Discord.Routes.applicationGuildCommands(client.user.id, process.env.ADMIN_GUILD), { body: commands.adminGuildCommands })
|
||||||
|
console.log(`Registered guild commands for admin guild successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
presenceLoop();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", (error) => {
|
||||||
|
console.error("An error occurred:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("interactionCreate", async (interaction) => {
|
||||||
|
switch (interaction.type) {
|
||||||
|
case Discord.InteractionType.ApplicationCommand:
|
||||||
|
const command = require(`./interactions/commands/${interaction.commandName}.js`);
|
||||||
|
if (command) {
|
||||||
|
try {
|
||||||
|
await command.execute(interaction, db, client);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error executing command ${interaction.commandName}:`, error);
|
||||||
|
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Command ${interaction.commandName} not found.`);
|
||||||
|
await interaction.reply({ content: 'This command does not exist.', ephemeral: true });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Discord.InteractionType.ModalSubmit:
|
||||||
|
const modal = require(`./interactions/modals/${interaction.customId}.js`)(interaction, db, client);
|
||||||
|
if (modal) {
|
||||||
|
try {
|
||||||
|
await modal.execute();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error executing modal ${interaction.customId}:`, error);
|
||||||
|
await interaction.reply({ content: 'There was an error while processing this modal!', ephemeral: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Modal ${interaction.customId} not found.`);
|
||||||
|
await interaction.reply({ content: 'This modal does not exist.', ephemeral: true });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Unhandled interaction type: ${interaction.type}`);
|
||||||
|
await interaction.reply({ content: 'This interaction type is not supported.', ephemeral: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("guildMemberAdd", async (member) => {
|
||||||
|
require("./eventHandlers/guildMemberAdd.js")(member, db, client);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login(process.env.TOKEN).then(() => {
|
||||||
|
console.log("Bot is online!");
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Failed to log in:", err);
|
||||||
|
});
|
87
interactions/commands/add.js
Normal file
87
interactions/commands/add.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const execute = async (interaction, db, client) => {
|
||||||
|
if (!interaction.isCommand()) return;
|
||||||
|
await interaction.deferReply({ ephemeral: true }).catch(console.error);
|
||||||
|
|
||||||
|
db.get("SELECT * FROM badActors WHERE id = ?", [interaction.options.getUser("user")], async (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return interaction.editReply({ content: "An error occurred while accessing the database.", ephemeral: true });
|
||||||
|
}
|
||||||
|
if (row) {
|
||||||
|
return interaction.editReply({ content: "That user is already listed!", ephemeral: true });
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const user = interaction.options.getUser("user").id;
|
||||||
|
const comment = interaction.options.getString("comment") || "No comment provided";
|
||||||
|
const attachments = [];
|
||||||
|
for (let i = 1; i <= 4; i++) {
|
||||||
|
const attachment = interaction.options.getAttachment(`attachment${i}`);
|
||||||
|
if (attachment) {
|
||||||
|
const url = attachment.url;
|
||||||
|
const filename = `${Date.now()}_${attachment.name}`;
|
||||||
|
const filepath = path.join(__dirname, '../../storage', filename);
|
||||||
|
|
||||||
|
// Await the download before continuing
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const file = fs.createWriteStream(filepath);
|
||||||
|
https.get(url, (response) => {
|
||||||
|
response.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close(resolve);
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
fs.unlink(filepath, () => { });
|
||||||
|
console.error('Error downloading attachment:', err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
attachments.push(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
db.run("INSERT INTO badActors (id, reportedBy, comment, timestamp, attachments) VALUES (?, ?, ?, ?, ?)", [
|
||||||
|
user,
|
||||||
|
interaction.user.id,
|
||||||
|
comment,
|
||||||
|
timestamp,
|
||||||
|
JSON.stringify(attachments)
|
||||||
|
], function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return interaction.editReply({ content: "An error occurred while adding the user to the list.", ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = {
|
||||||
|
title: "Bad Actor Added",
|
||||||
|
description: `User <@${user}> has been added to the bad actors list.`,
|
||||||
|
fields: [
|
||||||
|
{ name: "Reported By", value: `<@${interaction.user.id}>`, inline: true },
|
||||||
|
{ name: "Comment", value: comment, inline: false },
|
||||||
|
{ name: "Timestamp", value: new Date(timestamp).toLocaleString(), inline: true }
|
||||||
|
],
|
||||||
|
|
||||||
|
color: 0xff0000
|
||||||
|
};
|
||||||
|
|
||||||
|
interaction.editReply({ embeds: [embed], files: attachments.map(file => path.join(__dirname, '../../storage', file))});
|
||||||
|
client.channels.fetch(process.env.ADMIN_LOG_CHANNEL).then(logChan => {
|
||||||
|
logChan.send({
|
||||||
|
embeds: [embed],
|
||||||
|
content: `User <@${user}> has been added to the bad actors list by <@${interaction.user.id}>.`,
|
||||||
|
files: attachments.map(file => ({
|
||||||
|
attachment: path.join(__dirname, '../../storage', file),
|
||||||
|
name: file
|
||||||
|
}))
|
||||||
|
}).catch(console.error);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = { execute };
|
36
interactions/commands/lookup.js
Normal file
36
interactions/commands/lookup.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const execute = async (interaction, db, client) => {
|
||||||
|
if (!interaction.isCommand()) return;
|
||||||
|
await interaction.deferReply({ ephemeral: true }).catch(console.error);
|
||||||
|
interaction.options.getUser("user").id
|
||||||
|
db.get("SELECT * FROM badActors WHERE id = ?", [interaction.options.getUser("user").id], async (err, row) => {
|
||||||
|
if (row) {
|
||||||
|
const embed = {
|
||||||
|
title: "Bad Actor Found",
|
||||||
|
description: `User <@${row.id}> is in the bad actors list.`,
|
||||||
|
fields: [
|
||||||
|
{ name: "Reported By", value: `<@${row.reportedBy}>`, inline: true },
|
||||||
|
{ name: "Comment", value: row.comment || "No comment provided", inline: false },
|
||||||
|
{ name: "Timestamp", value: `<t:${Math.floor(new Date(row.timestamp)/1000)}>`, inline: true }
|
||||||
|
],
|
||||||
|
color: 0x00ff00
|
||||||
|
};
|
||||||
|
|
||||||
|
let files = [];
|
||||||
|
if (row.attachments) {
|
||||||
|
try {
|
||||||
|
const attachments = JSON.parse(row.attachments);
|
||||||
|
files = attachments.map(file => path.join(__dirname, '../../storage', file));
|
||||||
|
} catch (e) {
|
||||||
|
files = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interaction.editReply({ embeds: [embed], files });
|
||||||
|
} else {
|
||||||
|
interaction.editReply({ content: "That user is not listed as a bad actor.", ephemeral: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = { execute };
|
48
interactions/commands/remove.js
Normal file
48
interactions/commands/remove.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const execute = async (interaction, db, client) => {
|
||||||
|
if (!interaction.isCommand()) return;
|
||||||
|
await interaction.deferReply({ ephemeral: true }).catch(console.error);
|
||||||
|
|
||||||
|
db.get("SELECT * FROM badActors WHERE id = ?", [interaction.options.getUser("user").id], async (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return interaction.editReply({ content: "An error occurred while accessing the database.", ephemeral: true });
|
||||||
|
}
|
||||||
|
if (row) {
|
||||||
|
const user = interaction.options.getUser("user").id;
|
||||||
|
const confirmation = interaction.options.getString("confirmation");
|
||||||
|
|
||||||
|
if (confirmation !== "yes im absolutely sure") {
|
||||||
|
return interaction.editReply({ content: "You must type 'yes im absolutely sure' to confirm removal.", ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
db.run("DELETE FROM badActors WHERE id = ?", [user], function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return interaction.editReply({ content: "An error occurred while removing the user from the list.", ephemeral: true });
|
||||||
|
}
|
||||||
|
row.attachments = JSON.parse(row.attachments || "[]");
|
||||||
|
if (row.attachments && Array.isArray(row.attachments)) {
|
||||||
|
for (const filePath of row.attachments) {
|
||||||
|
fs.unlink(path.join(__dirname, "../../storage", filePath), (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`Failed to delete attachment: ${filePath}`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interaction.editReply({ content: `User <@${user}> has been removed from the bad actors list.`, ephemeral: true });
|
||||||
|
client.channels.fetch(process.env.ADMIN_LOG_CHANNEL).then(logChan => {
|
||||||
|
logChan.send({
|
||||||
|
content: `User <@${user}> has been removed from the bad actors list by <@${interaction.user.id}>.`
|
||||||
|
}).catch(console.error);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return interaction.editReply({ content: "That user is not listed as a bad actor.", ephemeral: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = { execute };
|
112
interactions/commands/serverconfig.js
Normal file
112
interactions/commands/serverconfig.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
const allowedChannelTypes = [Discord.ChannelType.GuildAnnouncement, Discord.ChannelType.GuildText, Discord.ChannelType.GuildVoice]
|
||||||
|
|
||||||
|
const execute = async (interaction, db, client) => {
|
||||||
|
if (!interaction.isCommand()) return;
|
||||||
|
if (!interaction.guild) return interaction.reply({ content: "This command can only be used in a server.", ephemeral: true });
|
||||||
|
await interaction.deferReply({ ephemeral: true }).catch(console.error);
|
||||||
|
var conf;
|
||||||
|
await db.get("SELECT * FROM guildConfigs WHERE guildId = ?", [interaction.guild.id], async (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return interaction.editReply({ content: "An error occurred while accessing the database.", ephemeral: true });
|
||||||
|
}
|
||||||
|
if (!row) {
|
||||||
|
db.run("INSERT INTO guildConfigs (guildId, mode) VALUES (?, ?)", [interaction.guild.id, "none"], function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
returninteraction.editReply({ content: "An error occurred while initializing server configuration.", ephemeral: true });
|
||||||
|
}
|
||||||
|
conf = { guildId: interaction.guild.id, mode: "none" };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
conf = row;
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
console.log(`Subcommand: ${subcommand}`);
|
||||||
|
switch (subcommand) {
|
||||||
|
case "mode": // Activate or deactivate automatic bad-actor management
|
||||||
|
const newMode = interaction.options.getString("mode");
|
||||||
|
if (!["none", "warn", "kick", "ban"].includes(newMode)) {
|
||||||
|
return interaction.editReply({ content: "Invalid mode selected. Choose from: none, warn, kick, ban.", ephemeral: true });
|
||||||
|
}
|
||||||
|
db.run("UPDATE guildConfigs SET mode = ? WHERE guildId = ?", [newMode, interaction.guild.id], function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return interaction.editReply({ content: "An error occurred while updating the mode.", ephemeral: true });
|
||||||
|
}
|
||||||
|
conf.mode = newMode;
|
||||||
|
return interaction.editReply({ content: `Bad actor management mode set to **${newMode}**.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "logchannel": // Set the log channel for bad actor actions
|
||||||
|
const logChannel = interaction.options.getChannel("channel");
|
||||||
|
|
||||||
|
if (!logChannel || !allowedChannelTypes.includes(logChannel.type)) {
|
||||||
|
return interaction.editReply({ content: "Please select a valid text channel for logging.", ephemeral: true });
|
||||||
|
}
|
||||||
|
db.run("UPDATE guildConfigs SET logChannelId = ? WHERE guildId = ?", [logChannel.id, interaction.guild.id], function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return interaction.editReply({ content: "An error occurred while updating the log channel.", ephemeral: true });
|
||||||
|
}
|
||||||
|
conf.logChannelId = logChannel.id;
|
||||||
|
return interaction.editReply({ content: `Log channel set to <#${logChannel.id}>.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "view": // View current server configuration
|
||||||
|
if (!conf) {
|
||||||
|
return interaction.editReply({ content: "No configuration found for this server.", ephemeral: true });
|
||||||
|
}
|
||||||
|
const mode = conf.mode || "none";
|
||||||
|
const logChannelId = conf.logChannelId ? `<#${conf.logChannelId}>` : "Not set";
|
||||||
|
const embed = new Discord.EmbedBuilder()
|
||||||
|
.setTitle("Server Configuration")
|
||||||
|
.addFields(
|
||||||
|
{ name: "Bad Actor Management Mode", value: mode, inline: true },
|
||||||
|
{ name: "Log Channel", value: logChannelId, inline: true }
|
||||||
|
)
|
||||||
|
.setColor("#0099ff")
|
||||||
|
.setTimestamp();
|
||||||
|
return interaction.editReply({ embeds: [embed], ephemeral: true });
|
||||||
|
break;
|
||||||
|
case "fullscan": // Perform a full scan of the server for bad actors
|
||||||
|
if (!conf.logChannelId) {
|
||||||
|
return interaction.editReply({ content: "Log channel is not set. Please set it using `/serverconfig logchannel`.", ephemeral: true });
|
||||||
|
}
|
||||||
|
const logChannelFullScan = await client.channels.fetch(conf.logChannelId).catch(console.error);
|
||||||
|
if (!logChannelFullScan || !allowedChannelTypes.includes(logChannelFullScan.type)) {
|
||||||
|
return interaction.editReply({ content: "Log channel is not valid or not found.", ephemeral: true });
|
||||||
|
}
|
||||||
|
const members = await interaction.guild.members.fetch();
|
||||||
|
const badActors = [];
|
||||||
|
for (const member of members.values()) {
|
||||||
|
if (member.user.bot) continue; // Ignore bots
|
||||||
|
const actor = await new Promise((resolve, reject) => {
|
||||||
|
db.get("SELECT * FROM badActors WHERE id = ?", [member.user.id], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Database error:", err);
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
resolve(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (actor) {
|
||||||
|
badActors.push(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (badActors.length === 0) {
|
||||||
|
return interaction.editReply({ content: "No bad actors found in the server.", ephemeral: true });
|
||||||
|
}
|
||||||
|
let message = `Bad Actor Scan Results\nFound ${badActors.length} bad actors in the server.\n\n`;
|
||||||
|
badActors.forEach(actor => {
|
||||||
|
message += `ID: ${actor.id}\nUser: <@${actor.id}>\nReported by: <@${actor.reportedBy}>\nDate: <t:${Math.floor(actor.timestamp / 1000)}:F>\nComment: ${actor.comment}\n\n`;
|
||||||
|
});
|
||||||
|
logChannelFullScan.send({ content: message }).catch(console.error);
|
||||||
|
return interaction.editReply({ content: "Full scan completed. Check the log channel for results.", ephemeral: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
module.exports = { execute };
|
69
migrations.js
Normal file
69
migrations.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = async function runMigrations(db) {
|
||||||
|
// Promisify db.run and db.get for easier async/await usage
|
||||||
|
function run(sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.run(sql, params, function (err) {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function get(sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.get(sql, params, function (err, row) {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function all(sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(sql, params, function (err, rows) {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Ensure migrations table exists
|
||||||
|
await run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
run_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 2. Read all migration files
|
||||||
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
|
let files = [];
|
||||||
|
try {
|
||||||
|
files = fs.readdirSync(migrationsDir)
|
||||||
|
.filter(f => f.endsWith('.js'))
|
||||||
|
.sort();
|
||||||
|
} catch (e) {
|
||||||
|
// If directory doesn't exist, skip
|
||||||
|
if (e.code !== 'ENOENT') throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Get already run migrations
|
||||||
|
const appliedRows = await all('SELECT name FROM migrations');
|
||||||
|
const applied = new Set(appliedRows.map(row => row.name));
|
||||||
|
|
||||||
|
// 4. Run pending migrations
|
||||||
|
for (const file of files) {
|
||||||
|
if (!applied.has(file)) {
|
||||||
|
const migration = require(path.join(migrationsDir, file));
|
||||||
|
if (typeof migration !== 'function') {
|
||||||
|
throw new Error(`Migration file ${file} does not export a function`);
|
||||||
|
}
|
||||||
|
console.log(`Running migration: ${file}`);
|
||||||
|
await migration(db, run, get, all);
|
||||||
|
// Mark migration as applied
|
||||||
|
await run('INSERT INTO migrations (name) VALUES (?)', [file]);
|
||||||
|
console.log(`Migration applied: ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
7
migrations/0001-create-tables.js
Normal file
7
migrations/0001-create-tables.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = async function createTables(db, run, get, all) {
|
||||||
|
// Create badActors table
|
||||||
|
run("CREATE TABLE IF NOT EXISTS badActors (id TEXT PRIMARY KEY, reportedBy TEXT, comment TEXT, timestamp INTEGER, attachments BLOB)");
|
||||||
|
|
||||||
|
// Create guildConfigs table
|
||||||
|
run("CREATE TABLE IF NOT EXISTS guildConfigs (guildId TEXT PRIMARY KEY, mode TEXT DEFAULT 'warn', logChannelId TEXT)")
|
||||||
|
}
|
12
migrations/0002-import-old-data.js
Normal file
12
migrations/0002-import-old-data.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = async function importOldData(db, run, get, all) {
|
||||||
|
oldData = require("../snowflakes.json");
|
||||||
|
for (const key in oldData) {
|
||||||
|
let actor = oldData[key][0];
|
||||||
|
run("INSERT INTO badActors (id, reportedBy, comment, timestamp) VALUES (?, ?, ?, ?)", [
|
||||||
|
key,
|
||||||
|
actor.author.id,
|
||||||
|
actor.orig_content,
|
||||||
|
new Date(actor.created_at).getTime()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
1782
package-lock.json
generated
Normal file
1782
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
package.json
Normal file
18
package.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "satanic-security",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"discord.js": "^14.20.0",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
|
}
|
||||||
|
}
|
13
presence.json
Normal file
13
presence.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"interval": 60,
|
||||||
|
"random": true,
|
||||||
|
"presenceList": [
|
||||||
|
"Praise yourself!",
|
||||||
|
"Happy pride month 🌈",
|
||||||
|
"I'll protect you",
|
||||||
|
"You are loved",
|
||||||
|
"You are not alone",
|
||||||
|
"You are enough",
|
||||||
|
"You are valid"
|
||||||
|
]
|
||||||
|
}
|
9
util/embedGen.js
Normal file
9
util/embedGen.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
listBadActor: (row) => {
|
||||||
|
return {
|
||||||
|
embeds: [{
|
||||||
|
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue