Gwug
This commit is contained in:
commit
a52e80c62e
142
.gitignore
vendored
Normal file
142
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
# 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
|
||||||
|
.output
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Sveltekit cache directory
|
||||||
|
.svelte-kit/
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
|
||||||
|
# Firebase cache directory
|
||||||
|
.firebase/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v3
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Vite files
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
.vite/
|
||||||
|
bansync.db
|
||||||
29
banHandler.js
Normal file
29
banHandler.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
const colors = require('colors');
|
||||||
|
|
||||||
|
module.exports = async (ban, db, client) => {
|
||||||
|
// Fetch perms this guild has
|
||||||
|
const guild = await client.guilds.fetch(ban.guild.id);
|
||||||
|
|
||||||
|
db.all("SELECT list_id, auto_add FROM perms WHERE entity_id = ? AND entity_type = 'guild'", [ban.guild.id], async (err, lists) => {
|
||||||
|
;
|
||||||
|
for (const entry of lists) {
|
||||||
|
const list = await db.get("SELECT * FROM lists WHERE id = ?", [entry.list_id]);
|
||||||
|
if (!list) {
|
||||||
|
console.log(`${colors.red('[ERROR]')} List not found for id ${entry.list_id}. How'd we get here?`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.auto_add) {
|
||||||
|
try {
|
||||||
|
const existingBan = await db.get("SELECT * FROM bans WHERE list_id = ? AND user_id = ?", [list.id, ban.user.id]);
|
||||||
|
if (existingBan) {
|
||||||
|
// Already banned on this list
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await db.run("INSERT INTO bans (list_id, user_id, reason, banned_by) VALUES (?, ?, ?, ?)", [list.id, ban.user.id, `${ban.reason || 'No reason provided'} | Sync from ${guild.id}`, `${ban.guild.id} (Auto-add)`]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ERROR] Could not auto-add ban for user ${ban.user.id} in guild ${guild.id}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
239
commands.js
Normal file
239
commands.js
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
const Discord = require('discord.js');
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
"name": "list",
|
||||||
|
"description": "Manage ban lists.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "view",
|
||||||
|
"description": "View your ban lists."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "create",
|
||||||
|
"description": "Create a new ban list.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the ban list.",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "delete",
|
||||||
|
"description": "Delete an existing ban list.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list",
|
||||||
|
"description": "The ban list to delete.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "transfer",
|
||||||
|
"description": "Transfer ownership of a ban list.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list",
|
||||||
|
"description": "The ban list to transfer.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.User,
|
||||||
|
"name": "to",
|
||||||
|
"description": "The user to transfer ownership to.",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "import",
|
||||||
|
"description": "Import bans from the current server to a ban list.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list",
|
||||||
|
"description": "The ban list to import bans into.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "allowguild",
|
||||||
|
"description": "Allow a guild to use your ban list.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list",
|
||||||
|
"description": "The ban list to allow the guild to use.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "guild_id",
|
||||||
|
"description": "The ID of the guild to allow. (Default: Current Guild)",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "revokeguild",
|
||||||
|
"description": "Revoke a guild's access to your ban list.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list",
|
||||||
|
"description": "The ban list to revoke the guild's access from.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "guild_id",
|
||||||
|
"description": "The ID of the guild to revoke. (Default: Current Guild)",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ban",
|
||||||
|
"description": "Add a user to the ban list.",
|
||||||
|
"default_member_permissions": Discord.PermissionFlagsBits.BanMembers.toString(),
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.User,
|
||||||
|
"name": "user",
|
||||||
|
"description": "The user to ban.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list",
|
||||||
|
"description": "The ban list to add the user to.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "reason",
|
||||||
|
"description": "The reason for the ban.",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unban",
|
||||||
|
"description": "Remove a user from the ban list.",
|
||||||
|
"default_member_permissions": Discord.PermissionFlagsBits.BanMembers.toString(),
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.User,
|
||||||
|
"name": "user",
|
||||||
|
"description": "The user to unban.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list",
|
||||||
|
"description": "The ban list to remove the user from.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "guild",
|
||||||
|
"description": "Manage your guild's ban list settings.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "join",
|
||||||
|
"description": "Join a ban list.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list_id",
|
||||||
|
"description": "The ID of the ban list to join.",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "leave",
|
||||||
|
"description": "Leave a ban list.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list_id",
|
||||||
|
"description": "The ID of the ban list to leave.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "autoadd",
|
||||||
|
"description": "Toggle automatic adding of bans from the list to this guild.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Boolean,
|
||||||
|
"name": "enabled",
|
||||||
|
"description": "Enable or disable auto-adding bans.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list_id",
|
||||||
|
"description": "The ID of the ban list to enable/disable auto-adding from.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "autoremove",
|
||||||
|
"description": "Toggle automatic removal of bans from the list to this guild.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Boolean,
|
||||||
|
"name": "enabled",
|
||||||
|
"description": "Enable or disable auto-removal of bans.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.String,
|
||||||
|
"name": "list_id",
|
||||||
|
"description": "The ID of the ban list to enable/disable auto-removal from.",
|
||||||
|
"required": true,
|
||||||
|
"autocomplete": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": Discord.ApplicationCommandOptionType.Subcommand,
|
||||||
|
"name": "sync",
|
||||||
|
"description": "Manually sync bans from all joined lists to this guild."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
118
index.js
Normal file
118
index.js
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
require("dotenv").config({ quiet: true });
|
||||||
|
const colors = require('colors');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const dbPath = path.join(__dirname, process.env.DB_PATH || 'bansync.db');
|
||||||
|
const cron = require('node-cron');
|
||||||
|
const { Client, REST, DiscordAPIError, InteractionType, Routes } = require('discord.js');
|
||||||
|
// const s3 = require("./s3"); // This *was* for S3 backups and file uploads for images and whatnot. But i figured it was overkill and cut it. s3.js still exists because it's a good base.
|
||||||
|
|
||||||
|
// Initialize SQLite database
|
||||||
|
const db = new sqlite3.Database(dbPath, (sqlErr) => {
|
||||||
|
if (sqlErr) {
|
||||||
|
console.error(`${colors.red('[ERROR]')} Failed to connect to the database:`, sqlErr);
|
||||||
|
process.exit(1); // Can't proceed without DB connection
|
||||||
|
} else {
|
||||||
|
console.log(`${colors.green('[INFO]')} Connected to the SQLite database at ${dbPath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
require('./migrations')(db).then(async () => {
|
||||||
|
// if (process.env.S3_DB_BACKUP == true) {
|
||||||
|
// s3.uploadFile('db_backup.db', await fs.readFile(dbPath)).then(() => {
|
||||||
|
// console.log(`${colors.cyan('[INFO]')} Initial DB backup uploaded to S3.`);
|
||||||
|
// let schedule = process.env.S3_DB_BACKUP_SCHEDULE || '0 0 * * *'; // Default to every day
|
||||||
|
// if (!cron.validate(schedule)) {
|
||||||
|
// console.log(`${colors.red('[ERROR]')} Invalid cron schedule for S3_DB_BACKUP_SCHEDULE. Using default '0 0 * * *'.`);
|
||||||
|
// schedule = '0 0 * * *';
|
||||||
|
// }
|
||||||
|
// cron.schedule(schedule, async () => {
|
||||||
|
// try {
|
||||||
|
// const data = await fs.readFile(dbPath);
|
||||||
|
// await s3.uploadFile('db_backup.db', data);
|
||||||
|
// console.log(`${colors.cyan('[INFO]')} Backed up DB to S3.`);
|
||||||
|
// } catch (err) {
|
||||||
|
// console.log(`${colors.red('[ERROR]')} Failed to back up DB to S3:`, err);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// console.log(`${colors.cyan('[INFO]')} Scheduled DB backups to S3.`);
|
||||||
|
// });
|
||||||
|
// } // Cut feature. Overkill lol
|
||||||
|
discord_setup();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const discord_setup = () => {
|
||||||
|
const client = new Client({
|
||||||
|
intents: ["GuildBans", "Guilds", "GuildModeration"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const banHandler = require('./banHandler');
|
||||||
|
const unbanHandler = require('./unbanHandler');
|
||||||
|
|
||||||
|
client.on('guildBanRemove', (ban) => {
|
||||||
|
unbanHandler(ban, db, client);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('guildBanAdd', (ban) => {
|
||||||
|
banHandler(ban, db, client);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('interactionCreate', async interaction => {
|
||||||
|
try {
|
||||||
|
switch (interaction.type) {
|
||||||
|
case InteractionType.ApplicationCommand:
|
||||||
|
require(path.join(__dirname, 'interactions', 'commands', interaction.commandName))(interaction, db, client);
|
||||||
|
break;
|
||||||
|
case InteractionType.MessageComponent:
|
||||||
|
require(path.join(__dirname, 'interactions', 'components', interaction.customId))(interaction, db, client);
|
||||||
|
break;
|
||||||
|
case InteractionType.ApplicationCommandAutocomplete:
|
||||||
|
require(path.join(__dirname, 'interactions', 'autocomplete', interaction.commandName))(interaction, db, client);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`${colors.yellow('[WARN]')} Unknown interaction type: ${interaction.type}`);
|
||||||
|
interaction.reply({ content: 'Unknown interaction type.', ephemeral: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${colors.red('[ERROR]')} Error handling interaction:`, error);
|
||||||
|
interaction.reply({ content: 'An error occurred while processing your request.', ephemeral: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.once('clientReady', () => {
|
||||||
|
console.log(`${colors.green('[DISCORD]')} Bot is online as ${client.user.displayName}`);
|
||||||
|
|
||||||
|
// Init application commands (global)
|
||||||
|
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
console.log(`${colors.cyan('[DISCORD]')} Refreshing application (/) commands.`);
|
||||||
|
await rest.put(Routes.applicationCommands(client.user.id), {
|
||||||
|
body: require('./commands')
|
||||||
|
});
|
||||||
|
console.log(`${colors.green('[DISCORD]')} Successfully reloaded application (/) commands.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${colors.red('[DISCORD]')} Error reloading application (/) commands:`, error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Sync bans on startup
|
||||||
|
const sync = require('./sync');
|
||||||
|
sync(client, db);
|
||||||
|
// Sync every 10 minutes
|
||||||
|
cron.schedule(process.env.SYNC_SCHEDULE, () => {
|
||||||
|
sync(client, db);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login(process.env.DISCORD_TOKEN).catch(err => {
|
||||||
|
console.log(`${colors.red('[DISCORD]')} Failed to log in:`, err);
|
||||||
|
process.exit(1); // Can't proceed without Discord connection
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
26
interactions/autocomplete/ban.js
Normal file
26
interactions/autocomplete/ban.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Autocomplete for ban.js list option
|
||||||
|
const colors = require('colors');
|
||||||
|
|
||||||
|
module.exports = async (interaction, db, client) => {
|
||||||
|
if (interaction.commandName !== 'ban') return;
|
||||||
|
|
||||||
|
const focusedOption = interaction.options.getFocused(true);
|
||||||
|
if (focusedOption.name !== 'list') return;
|
||||||
|
|
||||||
|
const query = focusedOption.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.all(`SELECT id, name FROM lists WHERE id IN (${Array(listIds.length).fill('?').join(',')}) AND name LIKE ? LIMIT 25`, [...listIds, `%${query}%`], async (err, lists) => {
|
||||||
|
|
||||||
|
const choices = lists.map(list => ({
|
||||||
|
name: list.name,
|
||||||
|
value: list.id.toString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
await interaction.respond(choices);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${colors.red('[ERROR]')} Error handling autocomplete for ban command:`, error);
|
||||||
|
await interaction.respond([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
33
interactions/autocomplete/guild.js
Normal file
33
interactions/autocomplete/guild.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Autocomplete for lists on /guild subcommands
|
||||||
|
const colors = require('colors');
|
||||||
|
|
||||||
|
module.exports = async (interaction, db, client) => {
|
||||||
|
if (interaction.commandName !== 'guild') return;
|
||||||
|
|
||||||
|
const focusedOption = interaction.options.getFocused(true);
|
||||||
|
if (focusedOption.name !== 'list_id') return;
|
||||||
|
|
||||||
|
const query = focusedOption.value || "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch lists joined by the guild
|
||||||
|
db.all(`SELECT l.id, l.name FROM lists l
|
||||||
|
JOIN guilds gl ON l.id = gl.list_id
|
||||||
|
WHERE gl.guild_id = ? AND l.name LIKE ? LIMIT 25`,
|
||||||
|
[interaction.guild.id, `%${query}%`], async (err, lists) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`${colors.red('[ERROR]')} Error fetching lists for autocomplete:`, err);
|
||||||
|
return await interaction.respond([]);
|
||||||
|
}
|
||||||
|
const choices = lists.map(list => ({
|
||||||
|
name: list.name,
|
||||||
|
value: list.id.toString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
await interaction.respond(choices);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${colors.red('[ERROR]')} Error handling autocomplete for guild command:`, error);
|
||||||
|
await interaction.respond([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
27
interactions/autocomplete/list.js
Normal file
27
interactions/autocomplete/list.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Autocomplete handler for /list delete and /list transfer commands
|
||||||
|
const colors = require('colors');
|
||||||
|
|
||||||
|
module.exports = async (interaction, db, client) => {
|
||||||
|
if (interaction.commandName !== 'list') return;
|
||||||
|
|
||||||
|
const focusedOption = interaction.options.getFocused(true);
|
||||||
|
if (focusedOption.name !== 'list') return;
|
||||||
|
|
||||||
|
const query = focusedOption.value || "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch lists owned by the user
|
||||||
|
db.all(`SELECT id, name FROM lists WHERE owner_id = ? AND name LIKE ? LIMIT 25`, [interaction.user.id, `%${query}%`], async (err, lists) => {
|
||||||
|
|
||||||
|
const choices = lists.map(list => ({
|
||||||
|
name: list.name,
|
||||||
|
value: list.id.toString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
await interaction.respond(choices);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${colors.red('[ERROR]')} Error handling autocomplete for list command:`, error);
|
||||||
|
await interaction.respond([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
7
interactions/commands/ban.js
Normal file
7
interactions/commands/ban.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
const colors = require('colors');
|
||||||
|
|
||||||
|
module.exports = async (interaction, db) => {
|
||||||
|
if (interaction.commandName !== 'ban') return;
|
||||||
|
|
||||||
|
await interaction.reply({content: "Not Implimented Yet. (Try just banning people on your guild lol)", ephemeral: true});
|
||||||
|
}
|
||||||
143
interactions/commands/guild.js
Normal file
143
interactions/commands/guild.js
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
// Guild subcommands
|
||||||
|
const colors = require('colors');
|
||||||
|
|
||||||
|
module.exports = async (interaction, db, client) => {
|
||||||
|
if (interaction.commandName !== 'guild') return;
|
||||||
|
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case 'autoadd':
|
||||||
|
{
|
||||||
|
const listId = interaction.options.getString('list_id');
|
||||||
|
const enabled = interaction.options.getBoolean('enabled');
|
||||||
|
|
||||||
|
// Check if the guild is joined to the list
|
||||||
|
db.get(`SELECT * FROM guilds WHERE guild_id = ? AND list_id = ?`, [interaction.guild.id, listId], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while accessing the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return interaction.reply({ content: '❌ This guild is not joined to the specified ban list.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update auto_add setting
|
||||||
|
db.run(`UPDATE guilds SET auto_add = ? WHERE guild_id = ? AND list_id = ?`, [enabled ? 1 : 0, interaction.guild.id, listId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while updating the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
interaction.reply({ content: `✅ Auto-add from the ban list has been ${enabled ? 'enabled' : 'disabled'}.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'autoremove':
|
||||||
|
{
|
||||||
|
const listId = interaction.options.getString('list_id');
|
||||||
|
const enabled = interaction.options.getBoolean('enabled');
|
||||||
|
|
||||||
|
// Check if the guild is joined to the list
|
||||||
|
db.get(`SELECT * FROM guilds WHERE guild_id = ? AND list_id = ?`, [interaction.guild.id, listId], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while accessing the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return interaction.reply({ content: '❌ This guild is not joined to the specified ban list.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update auto_remove setting
|
||||||
|
db.run(`UPDATE guilds SET auto_remove = ? WHERE guild_id = ? AND list_id = ?`, [enabled ? 1 : 0, interaction.guild.id, listId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while updating the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
interaction.reply({ content: `✅ Auto-remove from the ban list has been ${enabled ? 'enabled' : 'disabled'}.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "join":
|
||||||
|
{
|
||||||
|
// then fucking impliment it
|
||||||
|
const listId = interaction.options.getString('list_id');
|
||||||
|
|
||||||
|
// Check if the list exists
|
||||||
|
db.get(`SELECT * FROM lists WHERE id = ?`, [listId], (err, list) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while accessing the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list) {
|
||||||
|
return interaction.reply({ content: '❌ The specified ban list does not exist.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the guild is already joined to the list
|
||||||
|
db.get(`SELECT * FROM guilds WHERE guild_id = ? AND list_id = ?`, [interaction.guild.id, listId], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while accessing the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row) {
|
||||||
|
return interaction.reply({ content: '❌ This guild is already joined to the specified ban list.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join the guild to the list
|
||||||
|
db.run(`INSERT INTO guilds (guild_id, list_id, auto_add, auto_remove) VALUES (?, ?, 0, 0)`, [interaction.guild.id, listId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while updating the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
interaction.reply({ content: `✅ This guild has successfully joined the ban list "${list.name}".`, ephemeral: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "leave":
|
||||||
|
{
|
||||||
|
const listId = interaction.options.getString('list_id');
|
||||||
|
|
||||||
|
// Check if the guild is joined to the list
|
||||||
|
db.get(`SELECT * FROM guilds WHERE guild_id = ? AND list_id = ?`, [interaction.guild.id, listId], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while accessing the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return interaction.reply({ content: '❌ This guild is not joined to the specified ban list.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave the guild from the list
|
||||||
|
db.run(`DELETE FROM guilds WHERE guild_id = ? AND list_id = ?`, [interaction.guild.id, listId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({ content: '❌ An error occurred while updating the database.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
interaction.reply({ content: `✅ This guild has successfully left the ban list.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sync':
|
||||||
|
{
|
||||||
|
// Manual sync logic to be implemented
|
||||||
|
await interaction.reply({ content: '🔄 Manual sync initiated. (Not yet implemented)', ephemeral: true });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await interaction.reply({ content: '❌ Unknown subcommand.', ephemeral: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
263
interactions/commands/list.js
Normal file
263
interactions/commands/list.js
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
// List management (create, delete, and transfer)
|
||||||
|
const colors = require("colors")
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
module.exports = async (interaction, db) => {
|
||||||
|
if (interaction.commandName !== 'list') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (interaction.options.getSubcommand()) {
|
||||||
|
case 'create':
|
||||||
|
{
|
||||||
|
|
||||||
|
const id = crypto.randomBytes(16).toString("hex");
|
||||||
|
const name = interaction.options.getString("name");
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
"INSERT INTO lists (id, name, owner_id) VALUES (?, ?, ?)",
|
||||||
|
[id, name, interaction.user.id],
|
||||||
|
async function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return interaction.reply({
|
||||||
|
content: "❌ Failed to create ban list.",
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
content: `✅ Ban list **${name}** created with ID \`${id}\`.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
{
|
||||||
|
const listId = interaction.options.getString('list');
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
db.get("SELECT * FROM lists WHERE id = ?", [listId], async (err, list) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Database error occurred.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!list) {
|
||||||
|
await interaction.reply({ content: `❌ Ban list with ID \`${listId}\` does not exist.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (list.owner_id !== interaction.user.id) {
|
||||||
|
await interaction.reply({ content: `❌ You do not own ban list **${list.name}**.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the list
|
||||||
|
db.run("DELETE FROM lists WHERE id = ?", [listId], async (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Failed to delete ban list.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await interaction.reply({ content: `✅ Ban list **${list.name}** has been deleted.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'transfer':
|
||||||
|
{
|
||||||
|
const listId = interaction.options.getString('list');
|
||||||
|
const newOwner = interaction.options.getUser('to');
|
||||||
|
|
||||||
|
if (!newOwner) {
|
||||||
|
await interaction.reply({ content: '❌ Target user not found.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
db.get("SELECT * FROM lists WHERE id = ?", [listId], async (err, list) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Database error occurred.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!list) {
|
||||||
|
await interaction.reply({ content: `❌ Ban list with ID \`${listId}\` does not exist.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (list.owner_id !== interaction.user.id) {
|
||||||
|
await interaction.reply({ content: `❌ You do not own ban list **${list.name}**.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer ownership
|
||||||
|
db.run("UPDATE lists SET owner_id = ? WHERE id = ?", [newOwner.id, listId], async (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Failed to transfer ownership.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await interaction.reply({ content: `✅ Ban list **${list.name}** ownership has been transferred to ${newOwner.tag}.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "import":
|
||||||
|
{
|
||||||
|
const importBans = (list) => {
|
||||||
|
// Fetch bans from the guild
|
||||||
|
interaction.guild.bans.fetch().then(bans => {
|
||||||
|
let importedCount = 0;
|
||||||
|
bans.forEach(async ban => {
|
||||||
|
console.log(`Importing ban for user ${ban.user.id} into list ${list.id}`);
|
||||||
|
await db.run("INSERT OR IGNORE INTO bans (user_id, list_id, reason, banned_by) VALUES (?, ?, ?, ?)", [ban.user.id, list.id, ban.reason || '', "System"], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
} else {
|
||||||
|
importedCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
interaction.reply({ content: `✅ Imported ${importedCount} bans from this server into ban list **${list.name}**.`, ephemeral: true });
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
interaction.reply({ content: '❌ Failed to fetch bans from this server.', ephemeral: true });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const listId = interaction.options.getString('list');
|
||||||
|
|
||||||
|
// Verify ownership or guild has access (either works)
|
||||||
|
db.get("SELECT * FROM lists WHERE id = ?", [listId], async (err, list) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Database error occurred.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!list) {
|
||||||
|
await interaction.reply({ content: `❌ Ban list with ID \`${listId}\` does not exist.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check ownership
|
||||||
|
if (list.owner_id !== interaction.user.id) {
|
||||||
|
// Check if guild has access in perms table (guild will exist here if it has perms to write)
|
||||||
|
db.get("SELECT * FROM list_permissions WHERE list_id = ? AND guild_id = ?", [listId, interaction.guild.id], async (err, perm) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Database error occurred.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!perm) {
|
||||||
|
await interaction.reply({ content: `❌ You do not own ban list **${list.name}** and this server does not have permission to modify it.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Guild has permission, proceed
|
||||||
|
importBans(list);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// User owns the list, proceed
|
||||||
|
importBans(list);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "allowguild":
|
||||||
|
{
|
||||||
|
const listId = interaction.options.getString('list');
|
||||||
|
const guildId = interaction.options.getString('guild_id') || interaction.guild.id;
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
db.get("SELECT * FROM lists WHERE id = ?", [listId], async (err, list) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Database error occurred.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!list) {
|
||||||
|
await interaction.reply({ content: `❌ Ban list with ID \`${listId}\` does not exist.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (list.owner_id !== interaction.user.id) {
|
||||||
|
await interaction.reply({ content: `❌ You do not own ban list **${list.name}**.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant
|
||||||
|
db.run("INSERT OR IGNORE INTO perms (list_id, entity_id, entity_type) VALUES (?, ?, 'guild')", [listId, guildId], async (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Failed to allow guild.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await interaction.reply({ content: `✅ Guild \`${guildId}\` has been allowed to use ban list **${list.name}**.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "revokeguild":
|
||||||
|
{
|
||||||
|
const listId = interaction.options.getString('list');
|
||||||
|
const guildId = interaction.options.getString('guild_id') || interaction.guild.id;
|
||||||
|
// Verify ownership
|
||||||
|
db.get("SELECT * FROM lists WHERE id = ?", [listId], async (err, list) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Database error occurred.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!list) {
|
||||||
|
await interaction.reply({ content: `❌ Ban list with ID \`${listId}\` does not exist.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (list.owner_id !== interaction.user.id) {
|
||||||
|
await interaction.reply({ content: `❌ You do not own ban list **${list.name}**.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke
|
||||||
|
db.run("DELETE FROM perms WHERE list_id = ? AND guild_id = ?", [listId, guildId], async (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Failed to revoke guild.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await interaction.reply({ content: `✅ Guild \`${guildId}\` has been revoked access to ban list **${list.name}**.`, ephemeral: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "view":
|
||||||
|
{
|
||||||
|
// Fetch all lists owned by the user
|
||||||
|
db.all("SELECT * FROM lists WHERE owner_id = ?", [interaction.user.id], async (err, lists) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
await interaction.reply({ content: `❌ Database error occurred.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lists.length === 0) {
|
||||||
|
await interaction.reply({ content: '❌ You do not own any ban lists.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = '📋 **Your Ban Lists:**\n';
|
||||||
|
lists.forEach(list => {
|
||||||
|
response += `• **${list.name}** (ID: \`${list.id}\`)\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply({ content: response, ephemeral: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await interaction.reply({ content: '❌ Unknown subcommand.', ephemeral: true });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${colors.red('[ERROR]')} Error handling list command:`, error);
|
||||||
|
await interaction.reply({ content: '❌ An error occurred while processing your request.', ephemeral: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
56
migrations.js
Normal file
56
migrations.js
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const util = require('util');
|
||||||
|
const colors = require('colors');
|
||||||
|
|
||||||
|
module.exports = (db) => {
|
||||||
|
const run = util.promisify(db.run.bind(db));
|
||||||
|
const get = util.promisify(db.get.bind(db));
|
||||||
|
const exec = util.promisify(db.exec.bind(db));
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await run(`CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);`);
|
||||||
|
|
||||||
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
|
let files;
|
||||||
|
try {
|
||||||
|
files = await fs.readdir(migrationsDir);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ENOENT') return resolve(); // no migrations directory
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
files = files.filter(f => path.extname(f).toLowerCase() === '.sql').sort();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const name = file;
|
||||||
|
const already = await get('SELECT 1 FROM migrations WHERE name = ?', [name]);
|
||||||
|
if (already) continue;
|
||||||
|
|
||||||
|
const sql = await fs.readFile(path.join(migrationsDir, file), 'utf8');
|
||||||
|
|
||||||
|
await exec('BEGIN');
|
||||||
|
try {
|
||||||
|
await exec(sql);
|
||||||
|
console.log(`${colors.yellow('[MIGRATION]')} Applied migration: ${name}`);
|
||||||
|
await run('INSERT INTO migrations (name) VALUES (?)', [name]);
|
||||||
|
await exec('COMMIT');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`${colors.red('[MIGRATION]')} Failed migration: ${name}`);
|
||||||
|
await exec('ROLLBACK');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`${colors.green('[MIGRATION]')} All migrations applied.`);
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
69
migrations/001_init_tables.sql
Normal file
69
migrations/001_init_tables.sql
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
-- migrations/001_init_tables.sqlite.sql
|
||||||
|
-- SQLite-compatible rewrite of migrations/001_init_tables.sql
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
-- Table: lists
|
||||||
|
CREATE TABLE IF NOT EXISTS lists (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
name TEXT NOT NULL CHECK (length(trim(name)) > 0 AND length(name) <= 24),
|
||||||
|
owner_id TEXT NOT NULL,
|
||||||
|
private INTEGER NOT NULL DEFAULT 1, -- 0 = false, 1 = true
|
||||||
|
created_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Trigger to keep updated_at current.
|
||||||
|
-- Uses an AFTER UPDATE that only performs the extra UPDATE when the client
|
||||||
|
-- didn't explicitly change updated_at; the inner UPDATE won't recurse
|
||||||
|
-- because the WHEN clause will be false on the second invocation.
|
||||||
|
CREATE TRIGGER IF NOT EXISTS trg_lists_updated_at
|
||||||
|
AFTER UPDATE ON lists
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN NEW.updated_at = OLD.updated_at
|
||||||
|
BEGIN
|
||||||
|
UPDATE lists SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Index to quickly find lists by owner
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lists_owner_id ON lists(owner_id);
|
||||||
|
|
||||||
|
-- Table: guilds
|
||||||
|
CREATE TABLE IF NOT EXISTS guilds (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
||||||
|
auto_add INTEGER NOT NULL DEFAULT 1, -- 0 = false, 1 = true
|
||||||
|
auto_remove INTEGER NOT NULL DEFAULT 0, -- 0 = false, 1 = true
|
||||||
|
created_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table: Bans
|
||||||
|
CREATE TABLE IF NOT EXISTS bans (
|
||||||
|
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
reason TEXT,
|
||||||
|
list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
||||||
|
banned_by TEXT NOT NULL,
|
||||||
|
banned_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table: Perms (Users/Guilds with write access to a list that aren't the owner)
|
||||||
|
CREATE TABLE IF NOT EXISTS perms (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
||||||
|
entity_id TEXT NOT NULL, -- user id or guild id
|
||||||
|
entity_type TEXT NOT NULL CHECK (entity_type IN ('user', 'guild')),
|
||||||
|
auto_add INTEGER NOT NULL DEFAULT 0, -- 0 = false, 1 = true; Whether this entitys bans should be auto-added to the list (only for guilds)
|
||||||
|
auto_remove INTEGER NOT NULL DEFAULT 0, -- 0 = false, 1 = true; Whether this entitys unbans should be auto-removed from the list (only for guilds)
|
||||||
|
created_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Ensure a guild can't be attached to the same list twice
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_guilds_guildid_listid ON guilds(guild_id, list_id);
|
||||||
|
|
||||||
|
-- Helpful index to look up guild rows by guild_id
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_guilds_guild_id ON guilds(guild_id);
|
||||||
|
|
||||||
|
-- Example inserts
|
||||||
|
-- INSERT INTO lists (name, owner_id, private) VALUES ('Example List', 'owner123', 1);
|
||||||
|
-- INSERT INTO guilds (guild_id, list_id, auto_add) VALUES ('guild123', (SELECT id FROM lists WHERE name = 'Example List'), 1);
|
||||||
3512
package-lock.json
generated
Normal file
3512
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
package.json
Normal file
22
package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "bansync",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.970.0",
|
||||||
|
"colors": "^1.4.0",
|
||||||
|
"discord.js": "^14.25.1",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"mariadb": "^3.4.5",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
88
s3.js
Normal file
88
s3.js
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
const s3 = require("@aws-sdk/client-s3");
|
||||||
|
const { S3Client, PutObjectCommand, GetObjectCommand } = s3;
|
||||||
|
|
||||||
|
const s3Client = new S3Client({
|
||||||
|
region: process.env.AWS_REGION,
|
||||||
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {};
|
||||||
|
|
||||||
|
module.exports.uploadFile = async (key, body) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: process.env.S3_BUCKET,
|
||||||
|
Key: key,
|
||||||
|
Body: body,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await s3Client.send(command);
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.downloadFile = async (key) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: process.env.S3_BUCKET,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const response = await s3Client.send(command);
|
||||||
|
const stream = response.Body;
|
||||||
|
const chunks = [];
|
||||||
|
try {
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||||
|
}
|
||||||
|
resolve(Buffer.concat(chunks));
|
||||||
|
} catch (streamErr) {
|
||||||
|
reject(streamErr);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to see if file/folder exists
|
||||||
|
module.exports.fileExists = async (key) => {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: process.env.S3_BUCKET,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
await s3Client.send(command);
|
||||||
|
resolve(true);
|
||||||
|
} catch (err) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// List files in a folder
|
||||||
|
module.exports.listFiles = async (prefix) => {
|
||||||
|
const { ListObjectsV2Command } = require("@aws-sdk/client-s3");
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const command = new ListObjectsV2Command({
|
||||||
|
Bucket: process.env.S3_BUCKET,
|
||||||
|
Prefix: prefix,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const response = await s3Client.send(command);
|
||||||
|
const files = response.Contents ? response.Contents.map(item => item.Key) : [];
|
||||||
|
resolve(files);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
91
sync.js
Normal file
91
sync.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
const colors = require('colors');
|
||||||
|
const { PermissionFlagsBits } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = (client, db) => {
|
||||||
|
// Handle auto_add guilds
|
||||||
|
db.all("SELECT guild_id, list_id, auto_add FROM guilds WHERE auto_add = 1", async (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(`${colors.red('[ERROR]')} Could not query guilds for auto_add:`, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rows) {
|
||||||
|
for (const row of rows) {
|
||||||
|
// Check we can access guild and have ban perms
|
||||||
|
const guild = await client.guilds.fetch(row.guild_id);
|
||||||
|
if (!guild) {
|
||||||
|
console.log(`[ERROR] Could not find guild: ${row.guild_id}`); // We don't remove if they re-add the bot
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const member = await guild.members.fetch(client.user.id);
|
||||||
|
if (!member || !member.permissions.has(PermissionFlagsBits.BanMembers)) {
|
||||||
|
console.log(`${colors.red('[ERROR]')} Bot does not have ban permissions in guild: ${row.guild_id}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const listId = row.list_id;
|
||||||
|
const guildId = row.guild_id;
|
||||||
|
const guild = await client.guilds.fetch(guildId);
|
||||||
|
const member = await guild.members.fetch(client.user.id);
|
||||||
|
const guildBans = await guild.bans.fetch();
|
||||||
|
db.all("SELECT user_id, reason FROM bans WHERE list_id = ?", [listId], async (err, bans) => {
|
||||||
|
for (const ban of bans) {
|
||||||
|
if (!guildBans.has(ban.user_id)) {
|
||||||
|
await guild.members.ban(ban.user_id, { reason: `Auto-synced ban from list ${listId}: ${ban.reason}` });
|
||||||
|
if (process.env.ENVIRONMENT !== 'production') {
|
||||||
|
console.log(`[AUTO-ADD] Banned user ${ban.user_id} in guild ${guildId} from list ${listId}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ERROR] Could not sync bans for guild ${row.guild_id}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle auto_remove guilds
|
||||||
|
db.all("SELECT guild_id, list_id, auto_remove FROM guilds WHERE auto_remove = 1", async (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(`${colors.red('[ERROR]')} Could not query guilds for auto_remove:`, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rows) {
|
||||||
|
for (const row of rows) {
|
||||||
|
// Check we can access guild and have ban perms
|
||||||
|
const guild = await client.guilds.fetch(row.guild_id);
|
||||||
|
if (!guild) {
|
||||||
|
console.log(`[ERROR] Could not find guild: ${row.guild_id}`); // We don't remove if they re-add the bot
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const member = await guild.members.fetch(client.user.id);
|
||||||
|
if (!member || !member.permissions.has(PermissionFlagsBits.BanMembers)) {
|
||||||
|
console.log(`${colors.red('[ERROR]')} Bot does not have ban permissions in guild: ${row.guild_id}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const listId = row.list_id;
|
||||||
|
const guildId = row.guild_id;
|
||||||
|
db.all("SELECT user_id FROM bans WHERE list_id = ?", [listId], async (err, bans) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const guildBans = await guild.bans.fetch();
|
||||||
|
// Anybody not in the bans list should be unbanned
|
||||||
|
for (const [bannedUserId, banInfo] of guildBans) {
|
||||||
|
if (!bans.find(ban => ban.user_id === bannedUserId)) {
|
||||||
|
await guild.members.unban(bannedUserId, `Auto-removed ban from list ${listId}`);
|
||||||
|
if (process.env.ENVIRONMENT !== 'production') {
|
||||||
|
console.log(`[AUTO-REMOVE] Unbanned user ${bannedUserId} in guild ${guildId} from list ${listId}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ERROR] Could not sync unbans for guild ${row.guild_id}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
18
theory.md
Normal file
18
theory.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Discord Ban Sync Bot
|
||||||
|
Here's the plan, Ban Lists
|
||||||
|
- Allow anybody to install the bot to their user or guild.
|
||||||
|
- Anybody can create a "ban-list" with a unique ID, name, and list of users/guilds that can modify it.
|
||||||
|
- Allow user to make list public/private? (tl;dr private mode only users allowed to modify can link the list to a guild and use it. Public ones all you'd need is the ID)
|
||||||
|
|
||||||
|
## DB Struct
|
||||||
|
- lists
|
||||||
|
- id: default uuid(); primary
|
||||||
|
- name: varchar(24); do sanitizing, this will be user defined
|
||||||
|
- owner_id: varchar(24); User Snowflake of person who currently owns list. Default is creator, might add transfer feature?
|
||||||
|
- private: bool, default true
|
||||||
|
- guilds
|
||||||
|
- id: int, autoinc, primary
|
||||||
|
- guild_id: guild id
|
||||||
|
- list_id: list id (ref to lists.id)
|
||||||
|
- auto_add: Automatically sync new bans that are added to the db, otherwise require manual sync
|
||||||
|
|
||||||
29
unbanHandler.js
Normal file
29
unbanHandler.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
const colors = require('colors');
|
||||||
|
|
||||||
|
module.exports = async (ban, db, client) => {
|
||||||
|
// Fetch perms this guild has
|
||||||
|
const guild = await client.guilds.fetch(ban.guild.id);
|
||||||
|
|
||||||
|
db.all("SELECT list_id, auto_remove FROM perms WHERE entity_id = ? AND entity_type = 'guild'", [ban.guild.id], async (err, lists) => {
|
||||||
|
;
|
||||||
|
for (const entry of lists) {
|
||||||
|
const list = await db.get("SELECT * FROM lists WHERE id = ?", [entry.list_id]);
|
||||||
|
if (!list) {
|
||||||
|
console.log(`${colors.red('[ERROR]')} List not found for id ${entry.list_id}. How'd we get here?`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.auto_remove) {
|
||||||
|
try {
|
||||||
|
const existingBan = await db.get("SELECT * FROM bans WHERE list_id = ? AND user_id = ?", [list.id, ban.user.id]);
|
||||||
|
if (!existingBan) {
|
||||||
|
// Not banned on this list
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await db.run("DELETE FROM bans WHERE list_id = ? AND user_id = ?", [list.id, ban.user.id]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[ERROR] Could not auto-remove ban for user ${ban.user.id} in guild ${guild.id}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue