diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b584073 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +package-lock.json +old/ +databases/* +config.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..078fecf --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# BluBot + +A self-hosted Discord bot with various moderation features and fun commands. Features a simple CLI and setup script for easy management. + +## Features + +- Automatic moderation + - Censor list (deprecated by Discord AutoMod?) + - Phish protection (powered by phish.sinking.yachts) +- Moderation + - Ban, kick, timeout members + - Purge messages by channel and/or user + - Lock channels +- Logging + - Any moderation action + - Deleted and edited messages + - Automatic moderation actions +- Misc + - Get user avatar + - Define a word using Urban Dictionary + - Neofetch-like status command + - Ping command + - Your Mom jokes (powered by yomomma.info) +- Setup + - Guided setup with customization options +- CLI + - Simple CLI with ASCII art and uptime command (more coming soon) + +## Setup + +0. Install NodeJS +1. Clone this repository +2. Run `npm run setup` and follow the steps +3. Run `npm install` to install dependencies +4. Run `npm start` to start the bot + +## Screenshots + +### Direct Messages + +![](assets/dms.png) + +### Logs + +![](assets/logs.png) + +### Neofetch command (Discord ANSI support) + +![](assets/neofetch.png) + +### CLI + +![](assets/cli.png) + +## Support + +Want to request a new feature or report a bug? + +[Join my Discord Server](https://blnk.ga/dc) +or +[send me an email](mailto:bludood@bludood.com)! diff --git a/assets/cli.png b/assets/cli.png new file mode 100644 index 0000000..5dd72ee Binary files /dev/null and b/assets/cli.png differ diff --git a/assets/dms.png b/assets/dms.png new file mode 100644 index 0000000..962916b Binary files /dev/null and b/assets/dms.png differ diff --git a/assets/logs.png b/assets/logs.png new file mode 100644 index 0000000..a0490b7 Binary files /dev/null and b/assets/logs.png differ diff --git a/assets/neofetch.png b/assets/neofetch.png new file mode 100644 index 0000000..3655911 Binary files /dev/null and b/assets/neofetch.png differ diff --git a/commands/avatar.js b/commands/avatar.js new file mode 100644 index 0000000..5cd119b --- /dev/null +++ b/commands/avatar.js @@ -0,0 +1,31 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') + +module.exports = { + data: new SlashCommandBuilder() + .setName('avatar') + .setDescription("Get your or another user's avatar") + .addUserOption(option => + option.setName('target').setDescription('User to show avatar for') + ), + async execute(interaction) { + const user = interaction.options.getUser('target') || interaction.user + const avatar = format => user.avatarURL({ format }) + interaction.reply({ + embeds: [ + { + title: `${user.username}'s avatar`, + description: `Download as [png](${avatar('png')}), [jpeg](${avatar( + 'jpeg' + )}) or [webp](${avatar('webp')}).`, + color: accent, + image: { + url: avatar('png') + } + } + ] + }) + } +} diff --git a/commands/ban.js b/commands/ban.js new file mode 100644 index 0000000..d736204 --- /dev/null +++ b/commands/ban.js @@ -0,0 +1,76 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') +const directMessage = require('../utils/directMessage') +const log = require('../utils/log') + +module.exports = { + data: new SlashCommandBuilder() + .setName('ban') + .setDescription('Ban a member.') + .addUserOption(option => + option.setName('target').setDescription('User to ban').setRequired(true) + ) + .addStringOption(option => + option.setName('reason').setDescription('Reason for the ban') + ) + .addNumberOption(option => + option.setName('deletedays').setDescription('Days to delete messages') + ), + async execute(interaction) { + if (!checkUserPerms(interaction)) + return interaction.reply({ + content: 'You do not have permission to do that!', + ephemeral: true + }) + const target = interaction.options.getUser('target') + const reason = interaction.options.getString('reason') || 'N/A' + const days = interaction.options.getNumber('deletedays') || 0 + const member = await interaction.guild.members + .fetch({ user: target, force: true }) + .catch(() => null) + if (!member) + return interaction.reply({ + content: "I can't find that user!", + ephemeral: true + }) + if (!member.bannable) + return interaction.reply({ + content: "I can't ban that user!", + ephemeral: true + }) + await interaction.reply({ + embeds: [ + { + title: `${target.tag} banned.`, + color: accent + } + ], + ephemeral: true + }) + const dm = await directMessage(interaction.guild, target, 'ban', { + reason, + moderator: { + id: interaction.user.id + } + }) + if (!dm) + await interaction.followUp({ + content: 'I could not message that user!', + ephemeral: true + }) + await member.ban(target, { days: days, reason: reason }) + log(interaction.guild, 'ban', { + target: { + id: target.id, + tag: target.tag + }, + moderator: { + id: interaction.user.id + }, + reason + }) + } +} diff --git a/commands/censor.js b/commands/censor.js new file mode 100644 index 0000000..78395f8 --- /dev/null +++ b/commands/censor.js @@ -0,0 +1,93 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const fs = require('fs') +const checkUserPerms = require('../utils/checkUserPerms') + +module.exports = { + data: new SlashCommandBuilder() + .setName('censor') + .setDescription('Configure censored words') + .addSubcommand(subcommand => + subcommand + .setName('add') + .setDescription('Add a censored word') + .addStringOption(option => + option + .setName('word') + .setDescription('The word to censor') + .setRequired(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('remove') + .setDescription('Remove a censored word') + .addStringOption(option => + option + .setName('word') + .setDescription('The word to remove') + .setRequired(true) + ) + ) + .addSubcommand(subcommand => + subcommand.setName('list').setDescription('List all censored words') + ), + async execute(interaction) { + const word = interaction.options.getString('word')?.toLowerCase() + if (!checkUserPerms(interaction)) + return interaction.reply({ + content: 'You do not have permission to do that!', + ephemeral: true + }) + + if (interaction.options.getSubcommand() === 'add') { + const censored = JSON.parse(fs.readFileSync('./databases/censored.json')) + if (censored.map(c => c.word).includes(word)) + return await interaction.reply({ + content: 'This word is already censored!', + ephemeral: true + }) + censored.push({ + user: interaction.user.id, + word + }) + fs.writeFileSync('./databases/censored.json', JSON.stringify(censored)) + await interaction.reply({ + content: `Censored the word ${word}!`, + ephemeral: true + }) + } else if (interaction.options.getSubcommand() === 'remove') { + const censored = JSON.parse(fs.readFileSync('./databases/censored.json')) + const found = censored.find(c => c.word === word) + if (!found) + return await interaction.reply({ + content: 'This word is not censored!', + ephemeral: true + }) + const index = censored.indexOf(found) + censored.splice(index, 1) + fs.writeFileSync('./databases/censored.json', JSON.stringify(censored)) + await interaction.reply({ + content: `Removed censor for the word ${word}!`, + ephemeral: true + }) + } else if (interaction.options.getSubcommand() === 'list') { + const censored = JSON.parse(fs.readFileSync('./databases/censored.json')) + await interaction.reply({ + embeds: [ + { + title: 'Censored words', + color: accent, + fields: censored.map(c => ({ + name: c.word, + value: `Added by <@${c.user}>` + })) + } + ], + ephemeral: true + }) + } else return interaction.reply('Invalid option.') + } +} diff --git a/commands/checkPerms.js b/commands/checkPerms.js new file mode 100644 index 0000000..0e6fed7 --- /dev/null +++ b/commands/checkPerms.js @@ -0,0 +1,30 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') + +module.exports = { + data: new SlashCommandBuilder() + .setName('check') + .setDescription('Check if are allowed to moderate using this bot.'), + async execute(interaction) { + if (checkUserPerms(interaction)) + return interaction.reply({ + embeds: [ + { + title: `You are allowed to moderate using this bot!`, + color: accent + } + ] + }) + return interaction.reply({ + embeds: [ + { + title: `You are not allowed to moderate using this bot.`, + color: accent + } + ] + }) + } +} diff --git a/commands/define.js b/commands/define.js new file mode 100644 index 0000000..217093a --- /dev/null +++ b/commands/define.js @@ -0,0 +1,38 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const axios = require('axios').default + +module.exports = { + data: new SlashCommandBuilder() + .setName('define') + .setDescription('Define a word with Urban Dictionary!') + .addStringOption(option => + option + .setName('query') + .setDescription('Word to search for') + .setRequired(true) + ), + async execute(interaction) { + // my api ;) + const api = 'https://urbanapi.up.railway.app' + const query = interaction.options.getString('query') + interaction.deferReply() + const res = await axios + .get(`${api}/define/${query}`, { + validateStatus: false + }) + .catch(() => null) + if (!res?.data?.success) + return interaction.editReply('An error has occured!') + if (res.status === 404) + return interaction.editReply('Could not find that word!') + const embed = { + color: accent, + title: res.data.result[0].word, + description: res.data.result[0].description + } + return interaction.editReply({ embeds: [embed] }) + } +} diff --git a/commands/kick.js b/commands/kick.js new file mode 100644 index 0000000..44c527f --- /dev/null +++ b/commands/kick.js @@ -0,0 +1,72 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') +const directMessage = require('../utils/directMessage') +const log = require('../utils/log') + +module.exports = { + data: new SlashCommandBuilder() + .setName('kick') + .setDescription('Kick a member.') + .addUserOption(option => + option.setName('target').setDescription('User to kick').setRequired(true) + ) + .addStringOption(option => + option.setName('reason').setDescription('Reason for the kick') + ), + async execute(interaction) { + if (!checkUserPerms(interaction)) + return interaction.reply({ + content: 'You do not have permission to do that!', + ephemeral: true + }) + const target = interaction.options.getUser('target') + const reason = interaction.options.getString('reason') || 'N/A' + const member = await interaction.guild.members + .fetch({ user: target, force: true }) + .catch(() => null) + if (!member) + return interaction.reply({ + content: "I can't find that user!", + ephemeral: true + }) + if (!member.kickable) + return interaction.reply({ + content: "I can't kick that user!", + ephemeral: true + }) + await interaction.reply({ + embeds: [ + { + title: `${target.tag} kicked.`, + color: accent + } + ], + ephemeral: true + }) + const dm = await directMessage(interaction.guild, target, 'kick', { + reason, + moderator: { + id: interaction.user.id + } + }) + if (!dm) + await interaction.followUp({ + content: 'I could not message that user!', + ephemeral: true + }) + await member.kick(reason) + log(interaction.guild, 'kick', { + target: { + id: target.id, + tag: target.tag + }, + moderator: { + id: interaction.user.id + }, + reason + }) + } +} diff --git a/commands/lock.js b/commands/lock.js new file mode 100644 index 0000000..237b447 --- /dev/null +++ b/commands/lock.js @@ -0,0 +1,56 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') +const log = require('../utils/log') + +module.exports = { + data: new SlashCommandBuilder() + .setName('lock') + .setDescription('Lock a channel') + .addChannelOption(option => + option.setName('channel').setDescription('Channel to lock') + ), + async execute(interaction) { + if (!checkUserPerms(interaction)) + return interaction.reply({ + content: 'You do not have permission to do that!', + ephemeral: true + }) + const channel = + interaction.options.getChannel('channel') || interaction.channel + if (!channel) return interaction.reply('I cannot access that channel!') + if ( + !channel + .permissionsFor(interaction.guild.roles.everyone) + .has('SEND_MESSAGES') + ) + return interaction.reply('This channel is already locked!') + try { + channel.permissionOverwrites.edit(interaction.guild.roles.everyone, { + SEND_MESSAGES: false + }) + await interaction.reply({ + embeds: [ + { + title: `#${channel.name} locked.`, + color: accent + } + ] + }) + log(interaction.guild, 'lock', { + channel: { + id: channel.id, + name: channel.name + }, + moderator: { + id: interaction.user.id + } + }) + } catch (error) { + console.log(error) + interaction.reply('I cannot lock that channel!') + } + } +} diff --git a/commands/neofetch.js b/commands/neofetch.js new file mode 100644 index 0000000..90a9fa7 --- /dev/null +++ b/commands/neofetch.js @@ -0,0 +1,80 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const chalk = require('chalk') +const os = require('os') +const { getDependency, getVersion } = require('../utils/packagejson') + +module.exports = { + data: new SlashCommandBuilder() + .setName('neofetch') + .setDescription('Information about this bot') + .addBooleanOption(option => + option + .setName('minimal') + .setDescription( + 'Show minimal message without ASCII art. Better for mobile screens.' + ) + ), + async execute(interaction) { + const minimal = interaction.options.getBoolean('minimal') + let uptime = process.uptime() + const hours = Math.floor(uptime / 3600) + uptime %= 3600 + const minutes = Math.floor(uptime / 60) + const seconds = Math.floor(uptime % 60) + + const cpus = os.cpus() + const cpu_name = cpus[0].model + + // prettier-ignore + const info = [ + `${chalk.red("BluBot")} @ ${chalk.red(os.hostname())}`, + '', + `${chalk.red("Version")}: ${getVersion()}`, + `${chalk.red("node")}: ${process.version}`, + `${chalk.red("discord.js")}: v${getDependency('discord.js').version}`, + '', + `${chalk.red("Uptime")}: ${hours ? `${hours} hours, ` : ''}${minutes ? `${minutes} minutes and ` : ''}${seconds} seconds`, + `${chalk.red("Ping")}: ${interaction.client.ws.ping}ms`, + '', + `${chalk.red("OS")}: ${os.version()} ${os.arch()}`, + `${chalk.red("CPU")}: ${cpu_name} (${cpus.length})`, + `${chalk.red("Memory")}: ${Math.round(os.freemem() / 1000000)}MB / ${Math.round(os.totalmem() / 1000000)}MB`, + '', + `${chalk.black("██")}${chalk.red("██")}${chalk.green("██")}${chalk.yellow("██")}${chalk.blue("██")}${chalk.magenta("██")}${chalk.cyan("██")}${chalk.white("██")}` + ] + const ascii = [ + '', + ' .---::. .. ', + ' .=+++++=-::::::::-=+: ', + ' =+#*******+:..:=****+====- ', + ' *%%#**++++##**##++++**#%%* ', + ' -%%%%%%####+..+#####%%%%%. ', + ' :*%%%%%%%%+.:::*%%%%%%%%+ ', + ' :==+****+-.-+=-.-+****=. ', + ' :++=-----::::::::---- ', + ' -====----------------. ', + ' =======--::::::::::::=. ', + ' -=++=======:..........=. ', + ' .=++++======-::.....:::=. ', + ' :=++++======-::::::::--- ', + ' -++=+++++++=----------: ', + ' ==++++++++==--------: ', + ' ===++++++++=======: ', + ' ==-:=++++++++==++: ', + ' =+++++:..==. ', + ' .== -=. .. ', + ' ..::::-:::::..:::. ', + '' + ] + const offset = Math.floor((ascii.length - info.length) / 2) + return minimal + ? interaction.reply(`\`\`\`ansi\n \n ${info.join('\n ')}\n \`\`\``) + : interaction.reply( + `\`\`\`ansi\n${chalk.blue( + ascii + .map((a, i) => ' ' + a + ' ' + (info[i - offset] || '')) + .join('\n') + )}\n\n\n\`\`\`` + ) + } +} diff --git a/commands/ping.js b/commands/ping.js new file mode 100644 index 0000000..d16aef2 --- /dev/null +++ b/commands/ping.js @@ -0,0 +1,8 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') + +module.exports = { + data: new SlashCommandBuilder().setName('ping').setDescription('Pong!'), + async execute(interaction) { + return interaction.reply(`Pong! (${interaction.client.ws.ping}ms)`) + } +} diff --git a/commands/purge.js b/commands/purge.js new file mode 100644 index 0000000..a426205 --- /dev/null +++ b/commands/purge.js @@ -0,0 +1,126 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') +const log = require('../utils/log') + +module.exports = { + data: new SlashCommandBuilder() + .setName('purge') + .setDescription('Purge messages.') + .addSubcommand(subcommand => + subcommand + .setName('channel') + .setDescription('Purge messages by channel') + .addNumberOption(option => + option + .setName('amount') + .setDescription('Amount of messages to purge (max 100)') + .setRequired(true) + ) + .addChannelOption(option => + option + .setName('channel') + .setDescription('Channel to purge messages in') + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('user') + .setDescription('Purge messages by user') + .addUserOption(option => + option + .setName('user') + .setDescription('User to purge messages for') + .setRequired(true) + ) + .addNumberOption(option => + option + .setName('amount') + .setDescription('Amount of messages to purge (max 100)') + .setRequired(true) + ) + .addChannelOption(option => + option + .setName('channel') + .setDescription('Channel to purge messages in') + ) + ), + async execute(interaction) { + if (!checkUserPerms(interaction)) + return interaction.reply({ + content: 'You do not have permission to do that!', + ephemeral: true + }) + const amount = interaction.options.getNumber('amount') + const channel = + interaction.options.getChannel('channel') || interaction.channel + const user = interaction.options.getUser('user') + + if (amount > 100) + return interaction.reply('You can only purge up to 100 messages at once.') + if (amount < 1) + return interaction.reply('You must purge at least 1 message.') + + const command = interaction.options.getSubcommand() + if (command === 'channel') { + await channel.bulkDelete(amount) + await interaction.reply({ + embeds: [ + { + title: `Purged ${amount} message${amount === 1 ? '' : 's'} in #${ + channel.name + }!`, + color: accent + } + ], + ephemeral: true + }) + log(interaction.guild, 'purge', { + moderator: { + id: interaction.user.id + }, + channel: { + id: channel.id + }, + amount + }) + return + } else if (command === 'user') { + const messages = channel.messages.fetch({ limit: 100 }) + const userMessages = (await messages) + .filter(m => m.author.id === user.id) + .toJSON() + .splice(0, amount) + if (userMessages.length === 0) + return interaction.reply('Could not find any messages by that user.') + await channel.bulkDelete(userMessages) + await interaction.reply({ + embeds: [ + { + title: `Purged ${amount} message${amount == 1 ? '' : 's'} by ${ + user.tag + } in #${channel.name}!`, + color: accent + } + ], + ephemeral: true + }) + log(interaction.guild, 'purge', { + target: { + id: user.id, + tag: user.tag + }, + moderator: { + id: interaction.user.id + }, + channel: { + id: channel.id + }, + amount + }) + return + } + } +} diff --git a/commands/saveEmoji.js b/commands/saveEmoji.js new file mode 100644 index 0000000..02a3e56 --- /dev/null +++ b/commands/saveEmoji.js @@ -0,0 +1,21 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') + +module.exports = { + data: new SlashCommandBuilder() + .setName('saveemoji') + .setDescription('Converts an emoji to an image.') + .addStringOption(option => + option + .setName('emoji') + .setDescription('Emoji to convert') + .setRequired(true) + ), + async execute(interaction) { + const emojiName = interaction.options.getString('emoji') + const isCustom = emojiName.startsWith('<:') && emojiName.endsWith('>') + const emoji = isCustom + ? `https://cdn.discordapp.com/emojis/${emojiName.match(/[0-9]+/)[0]}.png` + : `https://emojicdn.elk.sh/${emojiName}?style=apple` + return interaction.reply(emoji) + } +} diff --git a/commands/timeout.js b/commands/timeout.js new file mode 100644 index 0000000..aed36e3 --- /dev/null +++ b/commands/timeout.js @@ -0,0 +1,109 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') +const directMessage = require('../utils/directMessage') +const log = require('../utils/log') + +module.exports = { + data: new SlashCommandBuilder() + .setName('timeout') + .setDescription('Time out a member.') + .addUserOption(option => + option + .setName('target') + .setDescription('User to timeout') + .setRequired(true) + ) + .addStringOption(option => + option + .setName('duration') + .setDescription('Duration for the timeout (s, m, h, d, w, M, y)') + .setRequired(true) + ) + .addStringOption(option => + option.setName('reason').setDescription('Reason for the timeout') + ), + async execute(interaction) { + const target = interaction.options.getUser('target') + const reason = interaction.options.getString('reason') || 'N/A' + const member = await interaction.guild.members + .fetch({ user: target, force: true }) + .catch(() => null) + if (!member) + return interaction.reply({ + content: "I can't find that user!", + ephemeral: true + }) + if (!member.moderatable) + return interaction.reply({ + content: "I can't timeout that user!", + ephemeral: true + }) + const durationUnits = { + s: 1000, + m: 60000, + h: 3600000, + d: 86400000, + w: 604800000, + M: 2592000000, + y: 31536000000 + } + const unitNames = { + s: 'second', + m: 'minute', + h: 'hour', + d: 'day', + w: 'week', + M: 'month', + y: 'year' + } + const [amount, unit] = + interaction.options + .getString('duration') + .match(/([0-9]+)([a-zA-Z]{1})/) + ?.splice(1) || [] + if (!durationUnits[unit]) + return await interaction.reply({ + content: `${unit} is not a valid unit!`, + ephemeral: true + }) + const duration = parseInt(amount) * durationUnits[unit] + await interaction.reply({ + embeds: [ + { + title: `${target.tag} timed out.`, + color: accent + } + ], + ephemeral: true + }) + const dm = await directMessage(interaction.guild, target, 'timeout', { + reason, + moderator: { + id: interaction.user.id + }, + duration: `Duration: ${amount} ${unitNames[unit]}${ + amount == 1 ? '' : 's' + }` + }) + if (!dm) + await interaction.followUp({ + content: 'I could not message that user!', + ephemeral: true + }) + await member.timeout(duration, reason) + log(interaction.guild, 'timeout', { + target: { + id: target.id, + tag: target.tag + }, + moderator: { + id: interaction.user.id + }, + reason, + duration: `${amount} ${unitNames[unit]}${amount == 1 ? '' : 's'}` + }) + } +} diff --git a/commands/unban.js b/commands/unban.js new file mode 100644 index 0000000..c46ac4d --- /dev/null +++ b/commands/unban.js @@ -0,0 +1,47 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') +const log = require('../utils/log') + +module.exports = { + data: new SlashCommandBuilder() + .setName('unban') + .setDescription('Revokes the ban for a member.') + .addUserOption(option => + option.setName('target').setDescription('User to unban').setRequired(true) + ) + .addStringOption(option => + option.setName('reason').setDescription('Reason for the unban') + ), + async execute(interaction) { + if (!checkUserPerms(interaction)) + return interaction.reply({ + content: 'You do not have permission to do that!', + ephemeral: true + }) + const target = interaction.options.getUser('target') + const reason = interaction.options.getString('reason') || 'N/A' + await interaction.guild.members.unban(target) + await interaction.reply({ + embeds: [ + { + title: `${target.tag} unbanned.`, + color: accent + } + ], + ephemeral: true + }) + log(interaction.guild, 'unban', { + target: { + id: target.id, + tag: target.tag + }, + moderator: { + id: interaction.user.id + }, + reason + }) + } +} diff --git a/commands/unlock.js b/commands/unlock.js new file mode 100644 index 0000000..8a5d6a8 --- /dev/null +++ b/commands/unlock.js @@ -0,0 +1,56 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') +const log = require('../utils/log') + +module.exports = { + data: new SlashCommandBuilder() + .setName('unlock') + .setDescription('Unlock a channel') + .addChannelOption(option => + option.setName('channel').setDescription('Channel to unlock') + ), + async execute(interaction) { + if (!checkUserPerms(interaction)) + return interaction.reply({ + content: 'You do not have permission to do that!', + ephemeral: true + }) + const channel = + interaction.options.getChannel('channel') || interaction.channel + if (!channel) return interaction.reply('I cannot access that channel!') + if ( + channel + .permissionsFor(interaction.guild.roles.everyone) + .has('SEND_MESSAGES') + ) + return interaction.reply('This channel is not locked!') + try { + channel.permissionOverwrites.edit(interaction.guild.roles.everyone, { + SEND_MESSAGES: null + }) + await interaction.reply({ + embeds: [ + { + title: `#${channel.name} unlocked.`, + color: accent + } + ] + }) + log(interaction.guild, 'unlock', { + channel: { + id: channel.id, + name: channel.name + }, + moderator: { + id: interaction.user.id + } + }) + } catch (error) { + console.log(error) + interaction.reply('I cannot unlock that channel!') + } + } +} diff --git a/commands/untimeout.js b/commands/untimeout.js new file mode 100644 index 0000000..5463027 --- /dev/null +++ b/commands/untimeout.js @@ -0,0 +1,70 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const { + customization: { accent } +} = require('../config.json') +const checkUserPerms = require('../utils/checkUserPerms') +const directMessage = require('../utils/directMessage') +const log = require('../utils/log') + +module.exports = { + data: new SlashCommandBuilder() + .setName('untimeout') + .setDescription('Remove the timeout a member.') + .addUserOption(option => + option + .setName('target') + .setDescription('User to remove timeout for') + .setRequired(true) + ) + .addStringOption(option => + option.setName('reason').setDescription('Reason for the timeout removal') + ), + async execute(interaction) { + const target = interaction.options.getUser('target') + const reason = interaction.options.getString('reason') || 'N/A' + const member = await interaction.guild.members + .fetch({ user: target, force: true }) + .catch(() => null) + if (!member) + return interaction.reply({ + content: "I can't find that user!", + ephemeral: true + }) + if (!member.moderatable) + return interaction.reply({ + content: "I can't remove the timeout for that user!", + ephemeral: true + }) + await interaction.reply({ + embeds: [ + { + title: `Removed timeout for ${target.tag}.`, + color: accent + } + ], + ephemeral: true + }) + const dm = await directMessage(interaction.guild, target, 'untimeout', { + reason, + moderator: { + id: interaction.user.id + } + }) + if (!dm) + await interaction.followUp({ + content: 'I could not message that user!', + ephemeral: true + }) + await member.timeout(null, reason) + log(interaction.guild, 'untimeout', { + target: { + id: target.id, + tag: target.tag + }, + moderator: { + id: interaction.user.id + }, + reason + }) + } +} diff --git a/commands/yourmom.js b/commands/yourmom.js new file mode 100644 index 0000000..0026a38 --- /dev/null +++ b/commands/yourmom.js @@ -0,0 +1,39 @@ +const { SlashCommandBuilder } = require('@discordjs/builders') +const axios = require('axios').default +const { + customization: { accent } +} = require('../config.json') + +module.exports = { + data: new SlashCommandBuilder() + .setName('yourmom') + .setDescription("yo momma so phat she couldn't run this command"), + async execute(interaction) { + const joke = await axios.get('https://api.yomomma.info').catch(() => null) + if (joke?.data.joke) + interaction.reply({ + embeds: [ + { + title: joke.data.joke, + color: accent, + footer: { + text: `Powered by api.yomomma.info` + } + } + ] + }) + else { + interaction.reply({ + embeds: [ + { + title: 'Yo momma so phat she rolled over the cables and broke them', + color: accent, + footer: { + text: 'I was not able to fetch a joke from api.yomomma.info.' + } + } + ] + }) + } + } +} diff --git a/console.js b/console.js new file mode 100644 index 0000000..e8f526a --- /dev/null +++ b/console.js @@ -0,0 +1,68 @@ +const chalk = require('chalk') +const sleep = require('./utils/sleep') + +async function motd(tag) { + console.clear() + const ascii = `______ _ ______ _ +| ___ \\ | | ___ \\ | | +| |_/ / |_ _| |_/ / ___ | |_ +| ___ \\ | | | | ___ \\/ _ \\| __| +| |_/ / | |_| | |_/ / (_) | |_ +\\____/|_|\\__,_\\____/ \\___/ \\__| +=============================== +`.split('\n') + for (let line of ascii) { + console.log(chalk.hex('#0064FF')(line)) + await sleep(50) + } + tag && console.log(`Welcome to BluBot! Your bot (${tag}) is now running.`) + console.log('Press h for help.') +} + +const commands = { + h: () => { + console.log(` + [h] Show this help page + [m] Show the MOTD + [u] Show uptime + [q] Quit + `) + }, + m: motd, + u: () => { + let uptime = process.uptime() + const hours = Math.floor(uptime / 3600) + uptime %= 3600 + const minutes = Math.floor(uptime / 60) + const seconds = Math.floor(uptime % 60) + console.log( + chalk.yellow( + `\nUptime: ${hours ? `${hours} hours, ` : ''}${ + minutes ? `${minutes} minutes and ` : '' + }${seconds} seconds.` + ) + ) + }, + q: () => { + console.log('Goodbye!') + process.exit() + } +} + +module.exports = { + init: () => { + console.clear() + console.log(chalk.yellow('Starting BluBot...')) + process.stdin.setRawMode?.(true) + process.stdin.resume() + process.stdin.setEncoding('utf8') + + process.stdin.on('data', key => { + if (key === '\u0003') process.exit() + const found = commands[key] + if (!found) return + found() + }) + }, + motd +} diff --git a/events/messageCreate.js b/events/messageCreate.js new file mode 100644 index 0000000..fe164c1 --- /dev/null +++ b/events/messageCreate.js @@ -0,0 +1,33 @@ +const fs = require('fs') +const phishing = require('../filters/phishing').check +const log = require('../utils/log') + +module.exports = { + event: 'messageCreate', + async listener(message) { + if (message.author.bot) return + const phishingLinks = await phishing(message.content) + if (phishingLinks && phishingLinks.length !== 0) { + await message.delete() + const messaged = JSON.parse(fs.readFileSync('./databases/messaged.json')) + if (messaged[message.author.id]?.time > Date.now() / 1000) return + message.author.send( + "Sorry, you can't send that link here!\nThe link was referring to a known phishing scam, so i deleted the message for you." + ) + if (!messaged[message.author.id]) messaged[message.author.id] = {} + messaged[message.author.id].time = Date.now() / 1000 + 300 + fs.writeFileSync('./databases/messaged.json', JSON.stringify(messaged)) + log(message.guild, 'phish', { + channel: { + id: message.channel.id + }, + target: { + id: message.author.id, + tag: message.author.tag + }, + content: message.content, + site: phishingLinks.join('\n') + }) + } + } +} diff --git a/events/messageDelete.js b/events/messageDelete.js new file mode 100644 index 0000000..23cd406 --- /dev/null +++ b/events/messageDelete.js @@ -0,0 +1,48 @@ +const fs = require('fs') +const log = require('../utils/log') +const sleep = require('../utils/sleep') + +module.exports = { + event: 'messageDelete', + async listener(message) { + if (!fs.existsSync('./databases/deleted.txt')) + fs.writeFileSync('./databases/deleted.txt', 'false', 'utf-8') + if (fs.readFileSync('./databases/deleted.txt', 'utf-8') === 'true') { + return fs.writeFileSync('./databases/deleted.txt', 'false', 'utf-8') + } + if (!message.guild) return + await sleep(1000) + const fetchedLogs = await message.guild.fetchAuditLogs({ + limit: 1, + type: 'MESSAGE_DELETE' + }) + const deletionLog = fetchedLogs.entries.first() + const { executor, target } = deletionLog || {} + if ( + target?.id === message.author.id && + deletionLog.createdAt > message.createdAt + ) + log(message.guild, 'messageDelete', { + moderator: { + id: executor.id + }, + content: message.content, + channel: { + id: message.channel.id + }, + target: { + id: message.author.id + } + }) + else + log(message.guild, 'messageDelete', { + content: message.content, + channel: { + id: message.channel.id + }, + target: { + id: message.author.id + } + }) + } +} diff --git a/events/messageUpdate.js b/events/messageUpdate.js new file mode 100644 index 0000000..78c942d --- /dev/null +++ b/events/messageUpdate.js @@ -0,0 +1,19 @@ +const fs = require('fs') +const log = require('../utils/log') + +module.exports = { + event: 'messageUpdate', + async listener(oldMessage, newMessage) { + if (oldMessage.content === newMessage.content) return + log(newMessage.guild, 'messageEdit', { + oldMessage: oldMessage.content, + content: newMessage.content, + target: { + id: newMessage.author.id + }, + channel: { + id: newMessage.channel.id + } + }) + } +} diff --git a/filters/phishing.js b/filters/phishing.js new file mode 100644 index 0000000..ffe1ed5 --- /dev/null +++ b/filters/phishing.js @@ -0,0 +1,18 @@ +const axios = require('axios').default + +module.exports = { + check: async message => { + const matches = message.match(/(http|https)(:\/\/)[a-zA-Z0-9\-\.]+/gm) + if (!matches) return null + const detected = [] + for (let match of matches) { + const domain = match.split('://')[1] + const phish = await axios + .get(`https://phish.sinking.yachts/v2/check/${domain}`) + .catch(() => null) + if (!phish?.data) continue + detected.push(match) + } + return detected + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..edee210 --- /dev/null +++ b/index.js @@ -0,0 +1,64 @@ +const { Client, Collection, Intents } = require('discord.js') +const fs = require('fs') +const deploy = require('./utils/deploy') +const bconsole = require('./console') + +if (!fs.existsSync('./config.json')) { + console.log( + "Looks like you haven't set up the bot yet! Please run 'npm run setup' and try again." + ) + process.exit() +} + +const client = new Client({ + intents: [ + Intents.FLAGS.GUILDS, + Intents.FLAGS.GUILD_MESSAGES, + Intents.FLAGS.GUILD_VOICE_STATES + ] +}) + +client.commands = new Collection() +const commandFiles = fs + .readdirSync('./commands') + .filter(file => file.endsWith('.js')) +for (const file of commandFiles) { + const command = require(`./commands/${file}`) + client.commands.set(command.data.name, command) +} + +bconsole.init() +client.once('ready', async c => { + bconsole.motd(c.user.tag) + deploy(c.user.id) +}) + +for (const eventFile of fs + .readdirSync('./events') + .filter(file => file.endsWith('.js'))) { + const event = require(`./events/${eventFile}`) + client.on(event.event, event.listener) +} + +client.on('unhandledRejection', error => { + console.log(error) +}) + +client.on('error', error => { + console.log(error) +}) + +client.on('interactionCreate', async interaction => { + if (interaction.isButton()) return + const command = client.commands.get(interaction.commandName) + if (command) { + try { + await command.execute(interaction) + } catch (error) { + console.error(error) + } + } +}) + +const { token } = require('./config.json') +client.login(token) diff --git a/package.json b/package.json new file mode 100644 index 0000000..0d7fe02 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "blubot", + "version": "2.0", + "description": "Discord moderation bot", + "main": "index.js", + "scripts": { + "start": "node index.js", + "setup": "node setup.js" + }, + "author": "BluDood", + "license": "ISC", + "dependencies": { + "@discordjs/builders": "^0.9.0", + "@discordjs/rest": "^0.2.0-canary.0", + "axios": "^0.24.0", + "chalk": "^4.1.2", + "discord-api-types": "^0.25.2", + "discord.js": "^13.6.0" + } +} diff --git a/setup.js b/setup.js new file mode 100644 index 0000000..0d288df --- /dev/null +++ b/setup.js @@ -0,0 +1,93 @@ +const fs = require('fs') + +const current = { + guildId: null, + token: null, + customization: { + accent: null, + colors: { + good: null, + medium: null, + bad: null + } + }, + modRoles: [], + channels: { + logs: null + } +} + +function readline() { + return new Promise(resolve => { + process.stdin.once('data', d => { + resolve(d.toString().trim()) + }) + }) +} + +;(async () => { + if (fs.existsSync('config.json')) { + console.log('Configuration file already exists! Overwrite? [y/N]') + const overwrite = (await readline()).toLowerCase() + if (overwrite === '' || overwrite === 'n') { + console.log('Abort.') + process.exit() + } + } + console.log( + 'Welcome to the BluBot setup!\nThis script will set up your bot with its token, guild, channels and roles.\nPress Enter to continue...' + ) + await readline() + console.log('Please enter your Discord bot token:') + current.token = await readline() + console.log( + 'Now enter the guild ID of your server. You can find this by enabling Developer Mode and right-clicking your guild.' + ) + current.guildId = await readline() + console.log( + "That's a really nice server you got! Just kidding, i'm not sentient ;(\nWould you like to customize your bot with custom colors for embeds? [y/N]" + ) + const customColors = (await readline()).toLowerCase() + if (customColors === 'y') { + console.log( + 'Alright. Please enter a HEX color you would like as an accent color. This will apply to generic embeds. Leave blank for default.' + ) + current.customization.accent = await readline() + console.log( + 'Now enter a HEX color you would like as a "good" color. This will apply to embeds which remove a moderation. Leave blank for default.' + ) + current.customization.colors.good = await readline() + console.log( + 'Now enter a HEX color you would like as a "medium" color. This will apply to embeds which add a semi-fatal moderation like a kick or timeout. Leave blank for default.' + ) + current.customization.colors.medium = await readline() + console.log( + 'Now enter a HEX color you would like as a "bad" color. This will apply to embeds which add a fatal moderation like a ban. Leave blank for default.' + ) + current.customization.colors.bad = await readline() + } + console.log( + (customColors === 'y' ? '' : "Alright, i'll use the default colors. ") + + 'Now, time to set up your moderation roles. Enter all your moderation role IDs here, separated by a comma.' + ) + current.modRoles = (await readline()).split(',').map(r => parseInt(r.trim())) + console.log("Got that. Now enter the channel you'd like me to send logs to:") + current.channels.logs = parseInt((await readline()).trim()) + console.log(` +Guild ID: ${current.guildId} +Bot token: ${current.token} +Moderator Roles: ${current.modRoles.join(', ')} +Logging channel: ${current.channels.logs} +Does this look correct? [Y/n] + `) + const correct = (await readline()).toLowerCase() + if (!(correct === '' || correct === 'y')) { + console.log('Abort.') + process.exit() + } + fs.writeFileSync('config.json', JSON.stringify(current)) + console.log( + "Success! Install dependencies with the command 'npm install', and start the bot with 'npm start'!" + ) + process.exit() +})() diff --git a/utils/checkBotPerms.js b/utils/checkBotPerms.js new file mode 100644 index 0000000..31019ce --- /dev/null +++ b/utils/checkBotPerms.js @@ -0,0 +1,26 @@ +module.exports = interaction => { + const requiredPerms = [ + 'KICK_MEMBERS', + 'BAN_MEMBERS', + 'ADD_REACTIONS', + 'VIEW_AUDIT_LOG', + 'VIEW_CHANNEL', + 'SEND_MESSAGES', + 'MANAGE_MESSAGES', + 'EMBED_LINKS', + 'ATTACH_FILES', + 'READ_MESSAGE_HISTORY', + 'CONNECT', + 'SPEAK', + 'CHANGE_NICKNAME', + 'MANAGE_NICKNAMES', + 'MANAGE_ROLES', + 'MODERATE_MEMBERS' + ] + const currentPerms = interaction.guild.me.permissions.serialize() + const missingPerms = requiredPerms.filter( + perm => currentPerms[perm] === false + ) + if (missingPerms.length === 0) return true + return missingPerms +} diff --git a/utils/checkUserPerms.js b/utils/checkUserPerms.js new file mode 100644 index 0000000..8c1cdab --- /dev/null +++ b/utils/checkUserPerms.js @@ -0,0 +1,10 @@ +const { modRoles } = require('../config.json') +module.exports = interaction => { + const roles = interaction.member.roles.cache.map(r => r.id) + if ( + roles.some(id => modRoles.includes(id)) || + interaction.member.permissions.has('ADMINISTRATOR') + ) + return true + return false +} diff --git a/utils/deploy.js b/utils/deploy.js new file mode 100644 index 0000000..b83f2fe --- /dev/null +++ b/utils/deploy.js @@ -0,0 +1,40 @@ +const sleep = require('./sleep') + +module.exports = id => { + const fs = require('fs') + const crypto = require('crypto') + const { REST } = require('@discordjs/rest') + const { Routes } = require('discord-api-types/v9') + const { guildId, token } = require('../config.json') + + const commands = [] + const commandFiles = fs + .readdirSync('./commands') + .filter(file => file.endsWith('.js')) + + for (const file of commandFiles) { + const command = require(`../commands/${file}`) + commands.push(command.data.toJSON()) + } + const commandsHash = crypto + .createHash('sha256') + .update(JSON.stringify(commands)) + .digest('hex') + if (!fs.existsSync('./databases/commandsHash.txt')) + fs.writeFileSync('./databases/commandsHash.txt', '') + if (fs.readFileSync('./databases/commandsHash.txt', 'utf-8') === commandsHash) + return + fs.writeFileSync('./databases/commandsHash.txt', commandsHash) + + const rest = new REST({ version: '9' }).setToken(token) + + rest + .put(Routes.applicationGuildCommands(id, guildId), { body: commands }) + .then(() => + // so it doesn't interfere with the console animation + sleep(1000).then(() => + console.log('Successfully updated guild commands.') + ) + ) + .catch(console.error) +} diff --git a/utils/directMessage.js b/utils/directMessage.js new file mode 100644 index 0000000..cd365a0 --- /dev/null +++ b/utils/directMessage.js @@ -0,0 +1,53 @@ +module.exports = async (guild, target, type, info) => { + const { + customization: { accent, colors }, + channels: { logs } + } = require('../config.json') + const template = { + title: '', + fields: [ + { + name: 'Reason', + value: info.reason || 'N/A' + }, + { + name: 'Moderator', + value: `<@${info.moderator?.id}>` + } + ] + } + const types = { + ban: () => { + const embed = JSON.parse(JSON.stringify(template)) + embed.title = `You have been banned in ${guild.name}!` + embed.color = colors.bad || '#f45450' + return embed + }, + kick: () => { + const embed = JSON.parse(JSON.stringify(template)) + embed.title = `You have been kicked from ${guild.name}!` + embed.color = colors.bad || '#f45450' + return embed + }, + timeout: () => { + const embed = JSON.parse(JSON.stringify(template)) + embed.title = `You have been timed out in ${guild.name}!` + embed.color = colors.medium || '#fdbc40' + embed.fields.splice(1, 0, { + name: 'Duration', + value: info.duration + }) + return embed + }, + untimeout: () => { + const embed = JSON.parse(JSON.stringify(template)) + embed.title = `Your timeout has been removed in ${guild.name}!` + embed.color = colors.good || '#36c84b' + return embed + } + } + const embed = types[type] + if (!embed) return + const dm = await target.send({ embeds: [embed()] }).catch(() => null) + return dm !== null ? true : false +} diff --git a/utils/log.js b/utils/log.js new file mode 100644 index 0000000..54f4bfd --- /dev/null +++ b/utils/log.js @@ -0,0 +1,170 @@ +module.exports = (guild, type, info) => { + const { + customization: { accent, colors }, + channels: { logs } + } = require('../config.json') + const templates = { + moderate: { + title: '', + fields: [ + { + name: 'User', + value: `<@${info.target?.id}>` + }, + { + name: 'Reason', + value: info.reason || 'N/A' + }, + { + name: 'Responsible Moderator', + value: `<@${info.moderator?.id}>` + }, + { + name: 'Time', + value: `\n` + } + ] + }, + channel: { + title: '', + fields: [ + { + name: 'Channel', + value: `<#${info.channel?.id}>` + }, + { + name: 'Responsible Moderator', + value: `<@${info.moderator?.id}>` + }, + { + name: 'Time', + value: `\n` + } + ] + }, + message: { + title: '', + fields: [ + { + name: 'User', + value: `<@${info.target?.id}>` + }, + { + name: 'Message', + value: info.content + }, + { + name: 'Channel', + value: `<#${info.channel?.id}>` + }, + { + name: 'Time', + value: `\n` + } + ] + } + } + const types = { + ban: () => { + const embed = JSON.parse(JSON.stringify(templates.moderate)) + embed.title = `Banned ${info.target.tag}` + embed.color = colors.bad || '#f45450' + return embed + }, + unban: () => { + const embed = JSON.parse(JSON.stringify(templates.moderate)) + embed.title = `Unbanned ${info.target.tag}` + embed.color = colors.good || '#36c84b' + return embed + }, + kick: () => { + const embed = JSON.parse(JSON.stringify(templates.moderate)) + embed.title = `Kicked ${info.target.tag}` + embed.color = colors.bad || '#f45450' + return embed + }, + timeout: () => { + const embed = JSON.parse(JSON.stringify(templates.moderate)) + embed.title = `Timed out ${info.target.tag}` + embed.color = colors.medium || '#fdbc40' + embed.fields.splice(2, 0, { + name: 'Duration', + value: `${info.duration}` + }) + return embed + }, + untimeout: () => { + const embed = JSON.parse(JSON.stringify(templates.moderate)) + embed.title = `Removed timeout for ${info.target.tag}` + embed.color = colors.good || '#36c84b' + return embed + }, + messageDelete: () => { + const embed = JSON.parse(JSON.stringify(templates.message)) + embed.title = `Message deleted by ${ + info.moderator ? 'moderator' : 'user' + }` + embed.color = colors.bad || '#f45450' + if (info.moderator) + embed.fields.splice(2, 0, { + name: 'Responsible Moderator', + value: `<@${info.moderator.id}>` + }) + return embed + }, + messageEdit: () => { + const embed = JSON.parse(JSON.stringify(templates.message)) + embed.title = 'Message edited' + embed.color = colors.medium || '#fdbc40' + embed.fields.splice(1, 0, { + name: 'Old Message', + value: info.oldMessage + }) + return embed + }, + purge: () => { + const embed = JSON.parse(JSON.stringify(templates.channel)) + embed.title = `Purged ${info.amount} message${ + info.amount === 1 ? '' : 's' + }${info.target ? ` by ${info.target.tag}` : ''}` + embed.color = colors.medium || '#fdbc40' + if (info.target) + embed.fields.splice(0, 0, { + name: 'User', + value: `<@${info.target.id}>` + }) + return embed + }, + lock: () => { + const embed = JSON.parse(JSON.stringify(templates.channel)) + embed.title = `Locked #${info.channel.name}` + embed.color = colors.medium || '#fdbc40' + return embed + }, + unlock: () => { + const embed = JSON.parse(JSON.stringify(templates.channel)) + embed.title = `Unlocked #${info.channel.name}` + embed.color = colors.good || '#36c84b' + return embed + }, + phish: () => { + const embed = JSON.parse(JSON.stringify(templates.message)) + embed.title = `Deleted phishing site by ${info.target.tag}` + embed.color = colors.bad || '#f45450' + embed.fields.splice(3, 0, { + name: 'Harmful Site', + value: info.site + }) + return embed + } + } + const embed = types[type] + if (!embed) return + guild.channels.cache.get(logs).send({ embeds: [embed()] }) +} diff --git a/utils/moderation.js b/utils/moderation.js new file mode 100644 index 0000000..8134f92 --- /dev/null +++ b/utils/moderation.js @@ -0,0 +1,80 @@ +const log = require('./log') + +// unused for now, don't look + +async function ban(target, reason, deleteMessages) { + if (!target.bannable) return 'that user is not bannable!' + const ban = await target + .ban(target, { days: deleteMessages, reason: reason }) + .catch(() => false) + if (ban == false) return 'I could not ban that user!' + return true +} + +async function unban(guild, target, reason) { + const bans = await guild.bans.fetch() + if (!bans.has(target.id)) return 'that user is not banned!' + const unban = await guild.members.unban(target, reason).catch(() => false) + if (unban == false) return 'an unknown error occurred!' + return true +} + +async function kick(target, reason) { + if (!target.kickable) return 'that user is not kickable!' + const kick = await target.kick(reason).catch(() => false) + if (kick == false) return 'an unknown error occurred!' + return true +} + +async function timeout(target, reason, duration) { + if (!target.moderatable) return 'that user is not moderatable!' + const timeout = await target.timeout(duration, reason).catch(() => false) + if (timeout == false) return 'an unknown error occurred!' + return true +} + +async function untimeout(target, reason) { + if (!target.moderatable) return 'that user is not moderatable!' + const untimeout = await target.timeout(null, reason).catch(() => false) + if (untimeout == false) return 'an unknown error occurred!' + return true +} + +async function purge(channel, amount, reason, target) { + if (!channel.manageable) return 'that channel is not manageable!' + let messages = await channel.messages + .fetch({ limit: amount }, { cache: false, force: true }) + .catch(() => false) + if (target) messages = messages.filter(m => m.author.id == target.id) + if (messages == false) return 'an unknown error occurred!' + if (messages.size == 0) return 'there are no messages to purge!' + const purge = channel.bulkDelete(messages, reason).catch(() => false) + if (purge == false) return 'an unknown error occurred!' + return true +} + +module.exports = { + checkBotPerms: interaction => { + return require('./checkBotPerms')(interaction) + }, + checkUserPerms: interaction => { + return require('./checkUserPerms')(interaction) + }, + moderate: (moderator, target, reason, type, options) => { + let = true + if (type == 'ban') { + const result = ban(target, reason, options.deleteMessages) + if (result !== true) su + } else if (type == 'kick') { + const result = kick(target, reason) + } else if (type == 'timeout') { + const result = timeout(target, reason, options.duration) + } + log(moderator.guild, type, { + moderator, + reason, + target + }) + }, + unmoderate: (moderator, target, reason, type) => {} +} diff --git a/utils/packagejson.js b/utils/packagejson.js new file mode 100644 index 0000000..8b60489 --- /dev/null +++ b/utils/packagejson.js @@ -0,0 +1,25 @@ +const fs = require('fs') + +function readPackage() { + return JSON.parse(fs.readFileSync('./package.json')) +} + +function getAllDependencies() { + const deps = Object.entries(readPackage().dependencies) + return deps.map(d => ({ + name: d[0], + version: d[1].replace('^', '') + })) +} + +module.exports = { + getDependency: name => { + const all = getAllDependencies() + const found = all.find(d => d.name === name) + if (!found) return null + return found + }, + getVersion: () => { + return readPackage().version + } +} diff --git a/utils/sleep.js b/utils/sleep.js new file mode 100644 index 0000000..50bb37c --- /dev/null +++ b/utils/sleep.js @@ -0,0 +1 @@ +module.exports = ms => new Promise(r => setTimeout(r, ms))