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