Merge branch 'message-commands' into slash-commands

This commit is contained in:
Erik 2022-07-30 11:14:02 +03:00
commit a88cb49c56
Signed by untrusted user: Navy.gif
GPG Key ID: 811EC0CD80E7E5FB
90 changed files with 1186 additions and 501 deletions

View File

@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"dev": "node --trace-warnings --unhandled-rejections=strict index.js", "dev": "nodemon --delay 10 --trace-warnings --unhandled-rejections=strict index.js",
"debug": "node --trace-warnings --inspect index.js", "debug": "node --trace-warnings --inspect index.js",
"update": "git pull && cd api && yarn update", "update": "git pull && cd api && yarn update",
"test": "jest --detectOpenHandles", "test": "jest --detectOpenHandles",

View File

@ -416,6 +416,8 @@
"seamen", "seamen",
"coming", "coming",
"seaman", "seaman",
"seus",
"seuss",
"rock", "rock",
"dock", "dock",
"lock", "lock",

View File

@ -44,6 +44,9 @@ Global permissions
[COMMAND_PERMISSIONS_SHOW_TITLE] [COMMAND_PERMISSIONS_SHOW_TITLE]
Granted permissions Granted permissions
[COMMAND_PERMISSIONS_DESC]
Showing permissions {forin} **{target}**
[COMMAND_PERMISSIONS_NO_PERMS] [COMMAND_PERMISSIONS_NO_PERMS]
No permissions granted No permissions granted

View File

@ -71,6 +71,9 @@ It seems like the mute role was deleted, use the command `-set mute` for more in
[COMMAND_MUTE_MISSING_MODERATE_PERM] [COMMAND_MUTE_MISSING_MODERATE_PERM]
Missing permissions to timeout users (Moderate Member) Missing permissions to timeout users (Moderate Member)
[COMMAND_MUTE_HIERARCHY_ERROR]
the bot cannot timeout a user above its highest role
[COMMAND_MUTE_MISSING_MANAGEROLE_PERM] [COMMAND_MUTE_MISSING_MANAGEROLE_PERM]
Missing permissions to manage roles. Missing permissions to manage roles.
@ -303,6 +306,10 @@ the provided role(s) are higher than the bot, I cannot add them
the provided role(s) are not on the grantable list the provided role(s) are not on the grantable list
//History Command //History Command
[COMMAND_HISTORY_HELP]
Display moderation history in the server.
Narrow the search down by using the parameters below.
[COMMAND_HISTORY_HISTORY] [COMMAND_HISTORY_HISTORY]
Display moderation history for the server or for certain users. Display moderation history for the server or for certain users.
@ -332,6 +339,9 @@ Failed to display cases in one embed, try a smaller page size.
for {targets} for {targets}
//target{plural}: //target{plural}:
[COMMAND_HISTORY_SUCCESSMODERATOR]
by {moderator}
[COMMAND_HISTORY_NO_EXPORT_PERMS] [COMMAND_HISTORY_NO_EXPORT_PERMS]
Must be admin to export moderation history. Must be admin to export moderation history.

View File

@ -1,3 +1,10 @@
[COMMAND_PING_HELP]
Check if the bot is online.
[COMMAND_AVATAR_HELP]
Display a user's avatar.
Use the member option to display their server avatar.
[COMMAND_AVATAR_FORMATERROR] [COMMAND_AVATAR_FORMATERROR]
Unable to find an avatar with those arguments, try a different size or format. Unable to find an avatar with those arguments, try a different size or format.
@ -37,6 +44,8 @@ You have no active reminders.
The content in your reminder was filtered. The content in your reminder was filtered.
// Poll command // Poll command
[COMMAND_POLL_HELP]
Have the bot send a poll message in a channel with an optional duration.
[COMMAND_POLL_QUESTIONS] [COMMAND_POLL_QUESTIONS]
Please respond with question {number}. Please respond with question {number}.

View File

@ -20,6 +20,9 @@ Required Permissions
Command takes no arguments Command takes no arguments
// Generic setting field names // Generic setting field names
[GENERAL_PREFIX]
》 Prefix
[GENERAL_STATUS] [GENERAL_STATUS]
》 Status 》 Status
@ -131,3 +134,11 @@ switch({toggle}) {
'off'; 'off';
break; break;
} }
[FOR_IN_TOGGLE]
switch({toggle}) {
case true:
'for'; break;
case false:
'in'; break;
}

View File

@ -26,4 +26,4 @@ The command **{command}** requires the __bot__ to have permissions to use.
The command **{command}** can only be run by developers. The command **{command}** can only be run by developers.
[INHIBITOR_GUILDONLY_ERROR] [INHIBITOR_GUILDONLY_ERROR]
The command **{command}** is only available in servers. The command **{command}** is only available in servers.

View File

@ -7,6 +7,9 @@ You can increase the amount of targets by upgrading to premium.
[INFRACTION_ERROR] [INFRACTION_ERROR]
an error occured an error occured
[INFRACTION_PROTECTIONPOSITIONERROR_SAME]
they have the same role position as you
[INFRACTION_PROTECTIONPOSITIONERROR] [INFRACTION_PROTECTIONPOSITIONERROR]
they have hierarchy over you they have hierarchy over you

View File

@ -5,15 +5,24 @@ This is an issue that should be reported to a bot developer.
[O_COMMANDHANDLER_GUILDONLY] [O_COMMANDHANDLER_GUILDONLY]
This command can only be run in servers. This command can only be run in servers.
[O_COMMANDHANDLER_GUILDONLY_OPT]
The option `{option}` is only valid in servers.
[O_COMMANDHANDLER_TYPEINTEGER] [O_COMMANDHANDLER_TYPEINTEGER]
The command option {option} requires an integer between `{min}` and `{max}`. The command option {option} requires an integer between `{min}` and `{max}`.
[O_COMMANDHANDLER_TYPEBOOLEAN]
The command option {option} requires a boolean resolveable (e.g. anything that can be interpreted as true or false)
[O_COMMANDHANDLER_TYPECOMMANDS] [O_COMMANDHANDLER_TYPECOMMANDS]
The command option {option} requires command resolveables (e.g. `command:ping`, `command:history`) The command option {option} requires command resolveables (e.g. `command:ping`, `command:history`)
[O_COMMANDHANDLER_TYPECOMMAND] [O_COMMANDHANDLER_TYPECOMMAND]
The command option {option} requires a command resolveable (e.g. `command:ping`) The command option {option} requires a command resolveable (e.g. `command:ping`)
[O_COMMANDHANDLER_TYPEMODULE]
The command option {option} requires a module resolveable (e.g. moderation)
[O_COMMANDHANDLER_TYPESTRING] [O_COMMANDHANDLER_TYPESTRING]
The command option {option} requires a string. The command option {option} requires a string.
@ -23,6 +32,9 @@ The command option {option} requires a date in the format `YYYY/MM/DD`
[O_COMMANDHANDLER_TYPETIME] [O_COMMANDHANDLER_TYPETIME]
The command option {option} requires a timestring (e.g. 5 min, 2w). The command option {option} requires a timestring (e.g. 5 min, 2w).
[O_COMMANDHANDLER_TYPEUSER]
The command option {option} requires a user.
[O_COMMANDHANDLER_TYPEUSERS] [O_COMMANDHANDLER_TYPEUSERS]
The command option {option} requires users. The command option {option} requires users.
@ -80,3 +92,14 @@ Command Error
[O_COMMANDHANDLER_COMMAND_NORESPONSE] [O_COMMANDHANDLER_COMMAND_NORESPONSE]
Command returned no response. This should not happen and is likely a bug. Command returned no response. This should not happen and is likely a bug.
[O_COMMANDHANDLER_UNRECOGNISED_FLAG]
Unrecognised flag: `{flag}`
See command help for valid flags.
[O_COMMANDHANDLER_UNRECOGNISED_OPTIONS]
Unrecognised options: `{opts}`
See command help for valid options.
[O_COMMANDHANDLER_INVALID_CHOICE]
`{value}` is an invalid choice for **{option}**.
Valid choices are `{choices}`.

View File

@ -1,3 +1,6 @@
[SETTING_TEXTCOMMANDS_HELP]
Enable or disable text commands or change the prefix.
[SETTING_PERMISSIONS_HELP] [SETTING_PERMISSIONS_HELP]
Configure which set of permissions the bot works with. Configure which set of permissions the bot works with.

View File

@ -94,15 +94,17 @@ class ApiClientUtil {
const { guildId } = message; const { guildId } = message;
const evalFunc = (client, { guildId }) => { const evalFunc = (client, { guildId }) => {
const guild = client.guilds.cache.get(guildId); try {
if (!guild) return null; const wrapper = client.getGuildWrapper(guildId);
const wrapper = new client.wrapperClasses.GuildWrapper(client, guild); return wrapper.toJSON();
return wrapper.toJSON(); } catch {
return null;
}
}; };
this.client.logger.debug(`guild-live request - shard: ${message.shard}, message id ${message.id}`); this.client.logger.debug(`guild-live request - shard: ${message.shard}, message id ${message.id}`);
const result = await this.client.shardingManager.broadcastEval(evalFunc, { context: { guildId } }); const result = await this.client.shardingManager.broadcastEval(evalFunc, { context: { guildId } });
const guild = result.find((elem) => elem !== undefined); const guild = result.find((elem) => elem !== null);
return guild; return guild;
} }

View File

@ -70,7 +70,7 @@ class Logger {
const maximumCharacters = Math.max(...Constants.Types.map((t) => t.length)); const maximumCharacters = Math.max(...Constants.Types.map((t) => t.length));
const spacers = maximumCharacters - type.length; const spacers = maximumCharacters - type.length;
const text = `${chalk[color](type)}${" ".repeat(spacers)} ${header} : ${string}`; const text = `${chalk[color](type)}${" ".repeat(spacers)} ${header}: ${string}`;
const strippedText = text.replace(stripRegex, ''); const strippedText = text.replace(stripRegex, '');
console.log(text); //eslint-disable-line no-console console.log(text); //eslint-disable-line no-console
@ -86,6 +86,7 @@ class Logger {
webhook(text, type) { webhook(text, type) {
if (!this._webhook) return;
const message = text.replace(new RegExp(process.env.DISCORD_TOKEN, 'gu'), '<redacted>') const message = text.replace(new RegExp(process.env.DISCORD_TOKEN, 'gu'), '<redacted>')
.replace(new RegExp(username, 'gu'), '<redacted>'); .replace(new RegExp(username, 'gu'), '<redacted>');

View File

@ -3,6 +3,7 @@ const { Routes } = require('discord-api-types/v9');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const hash = require('object-hash'); const hash = require('object-hash');
const { inspect } = require('util');
class SlashCommandManager { class SlashCommandManager {
@ -43,7 +44,7 @@ class SlashCommandManager {
this.client.logger.write('info', `Commands hash: ${cmdHash}, ${guilds.length} out of date`); this.client.logger.write('info', `Commands hash: ${cmdHash}, ${guilds.length} out of date`);
if (!guilds.length) return; if (!guilds.length) return;
const promises = []; const promises = [];
//console.log(JSON.stringify(commands)); //fs.writeFileSync(path.join(process.cwd(), 'commands.json'), JSON.stringify(commands));
for(const guild of guilds) { for(const guild of guilds) {
promises.push(this.rest.put( promises.push(this.rest.put(
Routes.applicationGuildCommands(clientId, guild), Routes.applicationGuildCommands(clientId, guild),
@ -72,6 +73,10 @@ class SlashCommandManager {
str += `${command.name}: `; str += `${command.name}: `;
const options = Object.keys(invalid[key].options); const options = Object.keys(invalid[key].options);
for (const optKey of options) { for (const optKey of options) {
if (!command.options[optKey]) {
this.client.logger.warn(`Missing properties for ${command.name}: ${optKey}\nOptions: ${inspect(command.options)}`);
continue;
}
str += `${command.options[optKey].name}\t`; str += `${command.options[optKey].name}\t`;
} }
str += `\n\n`; str += `\n\n`;

View File

@ -91,8 +91,8 @@ class DiscordClient extends Client {
// this.logger.error(`Uncaught exception:\n${err.stack || err}`); // this.logger.error(`Uncaught exception:\n${err.stack || err}`);
// }); // });
process.on('unhandledRejection', (err, reason) => { process.on('unhandledRejection', (err) => {
this.logger.error(`Unhandled rejection:\n${err?.stack || err}\n${inspect(reason)}`); this.logger.error(`Unhandled rejection:\n${err?.stack || err}`);
}); });
process.on('message', this._handleMessage.bind(this)); process.on('message', this._handleMessage.bind(this));
@ -309,6 +309,14 @@ class DiscordClient extends Client {
} }
format(index, params = {}, opts = {}) {
const {
code = false,
language = 'en_gb'
} = opts;
return this.localeLoader.format(language, index, params, code);
}
getGuildWrapper(id) { getGuildWrapper(id) {
if (this.guildWrappers.has(id)) return this.guildWrappers.get(id); if (this.guildWrappers.has(id)) return this.guildWrappers.get(id);

View File

@ -37,7 +37,7 @@ class RateLimiter {
if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel')); if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel'));
if (!message || !(message instanceof Message)) reject(new Error('Missing message')); if (!message || !(message instanceof Message)) reject(new Error('Missing message'));
if (!channel.permissionsFor(channel.guild.members.me).has('ManageMessages')) reject(new Error('Missing permission ManageMessages')); if (!channel.permissionsFor(this.client.user).has('ManageMessages')) reject(new Error('Missing permission ManageMessages'));
if (!this.deleteQueue[channel.id]) this.deleteQueue[channel.id] = []; if (!this.deleteQueue[channel.id]) this.deleteQueue[channel.id] = [];
this.deleteQueue[channel.id].push({ message, resolve, reject }); this.deleteQueue[channel.id].push({ message, resolve, reject });
@ -106,7 +106,7 @@ class RateLimiter {
if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel.')); if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel.'));
if (!message || !message.length) reject(new Error('Missing message.')); if (!message || !message.length) reject(new Error('Missing message.'));
if (!channel.permissionsFor(channel.guild.members.me).has('SendMessages')) reject(new Error('Missing permission SendMessages')); if (!channel.permissionsFor(this.client.user).has('SendMessages')) reject(new Error('Missing permission SendMessages'));
//Initiate queue //Initiate queue
if (!this.sendQueue[channel.id]) this.sendQueue[channel.id] = []; if (!this.sendQueue[channel.id]) this.sendQueue[channel.id] = [];
@ -173,7 +173,7 @@ class RateLimiter {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel')); if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel'));
if (!channel.permissionsFor(channel.guild.members.me).has('SendMessages')) reject(new Error('Missing permission SendMessages')); if (!channel.permissionsFor(this.client.user).has('SendMessages')) reject(new Error('Missing permission SendMessages'));
if (!message) reject(new Error('Missing message')); if (!message) reject(new Error('Missing message'));
if (limit === null) limit = 15; if (limit === null) limit = 15;

View File

@ -346,7 +346,7 @@ class Resolver {
const match = resolveable.match(id); const match = resolveable.match(id);
const [, ch] = match; const [, ch] = match;
const channel = await this.client.channels.fetch(ch).catch((e) => { }); //eslint-disable-line no-empty, no-empty-function, no-unused-vars const channel = await CM.fetch(ch).catch((e) => { }); //eslint-disable-line no-empty, no-empty-function, no-unused-vars
if (channel && filter(channel)) resolved.push(channel); if (channel && filter(channel)) resolved.push(channel);
} else if (name.test(resolveable)) { } else if (name.test(resolveable)) {

View File

@ -72,7 +72,7 @@ class GuildWrapper {
startedIn = await this.resolveChannel(startedIn); startedIn = await this.resolveChannel(startedIn);
const pollChannel = await this.resolveChannel(channel); const pollChannel = await this.resolveChannel(channel);
if (pollChannel) { if (pollChannel) {
const msg = await pollChannel.messages.fetch(message); const msg = await pollChannel.messages.fetch(message).catch(() => null);
if (msg) { if (msg) {
const { reactions } = msg; const { reactions } = msg;
const reactionEmojis = questions.length ? PollReactions.Multi : PollReactions.Single; const reactionEmojis = questions.length ? PollReactions.Multi : PollReactions.Single;
@ -360,7 +360,7 @@ class GuildWrapper {
} }
get prefix() { get prefix() {
return this._settings.prefix || this.client.prefix; return this._settings.textcommands.prefix || this.client.prefix;
} }
get available() { get available() {

View File

@ -217,7 +217,7 @@ class InteractionWrapper {
} }
isContextMenu() { isContextMenu() {
return this.interaction.isContextMenu(); return this.interaction.isContextMenuCommand();
} }
isSelectMenu() { isSelectMenu() {

View File

@ -118,7 +118,7 @@ class InvokerWrapper {
disableMentions: opts.disableMentions disableMentions: opts.disableMentions
}; };
if (opts.editReply) await this.editReply(data); if (opts.editReply || this.deferred) await this.editReply(data);
else await this.reply(data) //this.channel.send(data) else await this.reply(data) //this.channel.send(data)
.then((msg) => { .then((msg) => {
if (opts.delete) msg.delete(); if (opts.delete) msg.delete();

View File

@ -90,6 +90,7 @@ class MessageWrapper {
async editReply(options) { async editReply(options) {
if (!this._reply) throw new Error('Message not replied to'); if (!this._reply) throw new Error('Message not replied to');
if (!options.allowedMentions) options.allowedMentions = { repliedUser: false }; // Disables the mention in the inline reply
return this._reply.edit(options); return this._reply.edit(options);
} }

View File

@ -5,6 +5,7 @@ class AdministrationModule extends SettingsCommand {
constructor(client) { constructor(client) {
super(client, { super(client, {
name: 'administration', name: 'administration',
aliases: ['admin'],
description: 'Configure the administrative settings', description: 'Configure the administrative settings',
module: 'administration' module: 'administration'
}); });

View File

@ -39,7 +39,8 @@ class ImportCommand extends SlashCommand {
}, { }, {
name: 'overwrite', name: 'overwrite',
description: 'Whether any existing logs should be overwritten by the imports. By default new ones are bumped', description: 'Whether any existing logs should be overwritten by the imports. By default new ones are bumped',
type: 'BOOLEAN' type: 'BOOLEAN',
flag: true, valueOptional: true, defaultValue: true
}] }]
}], }],
clientPermissions: ['ManageWebhooks'] clientPermissions: ['ManageWebhooks']

View File

@ -46,7 +46,7 @@ class ModstatsCommand extends SlashCommand {
const data = await this.client.mongodb.infractions.find(query, { projection: { executor: 1, type: 1 } }); const data = await this.client.mongodb.infractions.find(query, { projection: { executor: 1, type: 1 } });
for (const log of data) { for (const log of data) {
if (log.executor === guild.members.me.id) continue; if (log.executor === this.client.user.id) continue;
if (!result[log.executor]) { if (!result[log.executor]) {
const user = await this.client.resolveUser(log.executor); const user = await this.client.resolveUser(log.executor);
result[log.executor] = { name: user.tag }; result[log.executor] = { name: user.tag };

View File

@ -27,7 +27,8 @@ class PermissionsCommand extends SlashCommand {
name: 'channel', name: 'channel',
description: 'The channel(s) in which this permission is granted to user or role', description: 'The channel(s) in which this permission is granted to user or role',
type: 'TEXT_CHANNELS', type: 'TEXT_CHANNELS',
dependsOn: ['permission'] dependsOn: ['permission'],
flag: true
}, },
{ {
name: 'role', name: 'role',
@ -64,6 +65,7 @@ class PermissionsCommand extends SlashCommand {
name: 'channel', name: 'channel',
description: 'The channel(s) to reset', description: 'The channel(s) to reset',
type: 'TEXT_CHANNELS', type: 'TEXT_CHANNELS',
flag: true
}, },
{ {
name: 'role', name: 'role',
@ -233,6 +235,7 @@ class PermissionsCommand extends SlashCommand {
const fields = []; const fields = [];
let update = false; let update = false;
const target = member?.value || role?.value || channel?.value;
if (member || role) { if (member || role) {
@ -340,6 +343,7 @@ class PermissionsCommand extends SlashCommand {
if(!fields.length) return { index: 'COMMAND_PERMISSIONS_NO_PERMS' }; if(!fields.length) return { index: 'COMMAND_PERMISSIONS_NO_PERMS' };
const embed = { const embed = {
description: target ? interaction.format('COMMAND_PERMISSIONS_DESC', { target: target.tag || target.name || 'any', forin: interaction.format('FOR_IN_TOGGLE', { toggle: target.type === undefined }, { code: true }) }) : '',
title: interaction.format('COMMAND_PERMISSIONS_SHOW_TITLE'), title: interaction.format('COMMAND_PERMISSIONS_SHOW_TITLE'),
fields: [ ] fields: [ ]
}; };

View File

@ -1,4 +1,4 @@
const { SlashCommand, CommandOption } = require("../../../interfaces"); const { SlashCommand } = require("../../../interfaces");
const { Util } = require('../../../../utilities'); const { Util } = require('../../../../utilities');
class SettingsCommand extends SlashCommand { class SettingsCommand extends SlashCommand {
@ -10,11 +10,11 @@ class SettingsCommand extends SlashCommand {
description: 'View settings', description: 'View settings',
options: [ options: [
// Probably add reset options here too // Probably add reset options here too
new CommandOption({ {
name: 'list', name: 'list',
description: 'List available settings', description: 'List available settings',
type: 'SUB_COMMAND' type: 'SUB_COMMAND'
}) }
], ],
memberPermissions: ['ManageGuild'] memberPermissions: ['ManageGuild']
}); });

View File

@ -13,16 +13,20 @@ class EvalCommand extends Command {
name: 'eval', name: 'eval',
aliases: ['e', 'evaluate'], aliases: ['e', 'evaluate'],
restricted: true, restricted: true,
module: 'developer' module: 'developer',
options: [
{ name: 'code' },
{ name: 'async', flag: true, valueOptional: true, defaultValue: true }
]
}); });
} }
async execute(invoker, { parameters: params }) { async execute(invoker, { code, async }) {
const args = {}; // Temporary args until I figure out how I want to implement them const args = {}; // Temporary args until I figure out how I want to implement them
params = params.join(' '); let params = code.value;
if (args.async) params = `(async () => {${params}})()`; if (async?.value) params = `(async () => {${params}})()`;
const { guild, author, member, client } = invoker; //eslint-disable-line no-unused-vars const { guild, author, member, client } = invoker; //eslint-disable-line no-unused-vars
let response = null; let response = null;

View File

@ -17,7 +17,8 @@ class StatsCommand extends SlashCommand {
name: 'log', name: 'log',
type: 'BOOLEAN', type: 'BOOLEAN',
types: ['FLAG'], types: ['FLAG'],
description: 'Logs the output in the console.' description: 'Logs the output in the console.',
flag: true, valueOptional: true, defaultValue: true
} }
], ],
clientPermissions: ['SendMessages', 'EmbedLinks'], clientPermissions: ['SendMessages', 'EmbedLinks'],

View File

@ -15,7 +15,7 @@ class Commands extends SlashCommand {
type: 'MODULE', type: 'MODULE',
description: ['List commands from a specific module'] description: ['List commands from a specific module']
}], }],
memberPermissions: ['ManageGuild'], memberPermissions: [],
guildOnly: true guildOnly: true
}); });
} }

View File

@ -18,7 +18,8 @@ class BanCommand extends ModerationCommand {
type: 'INTEGER', type: 'INTEGER',
description: 'How many days worth of messages to prune', description: 'How many days worth of messages to prune',
minimum: 1, minimum: 1,
maximum: 7 maximum: 7,
flag: true
}, { }, {
name: 'users', name: 'users',
type: 'USERS', type: 'USERS',

View File

@ -23,7 +23,8 @@ class CaseCommand extends SlashCommand {
'Print out more detailed information about the case', 'Print out more detailed information about the case',
'List changes to the case' 'List changes to the case'
], ],
depeondsOn: ['id'] depeondsOn: ['id'],
flag: true, valueOptional: true, defaultValue: true
}], }],
guildOnly: true, guildOnly: true,
showUsage: true, showUsage: true,

View File

@ -24,14 +24,15 @@ class EditCommand extends SlashCommand {
name: 'points', name: 'points',
type: 'INTEGER', type: 'INTEGER',
description: 'New point value for case', description: 'New point value for case',
minimum: 0, maximum: 100 minimum: 0, maximum: 100, flag: true
}, { }, {
name: ['expiration', 'duration'], name: ['expiration', 'duration'],
type: 'TIME', type: 'TIME',
description: [ description: [
'New expiration for points, starts from the time the infraction was issued', 'New expiration for points, starts from the time the infraction was issued',
'Duration if the infraction is timed' 'Duration if the infraction is timed'
] ],
flag: true
}] }]
}); });
} }

View File

@ -24,7 +24,8 @@ class HistoryCommand extends SlashCommand {
options: [{ options: [{
name: ['before', 'after'], name: ['before', 'after'],
type: 'DATE', type: 'DATE',
description: 'Filter by a date, must be in YYYY/MM/DD or YYYY-MM-DD format' description: 'Filter by a date, must be in YYYY/MM/DD or YYYY-MM-DD format',
flag: true
}, { }, {
name: ['verbose', 'oldest', 'export', 'private'], name: ['verbose', 'oldest', 'export', 'private'],
description: [ description: [
@ -33,26 +34,35 @@ class HistoryCommand extends SlashCommand {
'Export the list of infractions', 'Export the list of infractions',
'DM the command response' 'DM the command response'
], ],
type: 'BOOLEAN' type: 'BOOLEAN',
flag: true, valueOptional: true, defaultValue: true
}, { }, {
name: 'type', name: 'type',
description: 'Filter infractions by type', description: 'Filter infractions by type',
choices: Infractions.map((inf) => { choices: Infractions.map((inf) => {
return { name: inf.toLowerCase(), value: inf }; return { name: inf.toLowerCase(), value: inf };
}) }),
flag: true
}, { }, {
name: ['pagesize', 'page'], name: ['pagesize', 'page'],
description: ['Amount of infractions to list per page', 'Page to select'], description: ['Amount of infractions to list per page', 'Page to select'],
type: 'INTEGER', type: 'INTEGER',
minimum: 1 minimum: 1,
flag: true
}, { }, {
name: ['user', 'moderator'], // name: 'user',
description: ['User whose infractions to query, overrides channel if both are given', 'Query by moderator'], description: 'User whose infractions to query, overrides channel if both are given',
type: 'USER' type: 'USER'
}, {
name: 'moderator',
description: 'Query by moderator',
type: 'USER',
flag: true
}, { }, {
name: 'channel', name: 'channel',
description: 'Infractions done on channels, e.g. slowmode, lockdown', description: 'Infractions done on channels, e.g. slowmode, lockdown',
type: 'TEXT_CHANNEL' type: 'TEXT_CHANNEL',
flag: true
}] }]
}); });
} }
@ -95,6 +105,7 @@ class HistoryCommand extends SlashCommand {
limit: pageSize limit: pageSize
}); });
const me = await guild.resolveMember(this.client.user);
const embed = { const embed = {
author: { author: {
name: 'Infraction History', name: 'Infraction History',
@ -104,7 +115,7 @@ class HistoryCommand extends SlashCommand {
footer: { footer: {
text: `• Page ${_page}/${maxPage} | ${resultsAmt} Results` text: `• Page ${_page}/${maxPage} | ${resultsAmt} Results`
}, },
color: invoker.guild.members.me.roles.highest.color color: me.roles.highest.color
}; };
if (invoker.guild._settings.modpoints.enabled) { if (invoker.guild._settings.modpoints.enabled) {
@ -180,12 +191,18 @@ class HistoryCommand extends SlashCommand {
const type = invoker.format('COMMAND_HISTORY_SUCCESSTYPE', { old: oldest?.value || false }, { code: true }); const type = invoker.format('COMMAND_HISTORY_SUCCESSTYPE', { old: oldest?.value || false }, { code: true });
try { try {
let targets = '';
if (user || channel) targets = invoker.format('COMMAND_HISTORY_SUCCESSTARGETS', {
//plural: parsed.length === 1 ? '' : 's',
targets: `**${Util.escapeMarkdown(user?.value.tag || channel?.value.name)}**` //parsed.map((p) => `**${Util.escapeMarkdown(p.display)}**`).join(' ')
});
else if (moderator) targets = invoker.format('COMMAND_HISTORY_SUCCESSMODERATOR', {
moderator: `**${Util.escapeMarkdown(moderator.value.tag)}**`
});
return { return {
content: invoker.format('COMMAND_HISTORY_SUCCESS', { content: invoker.format('COMMAND_HISTORY_SUCCESS', {
targets: user || channel ? invoker.format('COMMAND_HISTORY_SUCCESSTARGETS', { targets,
//plural: parsed.length === 1 ? '' : 's',
targets: `**${Util.escapeMarkdown(user?.value.tag || channel?.value.name)}**` //parsed.map((p) => `**${Util.escapeMarkdown(p.display)}**`).join(' ')
}) : '',
type type
}), }),
emoji: 'success', emoji: 'success',

View File

@ -27,8 +27,9 @@ class MuteCommand extends ModerationCommand {
const { guild } = interaction; const { guild } = interaction;
const settings = await guild.settings(); const settings = await guild.settings();
const { type } = settings.mute; const { type } = settings.mute;
if (type === 3 && !guild.members.me.permissions.has('ModerateMembers')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ModerateMembers' } }); const me = await guild.resolveMember(this.client.user);
else if (!guild.members.me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } }); if (type === 3 && !me.permissions.has('ModerateMembers')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ModerateMembers' } });
else if (!me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } });
return this.client.moderationManager.handleInfraction(Mute, interaction, { return this.client.moderationManager.handleInfraction(Mute, interaction, {
targets: users.value, targets: users.value,

View File

@ -12,7 +12,7 @@ class NicknameCommand extends ModerationCommand {
name: 'name', name: 'name',
description: 'The new nickname to give', description: 'The new nickname to give',
type: 'STRING', type: 'STRING',
required: true required: true, flag: true
}], }],
memberPermissions: ['ManageNicknames'], memberPermissions: ['ManageNicknames'],
clientPermissions: ['ManageNicknames'], clientPermissions: ['ManageNicknames'],

View File

@ -1,6 +1,10 @@
const { ModerationCommand, CommandError } = require('../../../interfaces'); const { ModerationCommand, CommandError } = require('../../../interfaces');
const { Prune } = require('../../../infractions'); const { Prune } = require('../../../infractions');
const flag = true,
valueOptional = true,
defaultValue = true;
class PruneCommand extends ModerationCommand { class PruneCommand extends ModerationCommand {
constructor(client) { constructor(client) {
@ -26,56 +30,69 @@ class PruneCommand extends ModerationCommand {
}, { }, {
name: 'silent', name: 'silent',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Prune quietly' description: 'Prune quietly',
flag, valueOptional, defaultValue
}, { }, {
name: 'bots', name: 'bots',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Prune messages from bots' description: 'Prune messages from bots',
flag, valueOptional, defaultValue
}, { }, {
name: 'humans', name: 'humans',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Prune messages from humans' description: 'Prune messages from humans',
flag, valueOptional, defaultValue
}, { }, {
name: 'contains', name: 'contains',
type: 'STRING', type: 'STRING',
description: 'Text to look for messages by' description: 'Text to look for messages by',
flag
}, { }, {
name: 'startswith', name: 'startswith',
type: 'STRING', type: 'STRING',
description: 'Text the messages to delete start with' description: 'Text the messages to delete start with',
flag
}, { }, {
name: 'endswith', name: 'endswith',
type: 'STRING', type: 'STRING',
description: 'Text the messages to delete end with' description: 'Text the messages to delete end with',
flag
}, { }, {
name: 'text', name: 'text',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Only delete messages containing text' description: 'Only delete messages containing text',
flag, valueOptional, defaultValue
}, { }, {
name: 'invites', name: 'invites',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Delete messages containing invites' description: 'Delete messages containing invites',
flag, valueOptional, defaultValue
}, { }, {
name: 'links', name: 'links',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Delete messages containing links' description: 'Delete messages containing links',
flag, valueOptional, defaultValue
}, { }, {
name: 'emojis', name: 'emojis',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Prune messages cotaining emojis' description: 'Prune messages cotaining emojis',
flag, valueOptional, defaultValue
}, { }, {
name: 'after', name: 'after',
type: 'STRING', type: 'STRING',
description: 'ID of message after which to start deleting' description: 'ID of message after which to start deleting',
flag
}, { }, {
name: 'before', name: 'before',
type: 'STRING', type: 'STRING',
description: 'ID of message before which to start deleting' description: 'ID of message before which to start deleting',
flag
}, { }, {
name: 'logic', name: 'logic',
// type: '', // type: '',
choices: [{ name: 'AND', value: 'AND' }, { name: 'OR', value: 'OR' }], choices: [{ name: 'AND', value: 'AND' }, { name: 'OR', value: 'OR' }],
description: 'Logic type to use for combining options' description: 'Logic type to use for combining options',
flag
}, { }, {
name: 'reason', name: 'reason',
type: 'STRING', type: 'STRING',

View File

@ -22,7 +22,8 @@ class ResolveCommand extends SlashCommand {
}, { }, {
name: 'notify', name: 'notify',
description: 'Attempt to notify the user about the resolve, may not always be possible', description: 'Attempt to notify the user about the resolve, may not always be possible',
type: 'BOOLEAN' type: 'BOOLEAN',
flag: true, valueOptional: true, defaultValue: true
}] // Potentially add another option to enable a range of cases }] // Potentially add another option to enable a range of cases
}); });
} }

View File

@ -27,7 +27,7 @@ class StaffCommand extends SlashCommand {
// const role = await guild.resolveRole(staff.role); // const role = await guild.resolveRole(staff.role);
// if(!role) return invoker.editReply({ index: 'COMMAND_STAFF_ERROR', emoji: 'failure' }); // if(!role) return invoker.editReply({ index: 'COMMAND_STAFF_ERROR', emoji: 'failure' });
await channel.send({ return channel.send({
content: guild.format('COMMAND_STAFF_SUMMON', { author: author.tag, role: staff.role }), content: guild.format('COMMAND_STAFF_SUMMON', { author: author.tag, role: staff.role }),
allowedMentions: { parse: ['roles'] } // roles: [staff.role], allowedMentions: { parse: ['roles'] } // roles: [staff.role],
}); });

View File

@ -23,8 +23,9 @@ class UnmuteCommand extends ModerationCommand {
const { guild } = interaction; const { guild } = interaction;
const settings = await guild.settings(); const settings = await guild.settings();
const { type } = settings.mute; const { type } = settings.mute;
if (type === 3 && !guild.members.me.permissions.has('ModerateMembers')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ModerateMembers' } }); const me = await guild.resolveMember(this.client.user);
else if (!guild.members.me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } }); if (type === 3 && !me.permissions.has('ModerateMembers')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ModerateMembers' } });
else if (!me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } });
return this.client.moderationManager.handleInfraction(Unmute, interaction, { return this.client.moderationManager.handleInfraction(Unmute, interaction, {
targets: users.value, targets: users.value,

View File

@ -12,8 +12,9 @@ const { SlashCommand, Infraction } = require("../../../interfaces");
} }
*/ */
class ResolveCommand extends SlashCommand { class UnresolveCommand extends SlashCommand {
// TODO: make unresolving enact the infraction again
constructor(client) { constructor(client) {
super(client, { super(client, {
name: 'unresolve', name: 'unresolve',
@ -50,4 +51,4 @@ class ResolveCommand extends SlashCommand {
} }
module.exports = ResolveCommand; module.exports = UnresolveCommand;

View File

@ -13,14 +13,16 @@ class AvatarCommand extends SlashCommand {
// type: 'INTEGER', // type: 'INTEGER',
choices: [16, 32, 64, 128, 256, 512, 1024, 2048].map((i) => { choices: [16, 32, 64, 128, 256, 512, 1024, 2048].map((i) => {
return { name: `${i}`, value: `${i}` }; return { name: `${i}`, value: `${i}` };
}) }),
flag: true
}, { }, {
name: 'format', name: 'format',
description: 'Image format', description: 'Image format',
// type: 'STRING' // type: 'STRING'
choices: ['webp', 'png', 'jpeg', 'jpg', 'gif'].map((i) => { choices: ['webp', 'png', 'jpeg', 'jpg', 'gif'].map((i) => {
return { name: i, value: i }; return { name: i, value: i };
}) }),
flag: true
}, { }, {
name: 'user', name: 'user',
description: 'Use this for the user\'s global avatar', description: 'Use this for the user\'s global avatar',
@ -28,7 +30,8 @@ class AvatarCommand extends SlashCommand {
}, { }, {
name: 'member', name: 'member',
description: 'Use this for the user\'s server avatar', description: 'Use this for the user\'s server avatar',
type: 'MEMBER' type: 'MEMBER',
flag: true
}] }]
}); });
} }

View File

@ -57,7 +57,7 @@ class PollCommand extends SlashCommand {
const questions = []; const questions = [];
const _channel = channel?.value || invoker.channel; const _channel = channel?.value || invoker.channel;
const botMissing = _channel.permissionsFor(guild.members.me).missing(['SendMessages', 'EmbedLinks']); const botMissing = _channel.permissionsFor(this.client.user).missing(['SendMessages', 'EmbedLinks']);
const userMissing = _channel.permissionsFor(member).missing(['SendMessages']); const userMissing = _channel.permissionsFor(member).missing(['SendMessages']);
if (botMissing.length) return invoker.editReply({ index: 'COMMAND_POLL_BOT_PERMS', params: { missing: botMissing.join(', '), channel: _channel.id } }); if (botMissing.length) return invoker.editReply({ index: 'COMMAND_POLL_BOT_PERMS', params: { missing: botMissing.join(', '), channel: _channel.id } });
if (userMissing.length) return invoker.editReply({ index: 'COMMAND_POLL_USER_PERMS', params: { missing: userMissing.join(', '), channel: _channel.id } }); if (userMissing.length) return invoker.editReply({ index: 'COMMAND_POLL_USER_PERMS', params: { missing: userMissing.join(', '), channel: _channel.id } });
@ -65,10 +65,10 @@ class PollCommand extends SlashCommand {
for (let i = 0; i < choices.value; i++) { for (let i = 0; i < choices.value; i++) {
const response = await invoker.promptMessage({ const response = await invoker.promptMessage({
content: guild.format(`COMMAND_POLL_QUESTION${choices.value === 1 ? '' : 'S'}`, { number: i + 1 }) + '\n' + guild.format('COMMAND_POLL_ADDENDUM'), content: guild.format(`COMMAND_POLL_QUESTION${choices.value === 1 ? '' : 'S'}`, { number: i + 1 }) + '\n' + guild.format('COMMAND_POLL_ADDENDUM'),
time: 90, editReply: true time: 90, editReply: invoker.replied
}); });
if (!response || !response.content) return invoker.editReply({ index: 'COMMAND_POLL_TIMEOUT' }); if (!response || !response.content) return invoker.editReply({ index: 'COMMAND_POLL_TIMEOUT' });
if(invoker.channel.permissionsFor(guild.members.me).has('ManageMessages')) await response.delete().catch(() => null); if(invoker.channel.permissionsFor(this.client.user).has('ManageMessages')) await response.delete().catch(() => null);
const { content } = response; const { content } = response;
if (content.toLowerCase() === 'stop') break; if (content.toLowerCase() === 'stop') break;
if (content.toLowerCase() === 'cancel') return invoker.editReply({ index: 'GENERAL_CANCELLED' }); if (content.toLowerCase() === 'cancel') return invoker.editReply({ index: 'GENERAL_CANCELLED' });

View File

@ -34,7 +34,8 @@ class SelfroleCommand extends SlashCommand {
const { guild, member } = invoker; const { guild, member } = invoker;
const { selfrole } = await guild.settings(); const { selfrole } = await guild.settings();
if (!selfrole.roles.length) return { index: 'COMMAND_SELFROLE_NONE', emoji: 'failure' }; if (!selfrole.roles.length) return { index: 'COMMAND_SELFROLE_NONE', emoji: 'failure' };
const ownHighest = guild.members.me.roles.highest; const me = await guild.resolveMember(this.client.user);
const ownHighest = me.roles.highest;
const memberRoles = member.roles.cache.map((r) => r.id); const memberRoles = member.roles.cache.map((r) => r.id);
const tooHigh = roles?.value.filter((r) => r.position > ownHighest.position); const tooHigh = roles?.value.filter((r) => r.position > ownHighest.position);

View File

@ -13,7 +13,7 @@ class ClientPermissions extends Inhibitor {
async execute(invoker, command) { async execute(invoker, command) {
const missing = invoker.channel.permissionsFor(invoker.guild.members.me).missing(command.clientPermissions); const missing = invoker.channel.permissionsFor(this.client.user).missing(command.clientPermissions);
if (missing.length) return super._fail({ error: true, missing: missing.join(', '), silent: true }); if (missing.length) return super._fail({ error: true, missing: missing.join(', '), silent: true });
return super._succeed(); return super._succeed();

View File

@ -22,10 +22,10 @@ class AuditLogObserver extends Observer {
} }
async guildBanAdd({ guild, user, guildWrapper: wrapper }) { async guildBanAdd({ user, guildWrapper: wrapper }) {
const settings = await wrapper.settings(); const settings = await wrapper.settings();
if (!settings.moderation.channel || !settings.moderation.infractions.includes('BAN')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. if (!settings.moderation.channel || !settings.moderation.infractions.includes('BAN')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier.
const audit = await this._fetchFirstEntry(guild, user, 'MemberBanAdd'); const audit = await this._fetchFirstEntry(wrapper, user, 'MemberBanAdd');
if (!audit) return undefined; if (!audit) return undefined;
new Infraction(this.client, { new Infraction(this.client, {
type: 'BAN', type: 'BAN',
@ -37,10 +37,10 @@ class AuditLogObserver extends Observer {
}).handle(); }).handle();
} }
async guildBanRemove({ guild, user, guildWrapper: wrapper }) { async guildBanRemove({ user, guildWrapper: wrapper }) {
const settings = await wrapper.settings(); const settings = await wrapper.settings();
if (!settings.moderation.channel || !settings.moderation.infractions.includes('UNBAN')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. if (!settings.moderation.channel || !settings.moderation.infractions.includes('UNBAN')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier.
const audit = await this._fetchFirstEntry(guild, user, 'MemberBanRemove'); const audit = await this._fetchFirstEntry(wrapper, user, 'MemberBanRemove');
if (!audit) return undefined; if (!audit) return undefined;
new Infraction(this.client, { new Infraction(this.client, {
type: 'UNBAN', type: 'UNBAN',
@ -56,7 +56,7 @@ class AuditLogObserver extends Observer {
const { guildWrapper: wrapper } = member; const { guildWrapper: wrapper } = member;
const settings = await wrapper.settings(); const settings = await wrapper.settings();
if (!settings.moderation.channel || !settings.moderation.infractions.includes('KICK')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. if (!settings.moderation.channel || !settings.moderation.infractions.includes('KICK')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier.
const audit = await this._fetchFirstEntry(member.guild, member.user, 'MemberKick'); const audit = await this._fetchFirstEntry(wrapper, member.user, 'MemberKick');
if (!audit) return undefined; if (!audit) return undefined;
new Infraction(this.client, { new Infraction(this.client, {
type: 'KICK', type: 'KICK',
@ -123,7 +123,7 @@ class AuditLogObserver extends Observer {
const mutedRole = settings.mute.role; const mutedRole = settings.mute.role;
if (!mutedRole) return undefined; if (!mutedRole) return undefined;
const audit = await this._fetchFirstEntry(newMember.guild, newMember.user, 'MemberRoleUpdate'); const audit = await this._fetchFirstEntry(wrapper, newMember.user, 'MemberRoleUpdate');
if (!audit) return undefined; if (!audit) return undefined;
let type = null; let type = null;
@ -148,7 +148,8 @@ class AuditLogObserver extends Observer {
} }
async _fetchFirstEntry(guild, user, type, subtype = null) { async _fetchFirstEntry(guild, user, type, subtype = null) {
if (!guild.members.me.permissions.has('ViewAuditLog')) return null; const me = await guild.resolveMember(this.client.user);
if (!me.permissions.has('ViewAuditLog')) return null;
type = AuditLogEvent[type]; type = AuditLogEvent[type];
const audit = await guild.fetchAuditLogs({ limit: 1, type }); const audit = await guild.fetchAuditLogs({ limit: 1, type });
if (audit.entries.size === 0) return null; if (audit.entries.size === 0) return null;

View File

@ -133,7 +133,7 @@ module.exports = class AutoModeration extends Observer {
if (!enabled || roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; if (!enabled || roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return;
const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages'); const missing = channel.permissionsFor(this.client.user).missing('ManageMessages');
if (missing.length) { if (missing.length) {
this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'word', permissions: missing }); this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'word', permissions: missing });
return; return;
@ -432,7 +432,7 @@ module.exports = class AutoModeration extends Observer {
if (roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; if (roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return;
const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages'); const missing = channel.permissionsFor(this.client.user).missing('ManageMessages');
if (missing.length) { if (missing.length) {
this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'link', permissions: missing }); this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'link', permissions: missing });
return; return;
@ -454,9 +454,10 @@ module.exports = class AutoModeration extends Observer {
let log = `${guild.name} Link filter debug:`; let log = `${guild.name} Link filter debug:`;
for (const match of matches) { for (const match of matches) {
const { domain } = match.match(this.regex.linkReg).groups; let { domain } = match.match(this.regex.linkReg).groups;
domain = domain.toLowerCase();
// Invites are filtered separately // Invites are filtered separately
if (domain.toLowerCase() === 'discord.gg') continue; if (domain === 'discord.gg') continue;
log += `\nMatched link ${match}: `; log += `\nMatched link ${match}: `;
const predicate = (dom) => { const predicate = (dom) => {
@ -550,13 +551,13 @@ module.exports = class AutoModeration extends Observer {
const member = message.member || await guild.members.fetch(author.id).catch(() => null); const member = message.member || await guild.members.fetch(author.id).catch(() => null);
const settings = await wrapper.settings(); const settings = await wrapper.settings();
const { invitefilter: setting } = settings; const { invitefilter: setting } = settings;
const { bypass, ignore, actions, silent, enabled, whitelist } = setting; const { bypass, ignore, actions, silent, enabled, whitelist = [] } = setting;
if (!enabled) return; if (!enabled) return;
const roles = member?.roles.cache.map((r) => r.id) || []; const roles = member?.roles.cache.map((r) => r.id) || [];
if (roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; if (roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return;
const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages'); const missing = channel.permissionsFor(this.client.user).missing('ManageMessages');
if (missing.length) { if (missing.length) {
this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'invite', permissions: missing }); this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'invite', permissions: missing });
return; return;
@ -613,7 +614,7 @@ module.exports = class AutoModeration extends Observer {
if (!enabled || roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; if (!enabled || roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return;
const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages'); const missing = channel.permissionsFor(this.client.user).missing('ManageMessages');
if (missing.length) { if (missing.length) {
this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'mention', permissions: missing }); this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'mention', permissions: missing });
return; return;

View File

@ -2,6 +2,9 @@ const { EmbedBuilder, Message, ChannelType, ComponentType, ButtonStyle } = requi
const { Util } = require('../../../utilities'); const { Util } = require('../../../utilities');
const { InvokerWrapper, MessageWrapper } = require('../../client/wrappers'); const { InvokerWrapper, MessageWrapper } = require('../../client/wrappers');
const { Observer, CommandError } = require('../../interfaces/'); const { Observer, CommandError } = require('../../interfaces/');
// const { inspect } = require('util');
const flagReg = /(?:^| )(?<flag>(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu;
class CommandHandler extends Observer { class CommandHandler extends Observer {
@ -29,8 +32,12 @@ class CommandHandler extends Observer {
|| message.author.bot || message.author.bot
|| message.guild && !message.guild.available) return undefined; || message.guild && !message.guild.available) return undefined;
const userWrapper = await this.client.getUserWrapper(message.author.id); const userWrapper = await this.client.getUserWrapper(message.author.id);
if (!userWrapper.developer) return; if (message.guild) {
const settings = await message.guildWrapper.settings();
if (!settings.textcommands.enabled && !userWrapper.developer) return;
}
if(message.guild) { if(message.guild) {
if(!message.member) await message.guild.members.fetch(message.author.id); if(!message.member) await message.guild.members.fetch(message.author.id);
@ -55,7 +62,7 @@ class CommandHandler extends Observer {
// There was an error if _parseResponse return value is truthy, i.e. an error message was sent // There was an error if _parseResponse return value is truthy, i.e. an error message was sent
if (await this._parseResponse(invoker, response)) return; if (await this._parseResponse(invoker, response)) return;
await this._executeCommand(invoker, command.slash ? response.options.args : response.options); await this._executeCommand(invoker, response.options);
} }
@ -79,7 +86,7 @@ class CommandHandler extends Observer {
if (inhibitors.length) return this._generateError(invoker, { type: 'inhibitor', ...inhibitors[0] }); if (inhibitors.length) return this._generateError(invoker, { type: 'inhibitor', ...inhibitors[0] });
await invoker.deferReply(); await invoker.deferReply();
const response = await this._parseInteraction(interaction, command); const response = await this._parseInteraction(invoker, command);
if (await this._parseResponse(invoker, response)) return; if (await this._parseResponse(invoker, response)) return;
try { // Temp logging try { // Temp logging
@ -91,8 +98,31 @@ class CommandHandler extends Observer {
} }
_parseResponse(invoker, response) { _parseResponse(invoker, response) {
// Ensure option dependencies
outer:
if(response.options) for (const opt of Object.values(response.options)) {
let hasDep = false;
for (const dep of opt.dependsOn) {
// AND logic
if (!response.options[dep] && opt.dependsOnMode === 'AND') {
response = { option: opt, error: true, dependency: dep };
break outer;
}
// OR logic
if (response.options[dep]) hasDep = true;
}
if (!hasDep && opt.dependsOnMode === 'OR') {
response = { option: opt, error: true, dependency: opt.dependsOn.join('** OR **') };
break;
}
}
const { command } = invoker; const { command } = invoker;
if (response.error) { if (response.error && response.index) {
if(!response.emoji) response.emoji = 'failure';
return invoker.reply(response);
} else if (response.error) {
let content = invoker.format(`O_COMMANDHANDLER_TYPE${response.option.type}`, { let content = invoker.format(`O_COMMANDHANDLER_TYPE${response.option.type}`, {
option: response.option.name, min: response.option.minimum, max: response.option.maximum option: response.option.name, min: response.option.minimum, max: response.option.maximum
}); });
@ -133,9 +163,7 @@ class CommandHandler extends Observer {
} }
async _executeCommand(invoker, options) { async _executeCommand(invoker, options) {
// TODO defer all replies -- need to go through all commands to ensure they're not deferrign them to avoid errors
// Why? Occasionally some interacitons don't complete in time due to taking longer than normal to reach the bot -- unsure if deferring all replies will fix it
let response = null; let response = null;
const now = Date.now(); const now = Date.now();
if (this.client.developmentMode && !this.client.developers.includes(invoker.user.id)) return invoker.reply({ if (this.client.developmentMode && !this.client.developers.includes(invoker.user.id)) return invoker.reply({
@ -147,7 +175,7 @@ class CommandHandler extends Observer {
let debugstr = invoker.command.name; let debugstr = invoker.command.name;
if (invoker.subcommandGroup) debugstr += ` ${invoker.subcommandGroup.name}`; if (invoker.subcommandGroup) debugstr += ` ${invoker.subcommandGroup.name}`;
if(invoker.subcommand) debugstr += ` ${invoker.subcommand.name}`; if(invoker.subcommand) debugstr += ` ${invoker.subcommand.name}`;
this.logger.info(`${invoker.user.tag} (${invoker.user.id}) is executing ${debugstr} in ${invoker.guild?.name || 'dms'}`); this.logger.info(`[${invoker.type.toUpperCase()}] ${invoker.user.tag} (${invoker.user.id}) is executing ${debugstr} in ${invoker.guild?.name || 'dms'}`);
response = await invoker.command.execute(invoker, options); response = await invoker.command.execute(invoker, options);
invoker.command.success(now); invoker.command.success(now);
} catch (error) { } catch (error) {
@ -174,9 +202,9 @@ class CommandHandler extends Observer {
} }
async _parseInteraction(interaction, command) { async _parseInteraction(invoker, command) {
const { subcommand } = interaction; const { subcommand, guild, target: interaction } = invoker;
let error = null; let error = null;
const options = {}; const options = {};
@ -193,14 +221,11 @@ class CommandHandler extends Observer {
continue; continue;
} }
// const newOption = new CommandOption({ if (matched.guildOnly && !guild) return { error: true, params: { option: matched.name }, index: 'O_COMMANDHANDLER_GUILDONLY_OPT' };
// name: matched.name, type: matched.type, const rawValue = matched.plural && typeof option.value === 'string' ? Util.parseQuotes(option.value).map(([x]) => x) : option.value;
// minimum: matched.minimum, maximum: matched.maximum, const newOption = matched.clone(rawValue, guild, true);
// _rawValue: option.value, const parsed = await newOption.parse();
// dependsOn: matched.dependsOn, dependsOnMode: matched.dependsOnMode // const parsed = await this._parseOption(interaction, newOption);
// });
const newOption = matched.clone(option.value);
const parsed = await this._parseOption(interaction, newOption);
// console.log(parsed); // console.log(parsed);
if(parsed.error) { if(parsed.error) {
@ -211,7 +236,7 @@ class CommandHandler extends Observer {
break; break;
} }
newOption.value = parsed.value; // newOption.value = parsed.value;
options[matched.name] = newOption; options[matched.name] = newOption;
} }
@ -223,18 +248,6 @@ class CommandHandler extends Observer {
if(!options[req.name]) return { option: req, error: true, required: true }; if(!options[req.name]) return { option: req, error: true, required: true };
} }
// Ensure option dependencies
for (const opt of Object.values(options)) {
let hasDep = false;
for (const dep of opt.dependsOn) {
// AND logic
if (!options[dep] && opt.dependsOnMode === 'AND') return { option: opt, error: true, dependency: dep };
// OR logic
if (options[dep]) hasDep = true;
}
if(!hasDep && opt.dependsOnMode === 'OR') return { option: opt, error: true, dependency: opt.dependsOn.join('** OR **') };
}
return { return {
error: false, error: false,
options options
@ -244,13 +257,14 @@ class CommandHandler extends Observer {
async _parseMessage(invoker, params) { async _parseMessage(invoker, params) {
const { command, target: message } = invoker; const { command, target: message, guild } = invoker;
const { subcommands, subcommandGroups } = command; const { subcommands, subcommandGroups } = command;
const args = {}; const args = {};
// console.log(options); // console.log(options);
let group = null, let group = null,
subcommand = null; subcommand = null;
// Parse out subcommands
if (subcommandGroups.length || subcommands.length) { if (subcommandGroups.length || subcommands.length) {
const [first, second, ...rest] = params; const [first, second, ...rest] = params;
group = command.subcommandGroup(first); group = command.subcommandGroup(first);
@ -258,11 +272,11 @@ class CommandHandler extends Observer {
// Depending on how thoroughly I want to support old style commands this might have to try and resolve to other options // Depending on how thoroughly I want to support old style commands this might have to try and resolve to other options
// But for now I'm followin discord's structure for commands // But for now I'm followin discord's structure for commands
if (!group) { if (!group) {
subcommand = command.subcommand(first)?.raw; subcommand = command.subcommand(first);
params = []; params = [];
if (second) params.push(second, ...rest); if (second) params.push(second, ...rest);
} else { } else {
subcommand = command.subcommand(second)?.raw; subcommand = command.subcommand(second);
params = rest; params = rest;
} }
message.subcommand = subcommand; message.subcommand = subcommand;
@ -275,18 +289,166 @@ class CommandHandler extends Observer {
const activeCommand = subcommand || command; const activeCommand = subcommand || command;
const flags = activeCommand.options.filter((opt) => opt.flag); const flags = activeCommand.options.filter((opt) => opt.flag);
for (const flag of flags) { params = Util.parseQuotes(params.join(' ')).map(([x]) => x);
let currentFlag = null;
// console.log('params', params);
// Parse flags
for (let index = 0; index < params.length;) {
// console.log(params[index]);
const match = flagReg.exec(params[index]);
if (!match) {
// console.log('no match', currentFlag?.name);
if (currentFlag) { // Add potential value resolveables to the flag's raw value until next flag is hit, if there is one
if (currentFlag.plural) { // The parse function only parses consecutive values
if (!currentFlag._rawValue) currentFlag._rawValue = [];
currentFlag._rawValue.push(params[index]);
} else {
currentFlag._rawValue = params[index];
currentFlag = null;
}
}
index++;
continue;
}
// console.log('matched');
const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase();
let aliased = false;
const flag = flags.find((f) => {
aliased = f.valueAsAlias && f.choices.some((c) => c.value === _flag);
return f.name === _flag || aliased;
});
if (!flag) return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_FLAG', params: { flag: _flag } };
else if (flag.guildOnly && !guild) return { error: true, params: { option: flag.name }, index: 'O_COMMANDHANDLER_GUILDONLY_OPT' };
// console.log('aliased', aliased);
params.splice(index, 1, null);
if (aliased) {
(args[flag.name] = flag.clone(_flag, guild))._aliased = true;
currentFlag = null;
} else {
currentFlag = flag.clone(null, guild);
args[flag.name] = currentFlag;
}
index++;
// console.log('------------------------------');
}
// Clean up params for option parsing
for (const flag of Object.values(args)) {
// console.log('flags loop', flag.name, flag._rawValue);
const removed = await flag.parse();
if (removed.error) {
if (flag.choices.length) {
return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: flag.name, value: flag._rawValue, choices: flag.choices.map((c) => c.value).join('`, `') } };
}
return { option: flag, ...removed };
}
for(const r of removed) params.splice(params.indexOf(r), 1);
}
// console.log('params', params);
let options = activeCommand.options.filter((opt) => !opt.flag && (opt.type !== 'STRING' || opt.choices.length));
if(!guild) options = options.filter((opt) => !opt.guildOnly);
// const choiceOpts = activeCommand.options.filter((opt) => opt.choices.length);
const stringOpts = activeCommand.options.filter((opt) => !opt.flag && !opt.choices.length && opt.type === 'STRING');
// console.log('non-flag options', options.map((opt) => opt.name));
// Parse out non-flag options
for (const option of options) { // String options are parsed separately at the end
// console.log(1, params);
if (!params.some((param) => param !== null)) break;
// console.log(2);
const cloned = option.clone(null, guild);
let removed = null;
if (cloned.plural) { // E.g. if the type is CHANNEL**S**, parse out any potential channels from the message
// console.log('plural');
cloned._rawValue = params;
removed = await cloned.parse();
} else for (let index = 0; index < params.length;) { // Attempt to parse out a value from each param
// console.log('singular');
if (params[index] === null) {
index++;
continue;
}
cloned._rawValue = params[index];
removed = await cloned.parse();
if (!removed.error) break;
index++;
}
if (removed.error) continue;
args[cloned.name] = cloned;
// Clean up params for string parsing
for (const r of removed) params.splice(params.indexOf(r), 1, null);
} }
return { options: { args, parameters: params }, verbose: true }; const strings = [];
let tmpString = '';
// console.log('strings loop');
// console.log(params);
// Compile strings into groups of strings so we don't get odd looking strings from which options have been parsed out of
for (let index = 0; index < params.length;) {
const str = params[index];
// console.log(str);
if (!str) {
// console.log('null string');
if (tmpString.length) {
// console.log('pushing', tmpString);
strings.push(tmpString);
tmpString = '';
}
index++;
continue;
}
params.splice(index, 1);
tmpString += ` ${str}`;
tmpString = tmpString.trim();
}
// console.log('tmpString', tmpString);
if(tmpString.length) strings.push(tmpString);
// console.log('params after', params);
// console.log('strings', strings);
if (strings.length) for (const strOpt of stringOpts) {
const cloned = strOpt.clone(strings.shift());
// console.log(cloned.name, cloned._rawValue);
await cloned.parse();
args[cloned.name] = cloned;
}
// This part is obsolete now, I think, the string option checks the choice value
for (const arg of Object.values(args)) {
// console.log(arg.name, arg.value);
if (!arg.choices.length) continue;
if (!arg.choices.some((choice) => {
if (typeof arg.value === 'string') return arg.value.toLowerCase() === choice.value.toLowerCase();
return arg.value === choice.value;
})) return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: arg.name, value: arg.value, choices: arg.choices.map((c) => c.value).join('`, `') } };
}
for (const req of activeCommand.options.filter((opt) => opt.required))
if (!args[req.name]) return { option: req, error: true, required: true };
// console.log('parsed args final', Object.values(args).map((arg) => `\n${arg.name}: ${arg.value}, ${inspect(arg._rawValue)}`).join(''));
if (strings.length) return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_OPTIONS', params: { opts: strings.join('`, `') } };
return { options: args, verbose: true };
} }
// Should be unnecessary -- moved to commandoption
async _parseOption(interaction, option) { async _parseOption(interaction, option) {
const { guild } = interaction; const { guild } = interaction;
const types = { const types = {
POINTS: (value) => {
return { value };
},
ROLES: async (string) => { ROLES: async (string) => {
const args = Util.parseQuotes(string).map(([str]) => str); const args = Util.parseQuotes(string).map(([str]) => str);
const roles = await guild.resolveRoles(args); const roles = await guild.resolveRoles(args);
@ -372,6 +534,7 @@ class CommandHandler extends Observer {
return { error: false, value: parseInt(integer) }; return { error: false, value: parseInt(integer) };
}, },
BOOLEAN: (boolean) => { BOOLEAN: (boolean) => {
boolean = this.client.resolver.resolveBoolean(boolean);
return { error: false, value: boolean }; return { error: false, value: boolean };
}, },
MEMBER: async (user) => { MEMBER: async (user) => {
@ -430,7 +593,7 @@ class CommandHandler extends Observer {
async _getCommand(message) { async _getCommand(message) {
if (!this._mentionPattern) this._mentionPattern = new RegExp(`^(<@!?${this.client.user.id}>)`, 'iu'); if (!this._mentionPattern) this._mentionPattern = new RegExp(`^(<@!?${this.client.user.id}>)`, 'iu');
const [arg1, arg2, ...args] = message.content.split(' '); const [arg1, arg2, ...args] = message.content.split(' ').filter((str) => str.length);
if (message.guild) await message.guild.settings(); if (message.guild) await message.guild.settings();
const userWrapper = await this.client.getUserWrapper(message.author.id); const userWrapper = await this.client.getUserWrapper(message.author.id);

View File

@ -1,5 +1,5 @@
/* eslint-disable no-labels */ /* eslint-disable no-labels */
const { WebhookClient, AttachmentBuilder } = require('discord.js'); const { WebhookClient, AttachmentBuilder, AuditLogEvent } = require('discord.js');
const { stripIndents } = require('common-tags'); const { stripIndents } = require('common-tags');
const moment = require('moment'); const moment = require('moment');
const { inspect } = require('util'); const { inspect } = require('util');
@ -103,22 +103,23 @@ class GuildLogger extends Observer {
const hook = await guild.getWebhook('messages'); const hook = await guild.getWebhook('messages');
if (!hook) { if (!hook) {
this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); // this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`);
return this.client.emit('logError', { guild, logger: 'threadLogger', reason: 'MSGLOG_NO_HOOK' }); return this.client.emit('logError', { guild, logger: 'threadLogger', reason: 'MSGLOG_NO_HOOK' });
} }
let actor = null; let actor = null;
const auditLogPerm = guild.members.me.permissions.has('ViewAuditLog'); const me = await this.client.resolver.resolveMember(this.client.user, null, guild);
const auditLogPerm = me.permissions.has('ViewAuditLog');
if (type === 'CREATE') actor = owner; if (type === 'CREATE') actor = owner;
else if (type === 'DELETE' && auditLogPerm) { else if (type === 'DELETE' && auditLogPerm) {
const auditLogs = await guild.fetchAuditLogs({ type: 'THREAD_DELETE', limit: 1 }); const auditLogs = await guild.fetchAuditLogs({ type: AuditLogEvent.ThreadDelete, limit: 1 });
const log = auditLogs.entries.first(); const log = auditLogs.entries.first();
if (log) { if (log) {
if (thread.id !== log.target.id) return; if (thread.id !== log.target.id) return;
actor = log.executor; actor = log.executor;
} }
} else if (['ARCHIVE', 'UNARCHIVE'].includes(type) && auditLogPerm) { } else if (['ARCHIVE', 'UNARCHIVE'].includes(type) && auditLogPerm) {
const auditLogs = await guild.fetchAuditLogs({ type: 'THREAD_UPDATE', limit: 1 }); const auditLogs = await guild.fetchAuditLogs({ type: AuditLogEvent.ThreadUpdate, limit: 1 });
const log = auditLogs.entries.first(); const log = auditLogs.entries.first();
if (log) { if (log) {
if (thread.id !== log.target.id) return; if (thread.id !== log.target.id) return;
@ -178,7 +179,7 @@ class GuildLogger extends Observer {
const hook = await wrapper.getWebhook('messages'); const hook = await wrapper.getWebhook('messages');
if (!hook) { if (!hook) {
this.logger.debug(`Missing messageLog hook in ${wrapper.name} (${wrapper.id})`); // this.logger.debug(`Missing messageLog hook in ${wrapper.name} (${wrapper.id})`);
return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' }); return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' });
} }
@ -340,13 +341,13 @@ class GuildLogger extends Observer {
const { ignore, bypass } = chatlogs; const { ignore, bypass } = chatlogs;
if (ignore.includes(channel.id)) return; if (ignore.includes(channel.id)) return;
const missing = logChannel.permissionsFor(guild.members.me).missing(['ViewChannel', 'EmbedLinks', 'SendMessages', 'ManageWebhooks']); const missing = logChannel.permissionsFor(this.client.user).missing(['ViewChannel', 'EmbedLinks', 'SendMessages', 'ManageWebhooks']);
if (missing.length) if (missing.length)
return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_PERMS', params: { missing: missing.join(', ') } }); return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_PERMS', params: { missing: missing.join(', ') } });
const hook = await wrapper.getWebhook('messages'); const hook = await wrapper.getWebhook('messages');
if (!hook) { if (!hook) {
this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); // this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`);
return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' }); return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' });
} }
@ -364,6 +365,7 @@ class GuildLogger extends Observer {
content = Util.escapeMarkdown(content); content = Util.escapeMarkdown(content);
if (author.bot) continue; if (author.bot) continue;
// TODO: Apparently the cache for this doesn't work properly and the bot hits rate limits occasionally, make own cache at some point
if (!member || member.partial) member = await guild.members.fetch(message.author.id).catch(() => { if (!member || member.partial) member = await guild.members.fetch(message.author.id).catch(() => {
return false; return false;
}); });
@ -533,7 +535,7 @@ class GuildLogger extends Observer {
const hook = await wrapper.getWebhook('messages'); const hook = await wrapper.getWebhook('messages');
if (!hook) { if (!hook) {
this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`); // this.logger.debug(`Missing messageLog hook in ${guild.name} (${guild.id})`);
return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' }); return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_HOOK' });
} }
@ -710,7 +712,7 @@ class GuildLogger extends Observer {
async memberLeave(member) { async memberLeave(member) {
const { guild, guildWrapper: wrapper } = member; const { guildWrapper: wrapper } = member;
const settings = await wrapper.settings(); const settings = await wrapper.settings();
const setting = settings.members; const setting = settings.members;
if (!setting.channel || !setting.enabled) return; if (!setting.channel || !setting.enabled) return;
@ -732,7 +734,7 @@ class GuildLogger extends Observer {
if (oldMember.nickname === newMember.nickname) return; if (oldMember.nickname === newMember.nickname) return;
const { guild, user, guildWrapper: wrapper } = oldMember; const { user, guildWrapper: wrapper } = oldMember;
const settings = await wrapper.settings(); const settings = await wrapper.settings();
const setting = settings.nicknames; const setting = settings.nicknames;
if (!setting.channel || !setting.enabled) return; if (!setting.channel || !setting.enabled) return;

View File

@ -0,0 +1,31 @@
const { Observer } = require("../../interfaces");
class Metrics extends Observer {
constructor(client) {
super(client, {
name: 'metrics',
priority: 10,
disabled: false
});
this.hooks = [
['apiRequest', this.request.bind(this)],
['apiResponse', this.response.bind(this)]
];
this.cache = {};
}
async request(request) {
// console.log(request);
}
async response(request, response) {
// console.log(response, await response.json(), response.status);
}
}
module.exports = Metrics;

View File

@ -51,7 +51,8 @@ class UtilityHook extends Observer {
const { guildWrapper: guild } = member; const { guildWrapper: guild } = member;
const settings = await guild.settings(); const settings = await guild.settings();
const setting = settings.mute; const setting = settings.mute;
if (!guild.members.me.permissions.has('ManageRoles')) return; const me = await guild.resolveMember(this.client.user);
if (!me.permissions.has('ManageRoles')) return;
const infraction = await this.client.storageManager.mongodb.infractions.findOne({ const infraction = await this.client.storageManager.mongodb.infractions.findOne({
duration: { $gt: 0 }, duration: { $gt: 0 },
@ -79,7 +80,7 @@ class UtilityHook extends Observer {
if (managed) { if (managed) {
await member.roles.add(setting.role, 'automute upon rejoin, type 1'); await member.roles.add(setting.role, 'automute upon rejoin, type 1');
await member.roles.remove(remove, 'removing excess roles for type 1 mute'); await member.roles.remove(remove, 'removing excess roles for type 1 mute');
} else await member.roles.set(setting.role, 'automute upon join, type 1'); } else await member.roles.set([role], 'automute upon join, type 1');
} else if (infraction.data.muteType === 2) { } else if (infraction.data.muteType === 2) {
@ -100,7 +101,8 @@ class UtilityHook extends Observer {
const settings = await guild.settings(); const settings = await guild.settings();
const setting = settings.stickyrole; const setting = settings.stickyrole;
if (!setting.roles.length || guild.premium < 1) return; if (!setting.roles.length || guild.premium < 1) return;
if (!guild.members.me.permissions.has('ManageRoles')) return; const me = await guild.resolveMember(this.client.user);
if (!me.permissions.has('ManageRoles')) return;
const data = await this.client.storageManager.mongodb.role_cache.findOne({ guild: guild.id, member: member.id }); const data = await this.client.storageManager.mongodb.role_cache.findOne({ guild: guild.id, member: member.id });
if (!data) return; if (!data) return;
@ -115,7 +117,8 @@ class UtilityHook extends Observer {
const settings = await guild.settings(); const settings = await guild.settings();
const setting = settings.autorole; const setting = settings.autorole;
if (!setting.enabled) return; if (!setting.enabled) return;
if (!guild.members.me.permissions.has('ManageRoles')) return; const me = await guild.resolveMember(this.client.user);
if (!me.permissions.has('ManageRoles')) return;
const _roles = await guild.resolveRoles(setting.roles); const _roles = await guild.resolveRoles(setting.roles);
const roles = _roles.map((r) => r.id); const roles = _roles.map((r) => r.id);
@ -153,7 +156,8 @@ class UtilityHook extends Observer {
const { guild } = invite; const { guild } = invite;
if (!guild) return; if (!guild) return;
if (!guild.members.me.permissions.has('ManageGuild')) return; const me = await this.client.resolver.resolveMember(this.client.user, null, guild);
if (!me.permissions.has('ManageGuild')) return;
if (!guild.invites) guild.invites = await guild.fetchInvites(); if (!guild.invites) guild.invites = await guild.fetchInvites();
guild.invites.set(invite.code, invite); guild.invites.set(invite.code, invite);
@ -163,7 +167,8 @@ class UtilityHook extends Observer {
const { guild } = invite; const { guild } = invite;
if (!guild) return; if (!guild) return;
if (!guild.members.me.permissions.has('ManageGuild')) return; const me = await this.client.resolver.resolveMember(this.client.user, null, guild);
if (!me.permissions.has('ManageGuild')) return;
if (!guild.invites) guild.invites = await guild.fetchInvites(); if (!guild.invites) guild.invites = await guild.fetchInvites();
guild.invites.delete(invite.code); guild.invites.delete(invite.code);
@ -203,7 +208,8 @@ class UtilityHook extends Observer {
!selfrole.channel || selfrole.channel !== channel.id || !selfrole.channel || selfrole.channel !== channel.id ||
!selfrole.roles.length) return; !selfrole.roles.length) return;
const missing = guild.members.me.permissions.missing(['ManageRoles']); const me = await guild.resolveMember(this.client.user);
const missing = me.permissions.missing(['ManageRoles']);
if (missing.length) if (missing.length)
return this.client.emit('utilityError', { guild, utility: 'selfrole', reason: 'UTILITY_SELFROLE_PERMS', params: { missing: missing.join(', ') } }); return this.client.emit('utilityError', { guild, utility: 'selfrole', reason: 'UTILITY_SELFROLE_PERMS', params: { missing: missing.join(', ') } });

View File

@ -1,5 +1,5 @@
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
class IgnoreSetting extends Setting { class IgnoreSetting extends Setting {
@ -19,7 +19,7 @@ class IgnoreSetting extends Setting {
bypass: { ARRAY: 'GUILD_ROLE' } bypass: { ARRAY: 'GUILD_ROLE' }
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'list', name: 'list',
description: 'List to act on', description: 'List to act on',
type: 'STRING', type: 'STRING',
@ -27,9 +27,9 @@ class IgnoreSetting extends Setting {
{ name: 'channels', value: 'channels' }, { name: 'channels', value: 'channels' },
{ name: 'bypass', value: 'bypass' } { name: 'bypass', value: 'bypass' }
], ],
dependsOn: ['method'] dependsOn: ['method']//, valueAsAlias: true, flag: true
}), },
new CommandOption({ {
name: 'method', name: 'method',
description: 'Method of modifying', description: 'Method of modifying',
type: 'STRING', type: 'STRING',
@ -39,8 +39,8 @@ class IgnoreSetting extends Setting {
{ name: 'set', value: 'set' }, { name: 'set', value: 'set' },
{ name: 'reset', value: 'reset' }, { name: 'reset', value: 'reset' },
], ],
dependsOn: ['list'] dependsOn: ['list']//, valueAsAlias: true, flag: true
}) }
] ]
}); });

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require('../../../interfaces/'); const { Setting } = require('../../../interfaces/');
class PermissionType extends Setting { class PermissionType extends Setting {
@ -16,15 +16,16 @@ class PermissionType extends Setting {
type: 'STRING' type: 'STRING'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'type', name: 'type',
description: 'Where to read permissions from',
choices: [ choices: [
{ name: 'discord', value: 'discord' }, { name: 'discord', value: 'discord' },
{ name: 'both', value: 'both' }, { name: 'both', value: 'both' },
{ name: 'grant', value: 'grant' } { name: 'grant', value: 'grant' }
] ]
}) }
] ]
}); });

View File

@ -1,5 +1,5 @@
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
class ProtectionSetting extends Setting { class ProtectionSetting extends Setting {
@ -21,7 +21,7 @@ class ProtectionSetting extends Setting {
enabled: 'BOOLEAN' enabled: 'BOOLEAN'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'type', name: 'type',
description: 'Select protection type', description: 'Select protection type',
@ -30,8 +30,8 @@ class ProtectionSetting extends Setting {
{ name: 'position', value: 'position' } { name: 'position', value: 'position' }
], ],
dependsOn: [] dependsOn: []
}), },
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'roles', name: 'roles',
description: 'Method of modifying', description: 'Method of modifying',
@ -42,12 +42,12 @@ class ProtectionSetting extends Setting {
{ name: 'reset', value: 'reset' }, { name: 'reset', value: 'reset' },
], ],
dependsOn: [] dependsOn: []
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN',
name: 'enabled', name: 'enabled',
description: 'Whether setting is active or not' description: 'Whether setting is active or not'
}) }
] ]
}); });

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
class SilentSetting extends Setting { class SilentSetting extends Setting {
@ -17,11 +17,11 @@ class SilentSetting extends Setting {
enabled: 'BOOLEAN' enabled: 'BOOLEAN'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled', name: 'enabled',
description: 'Toggle state' description: 'Toggle state'
}) }
] ]
}); });

View File

@ -0,0 +1,55 @@
const { Setting } = require("../../../interfaces");
class TextCommands extends Setting {
constructor(client) {
super(client, {
name: 'textcommands',
display: 'Text Commands',
description: 'Message based commands configuration',
module: 'administration',
default: {
enabled: false,
prefix: '-'
},
commandOptions: [
{
name: 'enabled',
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
description: 'Toggle enable state'
},
{
name: 'prefix',
type: 'STRING',
description: 'Prefix to use'
}
]
});
}
async execute(invoker, { enabled, prefix }, setting) {
if (enabled) setting.enabled = enabled.value;
if (prefix) setting.prefix = prefix.value;
return { index: 'SETTING_SUCCESS_ALT' };
}
fields(guild) {
const setting = guild._settings[this.name];
return [{
name: 'GENERAL_STATUS',
value: guild.format('GENERAL_STATE', {
bool: setting.enabled
}, { code: true }),
inline: true
}, {
name: 'GENERAL_PREFIX',
value: setting.prefix,
inline: true
}];
}
}
module.exports = TextCommands;

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
const Infractions = [ const Infractions = [
'NOTE', 'NOTE',
'WARN', 'WARN',
@ -57,13 +57,13 @@ class DmInfraction extends Setting {
} }
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'message', name: 'message',
description: 'Set the message for an infraction type', description: 'Set the message for an infraction type',
type: 'STRING', type: 'STRING',
dependsOn: ['infraction'] dependsOn: ['infraction']
}), },
new CommandOption({ {
name: 'infraction', name: 'infraction',
description: 'Choose the infraction for which to modify the message', description: 'Choose the infraction for which to modify the message',
type: 'STRING', type: 'STRING',
@ -71,8 +71,8 @@ class DmInfraction extends Setting {
return { name: inf, value: inf }; return { name: inf, value: inf };
}), }),
dependsOn: ['message'] dependsOn: ['message']
}), },
new CommandOption({ {
name: 'infractions', name: 'infractions',
description: 'Modify the list of infractions that are sent', description: 'Modify the list of infractions that are sent',
type: 'STRING', type: 'STRING',
@ -82,15 +82,15 @@ class DmInfraction extends Setting {
{ name: 'set', value: 'set' }, { name: 'set', value: 'set' },
{ name: 'reset', value: 'reset' }, { name: 'reset', value: 'reset' },
] ]
}), },
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Enable or disable the sending of infractions in DMs', description: 'Enable or disable the sending of infractions in DMs',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
{ {
name: 'anonymous', name: 'anonymous',
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
description: 'Whether who issued the infraction is shown in moderation logs' description: 'Whether who issued the infraction is shown in moderation logs'
} }
] ]

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
class MessageLog extends Setting { class MessageLog extends Setting {
@ -22,16 +22,16 @@ class MessageLog extends Setting {
types: { ARRAY: 'ERROR_TYPES' } // TODO: Error types types: { ARRAY: 'ERROR_TYPES' } // TODO: Error types
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'channel', name: 'channel',
description: 'Channel in which to output logs', description: 'Channel in which to output logs',
type: 'TEXT_CHANNEL' type: 'TEXT_CHANNEL'
}), },
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Toggle logging on or off', description: 'Toggle logging on or off',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}) }
] ]
}); });
@ -39,14 +39,12 @@ class MessageLog extends Setting {
async execute(interaction, opts, setting) { async execute(interaction, opts, setting) {
const { guild } = interaction;
if (opts.enabled?.value === false) setting.channel = null; if (opts.enabled?.value === false) setting.channel = null;
if (opts.channel) { if (opts.channel) {
const channel = opts.channel.value; const channel = opts.channel.value;
const perms = channel.permissionsFor(guild.members.me); const perms = channel.permissionsFor(this.client.user);
const missingPerms = perms.missing(['ViewChannel', 'EmbedLinks', 'SendMessages']); const missingPerms = perms.missing(['ViewChannel', 'EmbedLinks', 'SendMessages']);
if (missingPerms.length) return { if (missingPerms.length) return {
error: true, error: true,

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
class MemberLog extends Setting { class MemberLog extends Setting {
@ -21,26 +21,26 @@ class MemberLog extends Setting {
leave: 'STRING' leave: 'STRING'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Enable/disable member logs', description: 'Enable/disable member logs',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'channel', name: 'channel',
description: 'Select the log output channel', description: 'Select the log output channel',
type: 'TEXT_CHANNEL' type: 'TEXT_CHANNEL'
}), },
new CommandOption({ {
name: 'join', name: 'join',
description: 'Set the join message', description: 'Set the join message',
type: 'STRING' type: 'STRING', flag: true
}), },
new CommandOption({ {
name: 'leave', name: 'leave',
description: 'Set the leave message', description: 'Set the leave message',
type: 'STRING' type: 'STRING', flag: true
}) }
] ]
}); });

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
const { Util } = require('../../../../utilities'); const { Util } = require('../../../../utilities');
class MessageLog extends Setting { class MessageLog extends Setting {
@ -26,22 +26,22 @@ class MessageLog extends Setting {
attachments: 'BOOLEAN' attachments: 'BOOLEAN'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'channel', name: 'channel',
description: 'Channel in which to output logs', description: 'Channel in which to output logs',
type: 'TEXT_CHANNEL' type: 'TEXT_CHANNEL'
}), },
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Toggle logging on or off', description: 'Toggle logging on or off',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'attachments', name: 'attachments',
description: 'Whether to log attachments. PREMIUM TIER 1', description: 'Whether to log attachments. PREMIUM TIER 1',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'list', name: 'list',
description: 'Select which list to modify', description: 'Select which list to modify',
type: 'STRING', type: 'STRING',
@ -50,8 +50,8 @@ class MessageLog extends Setting {
{ name: 'ignore', value: 'ignore' }, { name: 'ignore', value: 'ignore' },
], ],
dependsOn: ['method'] dependsOn: ['method']
}), },
new CommandOption({ {
name: 'method', name: 'method',
description: 'Select which modification method to use', description: 'Select which modification method to use',
type: 'STRING', type: 'STRING',
@ -62,7 +62,7 @@ class MessageLog extends Setting {
{ name: 'reset', value: 'reset' }, { name: 'reset', value: 'reset' },
], ],
dependsOn: ['list'] dependsOn: ['list']
}), },
] ]
}); });
@ -82,7 +82,7 @@ class MessageLog extends Setting {
if (opts.channel) { if (opts.channel) {
const channel = opts.channel.value; const channel = opts.channel.value;
const perms = channel.permissionsFor(guild.members.me); const perms = channel.permissionsFor(this.client.user);
const missingPerms = perms.missing(['ViewChannel', 'EmbedLinks', 'SendMessages', 'ManageWebhooks']); const missingPerms = perms.missing(['ViewChannel', 'EmbedLinks', 'SendMessages', 'ManageWebhooks']);
if (missingPerms.length) return { if (missingPerms.length) return {
error: true, error: true,

View File

@ -1,5 +1,5 @@
const { Infractions } = require("../../../../constants/Constants"); const { Infractions } = require("../../../../constants/Constants");
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
// [ // [
// 'NOTE', // 'NOTE',
@ -44,17 +44,17 @@ class ModerationLog extends Setting {
} }
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Enable/disable member logs', description: 'Enable/disable member logs',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'channel', name: 'channel',
description: 'Logging channel', description: 'Logging channel',
type: 'TEXT_CHANNEL' type: 'TEXT_CHANNEL'
}), },
new CommandOption({ {
name: 'infractions', name: 'infractions',
description: 'Modify the list of infractions that are sent', description: 'Modify the list of infractions that are sent',
type: 'STRING', type: 'STRING',
@ -64,10 +64,10 @@ class ModerationLog extends Setting {
{ name: 'set', value: 'set' }, { name: 'set', value: 'set' },
{ name: 'reset', value: 'reset' }, { name: 'reset', value: 'reset' },
] ]
}), },
{ {
name: 'anonymous', name: 'anonymous',
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
description: 'Whether who issued the infraction is shown in moderation logs' description: 'Whether who issued the infraction is shown in moderation logs'
} }
] ]

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
class Nicknames extends Setting { class Nicknames extends Setting {
@ -16,16 +16,16 @@ class Nicknames extends Setting {
channel: 'GUILD_TEXT' channel: 'GUILD_TEXT'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Toggle logging on or off', description: 'Toggle logging on or off',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'channel', name: 'channel',
type: 'TEXT_CHANNEL', type: 'TEXT_CHANNEL',
description: 'Set the channel for nickname logging' description: 'Set the channel for nickname logging'
}) }
] ]
}); });
} }

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
class Voice extends Setting { class Voice extends Setting {
@ -15,16 +15,16 @@ class Voice extends Setting {
channel: 'GUILD_TEXT' channel: 'GUILD_TEXT'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Toggle logging on or off', description: 'Toggle logging on or off',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'channel', name: 'channel',
type: 'TEXT_CHANNEL', type: 'TEXT_CHANNEL',
description: 'Set the channel for voice join/leave logging' description: 'Set the channel for voice join/leave logging'
}) }
] ]
}); });
} }

View File

@ -1,5 +1,5 @@
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
const Infractions = [ const Infractions = [
'WARN', 'WARN',
'MUTE', 'MUTE',
@ -17,7 +17,7 @@ class Automod extends Setting {
super(client, { super(client, {
name: 'automod', name: 'automod',
description: 'Define automatic infraction escalation', description: 'Define automatic infraction escalation',
display: 'Automatic Moderation', display: 'Automod',
module: 'moderation', module: 'moderation',
default: { default: {
enabled: false, enabled: false,
@ -36,17 +36,17 @@ class Automod extends Setting {
} }
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Toggle state', description: 'Toggle state',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'useprevious', name: 'useprevious',
description: 'Use the previously passed threshold if the point total lands between two thresholds', description: 'Use the previously passed threshold if the point total lands between two thresholds',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'threshold', name: 'threshold',
description: 'The threshold at which to issue an infraction', description: 'The threshold at which to issue an infraction',
type: 'INTEGER', type: 'INTEGER',
@ -54,8 +54,8 @@ class Automod extends Setting {
maximum: 100, maximum: 100,
dependsOn: ['infraction', 'length'], dependsOn: ['infraction', 'length'],
dependsOnMode: 'OR' dependsOnMode: 'OR'
}), },
new CommandOption({ {
name: 'infraction', name: 'infraction',
description: 'The type of infraction to issue', description: 'The type of infraction to issue',
type: 'STRING', type: 'STRING',
@ -63,13 +63,13 @@ class Automod extends Setting {
return { name: inf, value: inf }; return { name: inf, value: inf };
}), }),
dependsOn: ['threshold'] dependsOn: ['threshold']
}), },
new CommandOption({ {
name: 'length', name: 'length',
description: 'The duration for a tempban or a mute', description: 'The duration for a tempban or a mute',
type: 'TIME', type: 'TIME',
dependsOn: ['threshold'] dependsOn: ['threshold']
}) }
] ]
}); });
} }

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
class Grantable extends Setting { class Grantable extends Setting {
@ -18,12 +18,12 @@ class Grantable extends Setting {
enabled: 'BOOLEAN' enabled: 'BOOLEAN'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled', name: 'enabled',
description: 'Toggle state' description: 'Toggle state'
}), },
new CommandOption({ {
name: 'roles', name: 'roles',
description: '', description: '',
type: 'STRING', type: 'STRING',
@ -33,7 +33,7 @@ class Grantable extends Setting {
{ name: 'set', value: 'set' }, { name: 'set', value: 'set' },
{ name: 'reset', value: 'reset' }, { name: 'reset', value: 'reset' },
] ]
}) }
] ]
}); });
} }

View File

@ -1,4 +1,4 @@
const { FilterSetting, CommandOption } = require('../../../interfaces/'); const { FilterSetting } = require('../../../interfaces/');
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
class InviteFilterSetting extends FilterSetting { class InviteFilterSetting extends FilterSetting {
@ -36,7 +36,7 @@ class InviteFilterSetting extends FilterSetting {
actions: { ARRAY: 'ACTION' } actions: { ARRAY: 'ACTION' }
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'method', name: 'method',
description: 'Select which modification method to use', description: 'Select which modification method to use',
@ -49,8 +49,8 @@ class InviteFilterSetting extends FilterSetting {
{ name: 'list', value: 'list' } { name: 'list', value: 'list' }
], ],
dependsOn: ['list'] dependsOn: ['list']
}), },
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'list', name: 'list',
description: 'Select which list to modify', description: 'Select which list to modify',
@ -61,17 +61,17 @@ class InviteFilterSetting extends FilterSetting {
{ name: 'actions', value: 'actions' }, { name: 'actions', value: 'actions' },
], ],
dependsOn: ['method'] dependsOn: ['method']
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled', name: 'enabled',
description: 'Toggle enable state' description: 'Toggle enable state'
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'silent', name: 'silent',
description: 'Toggle silent operation' description: 'Toggle silent operation'
}) }
] ]
}); });

View File

@ -1,4 +1,4 @@
const { FilterSetting, CommandOption } = require('../../../interfaces/'); const { FilterSetting } = require('../../../interfaces/');
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
const { FilterPresets } = require('../../../../constants'); const { FilterPresets } = require('../../../../constants');
@ -43,7 +43,7 @@ class LinkFilterSetting extends FilterSetting {
actions: { ARRAY: 'ACTION' } actions: { ARRAY: 'ACTION' }
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'method', name: 'method',
description: 'Select which modification method to use', description: 'Select which modification method to use',
@ -56,8 +56,8 @@ class LinkFilterSetting extends FilterSetting {
{ name: 'list', value: 'list' } { name: 'list', value: 'list' }
], ],
dependsOn: ['list'] dependsOn: ['list']
}), },
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'list', name: 'list',
description: 'Select which list to modify', description: 'Select which list to modify',
@ -71,22 +71,22 @@ class LinkFilterSetting extends FilterSetting {
{ name: 'presets', value: 'presets' } { name: 'presets', value: 'presets' }
], ],
dependsOn: ['method'] dependsOn: ['method']
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled', name: 'enabled',
description: 'Toggle enable state' description: 'Toggle enable state'
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'whitelist', name: 'whitelist',
description: 'Toggle whitelist mode' description: 'Toggle whitelist mode'
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'silent', name: 'silent',
description: 'Toggle silent operation' description: 'Toggle silent operation'
}) }
] ]
}); });

View File

@ -1,4 +1,4 @@
const { FilterSetting, CommandOption } = require('../../../interfaces/'); const { FilterSetting } = require('../../../interfaces/');
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
class MentionFilter extends FilterSetting { class MentionFilter extends FilterSetting {
@ -28,27 +28,27 @@ class MentionFilter extends FilterSetting {
ignore: { ARRAY: 'GUILD_TEXT' } ignore: { ARRAY: 'GUILD_TEXT' }
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Toggle state', description: 'Toggle state',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'silent', name: 'silent',
description: 'Whether the bot will respond in chat', description: 'Whether the bot will respond in chat',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'unique', name: 'unique',
description: 'Mentions for the same user count as one', description: 'Mentions for the same user count as one',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'limit', name: 'limit',
description: 'How many mentions are allowed in a message', description: 'How many mentions are allowed in a message',
type: 'INTEGER' type: 'INTEGER'
}), },
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'method', name: 'method',
description: 'Select which modification method to use', description: 'Select which modification method to use',
@ -61,8 +61,8 @@ class MentionFilter extends FilterSetting {
{ name: 'list', value: 'list' } { name: 'list', value: 'list' }
], ],
dependsOn: ['list'] dependsOn: ['list']
}), },
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'list', name: 'list',
description: 'Select which list to modify', description: 'Select which list to modify',
@ -72,7 +72,7 @@ class MentionFilter extends FilterSetting {
{ name: 'actions', value: 'actions' }, { name: 'actions', value: 'actions' },
], ],
dependsOn: ['method'] dependsOn: ['method']
}), },
] ]
}); });
} }

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
const INFRACTIONS = ['WARN', 'MUTE', 'KICK', 'SOFTBAN', 'BAN', 'VCMUTE', 'VCKICK', 'VCBAN']; const INFRACTIONS = ['WARN', 'MUTE', 'KICK', 'SOFTBAN', 'BAN', 'VCMUTE', 'VCKICK', 'VCBAN'];
@ -37,22 +37,22 @@ class ModerationPoints extends Setting {
multiplier: false multiplier: false
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
name: 'points', name: 'points',
description: 'Point value', description: 'Point value',
type: 'INTEGER', type: 'INTEGER',
dependsOn: ['associate', 'type'], dependsOn: ['associate', 'infraction'],
dependsOnMode: 'OR', dependsOnMode: 'OR',
minimum: 0, minimum: 0,
maximum: 100 maximum: 100
}), },
new CommandOption({ {
name: 'expire', name: 'expire',
description: 'How long the points are counted for', description: 'How long the points are counted for',
type: 'TIME', type: 'TIME',
dependsOn: ['type'] dependsOn: ['infraction']
}), },
new CommandOption({ {
name: 'infraction', name: 'infraction',
description: 'Type of infraction', description: 'Type of infraction',
type: 'STRING', type: 'STRING',
@ -61,36 +61,36 @@ class ModerationPoints extends Setting {
}), }),
dependsOn: ['points', 'expire'], dependsOn: ['points', 'expire'],
dependsOnMode: 'OR' dependsOnMode: 'OR'
}), },
new CommandOption({ {
name: 'enabled', name: 'enabled',
description: 'Toggle on or off', description: 'Toggle on or off',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'multiplier', name: 'multiplier',
description: 'Use points as a multiplier for the expiration', description: 'Use points as a multiplier for the expiration',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}), },
new CommandOption({ {
name: 'associate', name: 'associate',
description: 'Associate a word within a reason to a point value', description: 'Associate a word within a reason to a point value',
type: 'STRING', type: 'STRING',
dependsOn: ['points'] dependsOn: ['points'], flag: true
}), },
] ]
}); });
} }
async execute(interaction, opts, setting) { async execute(interaction, opts, setting) {
const { points, type, enabled, associate, expire, multiplier } = opts; const { points, infraction, enabled, associate, expire, multiplier } = opts;
if (multiplier) setting.multiplier = multiplier.value; if (multiplier) setting.multiplier = multiplier.value;
if (enabled) setting.enabled = enabled.value; if (enabled) setting.enabled = enabled.value;
if (expire) setting.expirations[type.value] = expire.value * 1000; if (expire) setting.expirations[infraction.value] = expire.value * 1000;
if (associate) setting.associations[associate.value.toLowerCase()] = points.value; if (associate) setting.associations[associate.value.toLowerCase()] = points.value;
if (type && points) setting.points[type.value] = points.value; if (infraction && points) setting.points[infraction.value] = points.value;
return { index: 'SETTING_SUCCESS_ALT' }; return { index: 'SETTING_SUCCESS_ALT' };

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require('../../../interfaces/'); const { Setting } = require('../../../interfaces/');
const { inspect } = require('util'); const { inspect } = require('util');
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
@ -52,28 +52,28 @@ class MuteSetting extends Setting {
permanent: 'BOOLEAN' permanent: 'BOOLEAN'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING', flag: true,
name: 'create', name: 'create',
description: 'Create a mute role, mutually exclusive with role' description: 'Create a mute role, mutually exclusive with role'
}), },
new CommandOption({ {
type: 'ROLE', type: 'ROLE',
name: 'role', name: 'role',
description: 'Select the role to use for mutes, mutually exclusive with create' description: 'Select the role to use for mutes, mutually exclusive with create'
}), },
new CommandOption({ {
type: 'TIME', type: 'TIME', flag: true,
name: 'default', name: 'default',
description: 'Set the default duration for mutes' description: 'Set the default duration for mutes'
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'permanent', name: 'permanent',
description: 'Whether to allow permanent mutes or fall back to default mute duration' description: 'Whether to allow permanent mutes or fall back to default mute duration'
}), },
new CommandOption({ {
type: 'INTEGER', type: 'INTEGER', flag: true,
name: 'type', name: 'type',
description: 'Select the type of mute behaviour', description: 'Select the type of mute behaviour',
choices: [ { choices: [ {
@ -89,7 +89,7 @@ class MuteSetting extends Setting {
name: 'Type 3 (Use Discord timeouts)', name: 'Type 3 (Use Discord timeouts)',
value: 3 value: 3
} ] } ]
}) }
] ]
}); });
@ -187,7 +187,8 @@ class MuteSetting extends Setting {
return role; return role;
}; };
const hasPermission = guild.members.me.permissions.has('ManageRoles'); const me = await guild.resolveMember(this.client.user);
const hasPermission = me.permissions.has('ManageRoles');
if (!hasPermission) return { if (!hasPermission) return {
index: 'SETTING_MUTE_ROLEMISSINGPERMISSION', index: 'SETTING_MUTE_ROLEMISSINGPERMISSION',
error: true error: true
@ -251,7 +252,7 @@ class MuteSetting extends Setting {
for (const channel of channels.values()) { for (const channel of channels.values()) {
if (!channel.permissionsFor(guild.members.me).has('ManageRoles')) { if (!channel.permissionsFor(this.client.user).has('ManageRoles')) {
issues.push({ type: 'permission', channel: channel.name }); issues.push({ type: 'permission', channel: channel.name });
continue; continue;
} }

View File

@ -23,7 +23,7 @@ class StaffSetting extends Setting {
}, { }, {
name: 'enabled', name: 'enabled',
description: 'Whether the staff command is in use', description: 'Whether the staff command is in use',
type: 'BOOLEAN' type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
}] }]
}); });
} }

View File

@ -1,5 +1,5 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
const { FilterSetting, CommandOption } = require('../../../interfaces/'); const { FilterSetting } = require('../../../interfaces/');
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
const { FilterPresets } = require('../../../../constants'); const { FilterPresets } = require('../../../../constants');
@ -44,7 +44,7 @@ class WordFilterSetting extends FilterSetting {
actions: { ARRAY: 'ACTION' } actions: { ARRAY: 'ACTION' }
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'method', name: 'method',
description: 'Select which modification method to use', description: 'Select which modification method to use',
@ -57,8 +57,8 @@ class WordFilterSetting extends FilterSetting {
{ name: 'list', value: 'list' } { name: 'list', value: 'list' }
], ],
dependsOn: ['list'] dependsOn: ['list']
}), },
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'list', name: 'list',
description: 'Select which list to modify', description: 'Select which list to modify',
@ -73,17 +73,17 @@ class WordFilterSetting extends FilterSetting {
{ name: 'actions', value: 'actions' }, { name: 'actions', value: 'actions' },
], ],
dependsOn: ['method'] dependsOn: ['method']
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled', name: 'enabled',
description: 'Toggle enable state' description: 'Toggle enable state'
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'silent', name: 'silent',
description: 'Toggle silent operation' description: 'Toggle silent operation'
}) }
] ]
}); });

View File

@ -1,4 +1,4 @@
const { FilterSetting, CommandOption } = require('../../../interfaces/'); const { FilterSetting } = require('../../../interfaces/');
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
class WordWatcher extends FilterSetting { class WordWatcher extends FilterSetting {
@ -10,6 +10,7 @@ class WordWatcher extends FilterSetting {
description: 'Flag messages for potentially offensive content instead of deleting automatically', description: 'Flag messages for potentially offensive content instead of deleting automatically',
module: 'moderation', module: 'moderation',
default: { default: {
enabled: false,
channel: null, channel: null,
words: [], words: [],
regex: [], regex: [],
@ -18,7 +19,7 @@ class WordWatcher extends FilterSetting {
actions: [] actions: []
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'method', name: 'method',
description: 'Select which modification method to use', description: 'Select which modification method to use',
@ -31,8 +32,8 @@ class WordWatcher extends FilterSetting {
{ name: 'list', value: 'list' } { name: 'list', value: 'list' }
], ],
dependsOn: ['list'] dependsOn: ['list']
}), },
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'list', name: 'list',
description: 'Select which list to modify', description: 'Select which list to modify',
@ -44,12 +45,17 @@ class WordWatcher extends FilterSetting {
{ name: 'actions', value: 'actions' }, { name: 'actions', value: 'actions' },
], ],
dependsOn: ['method'] dependsOn: ['method']
}), },
new CommandOption({ {
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled',
description: 'Toggle enable state'
},
{
name: 'channel', name: 'channel',
type: 'TEXT_CHANNEL', type: 'TEXT_CHANNEL',
description: 'Where to output flagged messages' description: 'Where to output flagged messages',
}) }
] ]
}); });
} }

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
class Autorole extends Setting { class Autorole extends Setting {
@ -18,7 +18,7 @@ class Autorole extends Setting {
enabled: 'BOOLEAN' enabled: 'BOOLEAN'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'roles', name: 'roles',
description: 'Modification method for roles', description: 'Modification method for roles',
@ -30,12 +30,12 @@ class Autorole extends Setting {
{ name: 'edit', value: 'edit' }, { name: 'edit', value: 'edit' },
{ name: 'list', value: 'list' } { name: 'list', value: 'list' }
] ]
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled', name: 'enabled',
description: 'Toggle enable state' description: 'Toggle enable state'
}) }
] ]
}); });
} }

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
const { Util } = require("../../../../utilities"); const { Util } = require("../../../../utilities");
class Autorole extends Setting { class Autorole extends Setting {
@ -18,7 +18,7 @@ class Autorole extends Setting {
enabled: 'BOOLEAN' enabled: 'BOOLEAN'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'roles', name: 'roles',
description: 'Modification method for roles', description: 'Modification method for roles',
@ -30,12 +30,12 @@ class Autorole extends Setting {
{ name: 'edit', value: 'edit' }, { name: 'edit', value: 'edit' },
{ name: 'list', value: 'list' } { name: 'list', value: 'list' }
] ]
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled', name: 'enabled',
description: 'Toggle enable state' description: 'Toggle enable state'
}) }
], ],
premium: 1 premium: 1
}); });

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces"); const { Setting } = require("../../../interfaces");
class Autorole extends Setting { class Autorole extends Setting {
@ -17,16 +17,16 @@ class Autorole extends Setting {
enabled: 'BOOLEAN' enabled: 'BOOLEAN'
}, },
commandOptions: [ commandOptions: [
new CommandOption({ {
type: 'STRING', type: 'STRING',
name: 'message', name: 'message',
description: 'Set the welcome message' description: 'Set the welcome message'
}), },
new CommandOption({ {
type: 'BOOLEAN', type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled', name: 'enabled',
description: 'Toggle enable state' description: 'Toggle enable state'
}) }
] ]
}); });
} }

View File

@ -53,7 +53,8 @@ class AddroleInfraction extends Infraction {
const { grantable } = await this.guild.settings(); const { grantable } = await this.guild.settings();
let filtered = []; let filtered = [];
const { highest: clientHighest } = this.guild.members.me.roles; const me = await this.guild.resolveMember(this.client.user);
const { highest: clientHighest } = me.roles;
filtered = this.data.roles.filter((r) => r.comparePositionTo(clientHighest) < 0); filtered = this.data.roles.filter((r) => r.comparePositionTo(clientHighest) < 0);
if (filtered.length === 0) { if (filtered.length === 0) {
return super._fail('C_ADDROLE_ROLEHIERARCHYBOT'); return super._fail('C_ADDROLE_ROLEHIERARCHYBOT');

View File

@ -48,7 +48,7 @@ class BanInfraction extends Infraction {
const callbacks = this.client.moderationManager.callbacks.filter((c) => c.infraction.type === 'BAN' const callbacks = this.client.moderationManager.callbacks.filter((c) => c.infraction.type === 'BAN'
&& c.infraction.target === this.target.id); && c.infraction.target === this.target.id);
if (callbacks.size > 0) callbacks.map((c) => this.client.moderationManager.removeCallback(c.infraction)); if (callbacks.size > 0) callbacks.map((c) => this.client.moderationManager.removeCallback(c.infraction, true));
return this._succeed(); return this._succeed();
@ -56,13 +56,14 @@ class BanInfraction extends Infraction {
async verify() { async verify() {
if (this.target instanceof GuildMember) { const member = await this.guild.resolveMember(this.target.id);
if (!this.member.bannable) return super._fail('C_BAN_CANNOTBEBANNED'); if (member && !member.bannable)
} return super._fail('C_BAN_CANNOTBEBANNED');
let alreadyBanned = null; let alreadyBanned = null;
try { try {
alreadyBanned = await this.guild.bans.fetch(this.member.id); alreadyBanned = await this.guild.bans.fetch(this.target.id);
} catch (e) { } //eslint-disable-line no-empty } catch (e) { } //eslint-disable-line no-empty
if (alreadyBanned) return super._fail('C_BAN_ALREADYBANNED'); if (alreadyBanned) return super._fail('C_BAN_ALREADYBANNED');

View File

@ -140,9 +140,9 @@ class LockdownInfraction extends Infraction {
async verify() { async verify() {
const perms = this.target.permissionsFor(this.guild.members.me); const perms = this.target.permissionsFor(this.client.user);
const missing = perms.missing(['ManageRoles', 'SendMessages', 'AddReactions']); const missing = perms.missing(['ManageRoles', 'SendMessages', 'AddReactions']);
if(missing.length) return this._fail('INFRACTION_LOCKDOWN_MISSING_PERMS', { missing: missing.join('**, **') }); if (missing.length) return this._fail(this.guild.format('INFRACTION_LOCKDOWN_MISSING_PERMS', { missing: missing.join('**, **') }), null, true);
return super._verify(); return super._verify();
} }

View File

@ -48,6 +48,8 @@ class MuteInfraction extends Infraction {
role = await this.client.resolver.resolveRole(setting.role, true, this.guild); role = await this.client.resolver.resolveRole(setting.role, true, this.guild);
} }
const me = await this.guild.resolveMember(this.client.user);
let removed = []; let removed = [];
switch (setting.type) { switch (setting.type) {
case 0: case 0:
@ -60,12 +62,12 @@ class MuteInfraction extends Infraction {
break; break;
case 1: case 1:
removed = this.member.roles.cache.filter((r) => !r.managed && removed = this.member.roles.cache.filter((r) => !r.managed &&
r.comparePositionTo(this.guild.members.me.roles.highest) < 0 && r.comparePositionTo(me.roles.highest) < 0 &&
r.id !== this.guild.id); r.id !== this.guild.id);
try { try {
await this.member.roles.set([ await this.member.roles.set([
...this.member.roles.cache.filter((r) => r.managed || ...this.member.roles.cache.filter((r) => r.managed ||
r.comparePositionTo(this.guild.members.me.roles.highest) >= 0 || r.comparePositionTo(me.roles.highest) >= 0 ||
r.id === this.guild.id).values(), r.id === this.guild.id).values(),
role role
], this._reason); ], this._reason);
@ -76,11 +78,11 @@ class MuteInfraction extends Infraction {
break; break;
case 2: case 2:
removed = this.member.roles.cache.filter((r) => !r.managed && removed = this.member.roles.cache.filter((r) => !r.managed &&
r.comparePositionTo(this.guild.members.me.roles.highest) < 0 && r.comparePositionTo(me.roles.highest) < 0 &&
r.id !== this.guild.id); r.id !== this.guild.id);
try { try {
await this.member.roles.set(this.member.roles.cache.filter((r) => r.managed || await this.member.roles.set(this.member.roles.cache.filter((r) => r.managed ||
r.comparePositionTo(this.guild.members.me.roles.highest) >= 0 || r.comparePositionTo(me.roles.highest) >= 0 ||
r.id === this.guild.id), this._reason); r.id === this.guild.id), this._reason);
} catch (error) { } catch (error) {
this.client.logger.error(`Mute infraction failed to calculate removeable roles, might want to check this out.\n${error.stack || error}`); this.client.logger.error(`Mute infraction failed to calculate removeable roles, might want to check this out.\n${error.stack || error}`);
@ -96,6 +98,8 @@ class MuteInfraction extends Infraction {
} }
} }
if (this.member.voice.channel) await this.member.voice.disconnect(this._reason);
this.data = { this.data = {
removedRoles: removed.map((r) => r.id), removedRoles: removed.map((r) => r.id),
muteType: setting.type, muteType: setting.type,
@ -107,7 +111,7 @@ class MuteInfraction extends Infraction {
if (callback) { if (callback) {
this.data.removedRoles = [...new Set([...this.data.removedRoles, ...callback.infraction.data.removedRoles])]; this.data.removedRoles = [...new Set([...this.data.removedRoles, ...callback.infraction.data.removedRoles])];
this.client.moderationManager.removeCallback(callback.infraction); this.client.moderationManager.removeCallback(callback.infraction, true);
} }
// if(callbacks.size > 0) callbacks.map((c) => this.client.moderationManager._removeExpiration(c)); // if(callbacks.size > 0) callbacks.map((c) => this.client.moderationManager._removeExpiration(c));
@ -134,10 +138,12 @@ class MuteInfraction extends Infraction {
return this._fail('COMMAND_MUTE_INVALIDMUTEROLE', true); return this._fail('COMMAND_MUTE_INVALIDMUTEROLE', true);
} }
} }
const me = await this.guild.resolveMember(this.client.user);
if (settings.mute.type === 3) { if (settings.mute.type === 3) {
if (this.guild.members.me.permissions.missing('ModerateMembers').length) return this._fail('COMMAND_MUTE_MISSING_MODERATE_PERM', true); if (me.permissions.missing('ModerateMembers').length) return this._fail('COMMAND_MUTE_MISSING_MODERATE_PERM', true);
if (me.roles.highest.position <= this.member.roles.highest.position) return this._fail('COMMAND_MUTE_HIERARCHY_ERROR');
// if (!this.duration && !settings.mute.default) // if (!this.duration && !settings.mute.default)
} else if (this.guild.members.me.permissions.missing('ManageRoles').length) return this._fail('COMMAND_MUTE_MISSING_MANAGEROLE_PERM'); } else if (me.permissions.missing('ManageRoles').length) return this._fail('COMMAND_MUTE_MISSING_MANAGEROLE_PERM');
return super._verify(); return super._verify();

View File

@ -82,7 +82,8 @@ class NicknameInfraction extends Infraction {
async verify() { async verify() {
const { highest } = this.member.roles; const { highest } = this.member.roles;
if (highest.comparePositionTo(this.guild.members.me.roles.highest) > 0 || !this.guild.members.me.permissions.has('ManageNicknames')) { const me = await this.guild.resolveMember(this.client.user);
if (highest.comparePositionTo(me.roles.highest) > 0 || !me.permissions.has('ManageNicknames')) {
return this._fail('C_NICKNAME_MISSINGPERMISSIONS'); return this._fail('C_NICKNAME_MISSINGPERMISSIONS');
} }

View File

@ -53,7 +53,8 @@ class RemoveroleInfraction extends Infraction {
const { grantable } = await this.guild.settings(); const { grantable } = await this.guild.settings();
let filtered = []; let filtered = [];
const { highest: clientHighest } = this.guild.members.me.roles; const me = await this.guild.resolveMember(this.client.user);
const { highest: clientHighest } = me.roles;
filtered = this.data.roles.filter((r) => r.comparePositionTo(clientHighest) < 0); filtered = this.data.roles.filter((r) => r.comparePositionTo(clientHighest) < 0);
if (filtered.length === 0) { if (filtered.length === 0) {
return super._fail('C_REMOVEROLE_ROLEHIERARCHYBOT'); return super._fail('C_REMOVEROLE_ROLEHIERARCHYBOT');

View File

@ -39,7 +39,7 @@ class UnbanInfraction extends Infraction {
const callbacks = this.client.moderationManager.callbacks.filter((c) => c.infraction.type === 'BAN' const callbacks = this.client.moderationManager.callbacks.filter((c) => c.infraction.type === 'BAN'
&& c.infraction.target === this.target.id); && c.infraction.target === this.target.id);
if (callbacks.size > 0) callbacks.map((c) => this.client.moderationManager.removeCallback(c.infraction)); if (callbacks.size > 0) callbacks.map((c) => this.client.moderationManager.removeCallback(c.infraction, true));
await this.handle(); await this.handle();
return this._succeed(); return this._succeed();

View File

@ -95,9 +95,9 @@ class UnlockdownInfraction extends Infraction {
async verify() { async verify() {
const perms = this.target.permissionsFor(this.guild.members.me); const perms = this.target.permissionsFor(this.client.user);
const missing = perms.missing(['ManageRoles', 'SendMessages', 'AddReactions']); const missing = perms.missing(['ManageRoles', 'SendMessages', 'AddReactions']);
if(missing.length) return this._fail('INFRACTION_LOCKDOWN_MISSING_PERMS', { missing: missing.join('**, **') }); if (missing.length) this._fail(this.guild.format('INFRACTION_LOCKDOWN_MISSING_PERMS', { missing: missing.join('**, **') }), null, true);
return super._verify(); return super._verify();
} }

View File

@ -1,5 +1,7 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
const { ChannelType } = require("discord.js");
const Constants = { const Constants = {
CommandOptionTypes: { CommandOptionTypes: {
SUB_COMMAND: 1, SUB_COMMAND: 1,
@ -28,7 +30,8 @@ const Constants = {
ROLE: 8, ROLE: 8,
MENTIONABLE: 9, MENTIONABLE: 9,
NUMBER: 10, NUMBER: 10,
FLOAT: 10 FLOAT: 10,
POINTS: 4
}, },
ChannelTypes: { ChannelTypes: {
TEXT_CHANNEL: 0, TEXT_CHANNEL: 0,
@ -36,12 +39,17 @@ const Constants = {
} }
}; };
const PointsReg = /^([-+]?[0-9]+) ?(points|point|pts|pt|p)$/iu;
class CommandOption { class CommandOption {
constructor(options = {}) { constructor(options = {}) {
this._options = options;
this.name = options.name; this.name = options.name;
this.description = options.description || "A missing description, let a bot developer know."; this.description = options.description || "A missing description, let a bot developer know.";
if(!options.client) throw new Error(`${this.name} is missing client`);
this.client = options.client;
this.type = Object.keys(Constants.CommandOptionTypes).includes(options.type) ? options.type : 'STRING'; this.type = Object.keys(Constants.CommandOptionTypes).includes(options.type) ? options.type : 'STRING';
this.required = Boolean(options.required); this.required = Boolean(options.required);
@ -52,8 +60,10 @@ class CommandOption {
if (options.options) if (options.options)
for (const opt of options.options) { for (const opt of options.options) {
// console.log(opt); // console.log(opt);
if (opt instanceof CommandOption) this.options.push(opt); if (opt instanceof CommandOption) {
else if (opt.name instanceof Array) { opt.client = this.client;
this.options.push(opt);
} else if (opt.name instanceof Array) {
const { name: names, description, type, dependsOn, ...opts } = opt; const { name: names, description, type, dependsOn, ...opts } = opt;
for (const name of names) { for (const name of names) {
// console.log(name); // console.log(name);
@ -69,9 +79,12 @@ class CommandOption {
if (dependsOn instanceof Array) { if (dependsOn instanceof Array) {
_dependsOn = dependsOn[index]; _dependsOn = dependsOn[index];
} }
this.options.push(new CommandOption({ name, type: _type, description: desc, dependsOn: _dependsOn, ...opts })); this.options.push(new CommandOption({
client: this.client, name, type: _type,
description: desc, dependsOn: _dependsOn, ...opts
}));
} }
} else this.options.push(new CommandOption(opt)); } else this.options.push(new CommandOption({ client: this.client, ...opt }));
} }
// this.options = options.options || []; //Used for SUB_COMMAND/SUB_COMMAND_GROUP types. // this.options = options.options || []; //Used for SUB_COMMAND/SUB_COMMAND_GROUP types.
@ -83,32 +96,51 @@ class CommandOption {
this.minimum = typeof options.minimum === 'number' ? options.minimum : undefined; //Used for INTEGER/NUMBER/FLOAT types. this.minimum = typeof options.minimum === 'number' ? options.minimum : undefined; //Used for INTEGER/NUMBER/FLOAT types.
this.maximum = typeof options.maximum === 'number' ? options.maximum : undefined; this.maximum = typeof options.maximum === 'number' ? options.maximum : undefined;
this.flag = true; // used with message based command options this.slashOption = options.slashOption || false;
this.flag = options.flag ?? false; // used with message based command options
this.valueOptional = options.valueOptional ?? false;
this.defaultValue = options.defaultValue ?? null;
this.valueAsAlias = options.choices?.length && (options.valueAsAlias ?? false);
// this.words = options.words ?? null; // Used when parsing strings if the command has multiple string types that aren't flags
this.value = undefined; this.value = undefined;
// Used in cloned options when parsing final value
this.guild = options.guild || null;
this._rawValue = options._rawValue ?? null; //Raw value input from Discord. -- use ?? where the value is potentially false, otherwise we end up with false -> null this._rawValue = options._rawValue ?? null; //Raw value input from Discord. -- use ?? where the value is potentially false, otherwise we end up with false -> null
} }
usage(guild) { get guildOnly() {
const name = `${this.name.toUpperCase()} [${this.type}]`; return ['ROLE', 'MEMBER', 'CHANNEL'].some((t) => this.type.includes(t));
}
usage(guild, verbose = false) {
let name = `${this.name.toUpperCase()} [${this.type}]`;
let flagProps = ['flag'];
if (this.valueOptional) flagProps.push('optional value');
if (this.defaultValue !== null) flagProps.push(`default value: \`${this.defaultValue}\``);
flagProps = `(${flagProps.join(', ')})`;
if(this.flag) name += ` ${flagProps}`;
let value = null; let value = null;
const format = (...args) => guild ? guild.format(...args) : this.client.format(...args);
if (this.type === 'SUB_COMMAND_GROUP') { if (this.type === 'SUB_COMMAND_GROUP') {
value = this.options.map((opt) => { value = this.options.map((opt) => {
const usage = opt.usage(guild); const usage = opt.usage(guild, true);
return `__${usage.name.replace('》', '').trim()}__\n${usage.value}`; return `__${usage.name.replace('》', '').trim()}__\n${usage.value}`;
}).join('\n\n'); }).join('\n\n');
} else if (this.type === 'SUB_COMMAND') { } else if (this.type === 'SUB_COMMAND') {
if (!this.options.length) value = guild.format('GENERAL_NO_ARGS'); if (!this.options.length) value = format('GENERAL_NO_ARGS');
else value = this.options.map((opt) => opt.usage(guild).value).join('\n'); else value = this.options.map((opt) => opt.usage(guild, true).value).join('\n');
} else { } else {
value = `**${this.name} [${this.type}]:** ${this.description}`; value = `${verbose ? `**${this.name} [${this.type}]** ${this.flag ? flagProps : ''}\n` : ''}`;
value += `${this.description}`;
if (this.choices.length) if (this.choices.length)
value += `\n__${guild.format('GENERAL_CHOICES')}__: ${this.choices.map((choice) => choice.name).join(', ')}`; value += `\n__${format('GENERAL_CHOICES')}__: ${this.choices.map((choice) => choice.name).join(', ')}`;
if (this.dependsOn.length) if (this.dependsOn.length)
value += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: this.dependsOn.join('`, `') })}`; value += `\n${format('GENERAL_DEPENDSON', { dependencies: this.dependsOn.join('`, `') })}`;
if (this.minimum !== undefined) if (this.minimum !== undefined)
value += `\nMIN: \`${this.minimum}\``; value += `\nMIN: \`${this.minimum}\``;
if (this.maximum !== undefined) { if (this.maximum !== undefined) {
@ -130,20 +162,268 @@ class CommandOption {
* @return {CommandOption} * @return {CommandOption}
* @memberof CommandOption * @memberof CommandOption
*/ */
clone(value) { clone(_rawValue, guild, slashOption = false) {
return new CommandOption({ return new CommandOption({
name: this.name, type: this.type, ...this._options, _rawValue, guild, slashOption
minimum: this.minimum, maximum: this.maximum,
dependsOn: this.dependsOn, dependsOnMode: this.dependsOnMode,
_rawValue: value, strict: this.strict
}); });
} }
async parse() {
if(!this._rawValue && !this.valueOptional) throw new Error(`Null _rawValue`);
// console.log('-------PARSE BEGIN---------');
// console.log('1', this.name, this._rawValue, this.valueOptional);
const { removed, value, error } = await this.types[this.type]();
// console.log('2', removed, value, error);
// console.log('--------PARSE END----------');
if(error) return { error };
this.value = value;
return removed || [];
}
format(index, params, opts) {
if (this.guild) return this.guild.format(index, params, opts);
return this.client.format(index, params, opts);
}
get types() {
return {
POINTS: () => {
if(this.slashOption) return { value: this._rawValue };
let value = null,
removed = null;
for (const str of this._rawValue) {
const num = parseInt(str);
if (isNaN(num)) continue;
if(PointsReg.test(str)) {
value = num; removed = [str];
break;
}
const index = this._rawValue.indexOf(str);
const next = this._rawValue[index + 1];
const tmp = str + next;
if (PointsReg.test(tmp)) {
value = num; removed= [str, next];
break;
}
}
if (this.minimum !== undefined && value < this.minimum) return { error: true };
if (this.maximum !== undefined && value > this.maximum) return { error: true };
return { value, removed };
},
ROLES: async () => {
const roles = [],
removed = [];
for (const str of this._rawValue) {
const role = await this.guild.resolveRole(str, this.strict);
if (role) {
roles.push(role);
removed.push(str);
} else if(roles.length) break;
}
if (!roles.length) return { error: true };
return { value: roles, removed };
},
MEMBERS: async () => {
const members = [],
removed = [];
for (const arg of this._rawValue) {
const member = await this.guild.resolveMember(arg, this.strict);
if (member) {
members.push(member);
removed.push(arg);
} else if(members.length) break;
}
if (!members.length) return { error: true, message: this.strict ? this.format('O_COMMANDHANDLER_TYPEMEMBER_STRICT') : null };
return { value: members, removed };
},
USERS: async () => {
const users = [],
removed = [];
for (const arg of this._rawValue) {
const user = await this.client.resolveUser(arg, this.strict);
if (user) {
users.push(user);
removed.push(arg);
} else if(users.length) break;
}
if (!users.length) return { error: true, message: this.strict ? this.format('O_COMMANDHANDLER_TYPEUSERS_STRICT') : null };
return { value: users, removed };
},
CHANNELS: async () => {
const channels = [],
removed = [];
for (const arg of this._rawValue) {
const channel = await this.guild.resolveChannel(arg, this.strict);
if (channel) {
channels.push(channel);
removed.push(arg);
} else if(channels.length) break;
}
if (!channels.length) return { error: true };
return { value: channels, removed };
},
TEXT_CHANNELS: async () => {
const channels = [],
removed = [];
for(const arg of this._rawValue) {
const channel = await this.guild.resolveChannel(arg, this.strict, (channel) => channel.type === ChannelType.GuildText);
if (channel) {
channels.push(channel);
removed.push(arg);
} else if(channels.length) break;
}
if (!channels.length) return { error: true };
return { value: channels, removed };
},
VOICE_CHANNELS: async () => {
const channels = [],
removed = [];
for (const arg of this._rawValue) {
const channel = await this.guild.resolveChannel(arg, this.strict, (channel) => channel.type === ChannelType.GuildVoice);
if (channel) {
channels.push(channel);
removed.push(arg);
} else if(channels.length) break;
}
if (!channels.length) return { error: true };
return { value: channels, removed };
},
TIME: () => {
const value = this.client.resolver.resolveTime(this._rawValue);
if (value === null) return { error: true };
return { value, removed: [this._rawValue] };
},
COMPONENT: () => {
const [component] = this.client.resolver.components(this._rawValue, 'any');
if (!component) return { error: true };
return { value: component, removed: [this._rawValue] };
},
COMPONENTS: () => {
const strings = this._rawValue;
const components = [],
removed = [];
for (const str of strings) {
const [component] = this.client.resolver.components(str, 'any');
if (component && !components.includes(component)) {
components.push(component);
removed.push(str);
} else if(components.length) break;
}
if (!components.length) return { error: true };
return { value: components, removed };
},
COMMAND: () => {
const [command] = this.client.resolver.components(this._rawValue, 'command');
if (!command) return { error: true };
return { value: command, removed: [this._rawValue] };
},
COMMANDS: () => {
const strings = this._rawValue;
const commands = [],
removed = [];
for (const str of strings) {
const [command] = this.client.resolver.components(str, 'command');
if (command && !commands.includes(command)) {
commands.push(command);
removed.push(str);
} else if(commands.length) break;
}
if (!commands.length) return { error: true };
return { value: commands, removed };
},
MODULE: () => {
const [module] = this.client.resolver.components(this._rawValue, 'module');
if (!module) return { error: true };
return { value: module, removed: [this._rawValue] };
},
STRING: () => {
if (this.slashOption) return { value: this._rawValue };
if (this._aliased) return { value: this._rawValue, removed: [] };
if (this.choices.length) {
const found = this.choices.find((c) => c.value.toLowerCase() === this._rawValue.toLowerCase());
if (found) return { value: found.value, removed: [this._rawValue] };
return { error: true };
}
return { value: this._rawValue, removed: [this._rawValue] };
},
INTEGER: () => {
const integer = parseInt(this._rawValue);
if(isNaN(integer)) return { error: true };
if (this.minimum !== undefined && integer < this.minimum) return { error: true };
if (this.maximum !== undefined && integer > this.maximum) return { error: true };
return { value: integer, removed: [this._rawValue] };
},
BOOLEAN: () => {
const boolean = this.client.resolver.resolveBoolean(this._rawValue);
if (boolean === null && this.valueOptional) return { value: this.defaultValue, removed: [] };
else if(boolean === null) return { error: true };
return { value: boolean, removed: [this._rawValue] };
},
MEMBER: async () => {
const member = await this.guild.resolveMember(this._rawValue, this.strict);
if (!member) return { error: true };
return { value: member, removed: [this._rawValue] };
},
USER: async () => {
const user = await this.client.resolver.resolveUser(this._rawValue, this.strict);
if(!user) return { error: true };
return { value: user, removed: [this._rawValue] };
},
TEXT_CHANNEL: async () => {
const channel = await this.guild.resolveChannel(this._rawValue);
if (!channel || channel.type !== ChannelType.GuildText) return { error: true };
return { value: channel, removed: [this._rawValue] };
},
VOICE_CHANNEL: async () => {
const channel = await this.guild.resolveChannel(this._rawValue);
if (!channel || channel.type !== ChannelType.GuildVoice) return { error: true };
return { value: channel, removed: [this._rawValue] };
},
CHANNEL: async () => {
const channel = await this.guild.resolveChannel(this._rawValue);
if(!channel) return { error: true };
return { value: channel, removed: [this._rawValue] };
},
ROLE: async () => {
const role = await this.guild.resolveRole(this._rawValue);
if(!role) return { error: true };
return { value: role, removed: [this._rawValue] };
},
MENTIONABLE: (mentionable) => {
return { value: mentionable };
},
NUMBER: () => {
const number = parseFloat(this._rawValue);
if(isNaN(number))return { error: true };
if (this.minimum !== undefined && number < this.minimum) return { error: true };
if (this.maximum !== undefined && number > this.maximum) return { error: true };
return { value: number, removed: [this._rawValue] };
},
FLOAT: () => {
const float = parseFloat(this._rawValue);
if(isNaN(float)) return { error: true };
if (this.minimum !== undefined && float < this.minimum) return { error: true };
if (this.maximum !== undefined && float > this.maximum) return { error: true };
return { value: parseFloat(float), removed: [this._rawValue] };
},
DATE: async () => {
const date = await this.client.resolver.resolveDate(this._rawValue);
if (!date) return { error: true };
return { value: date, removed: [this._rawValue] };
}
};
}
get plural() {
return this.type.endsWith('S');
}
get raw() { get raw() {
return { return {
name: this.name, name: this.name,
type: this.type, type: this.type,
options: [] options: this.options.map((opt) => opt.raw)
}; };
} }

View File

@ -9,6 +9,7 @@ const {
} = require('../../constants'); } = require('../../constants');
const { Util } = require('../../utilities'); const { Util } = require('../../utilities');
const { inspect } = require('util');
const Constants = { const Constants = {
MaxCharacters: 1024, // Max embed description is 2048 characters, however some of those description characters are going to usernames, types, filler text, etc. MaxCharacters: 1024, // Max embed description is 2048 characters, however some of those description characters are going to usernames, types, filler text, etc.
@ -147,7 +148,7 @@ class Infraction {
if(this._mongoId) filter._id = this._mongoId; if(this._mongoId) filter._id = this._mongoId;
return this.client.storageManager.mongodb.infractions.updateOne(filter, this.json) return this.client.storageManager.mongodb.infractions.updateOne(filter, this.json)
.catch((error) => { .catch((error) => {
this.client.logger.error(`There was an issue saving infraction data to the database.\n${error.stack || error}\nInfraction data:\n${this.json}`); this.client.logger.error(`There was an issue saving infraction data to the database.\n${error.stack || error}\nInfraction data:\n${inspect(this.json)}`);
}); });
} }
@ -325,9 +326,10 @@ class Infraction {
if (protection.type === 'position') { if (protection.type === 'position') {
const executorHighest = executor.roles.highest; const executorHighest = executor.roles.highest;
const targetHighest = target.roles.highest; const targetHighest = target.roles.highest;
if (executorHighest.comparePositionTo(targetHighest) < 0) { if (executorHighest.comparePositionTo(targetHighest) === 0)
return this._fail('INFRACTION_PROTECTIONPOSITIONERROR_SAME');
if (executorHighest.comparePositionTo(targetHighest) < 0)
return this._fail('INFRACTION_PROTECTIONPOSITIONERROR'); return this._fail('INFRACTION_PROTECTIONPOSITIONERROR');
}
} else if (protection.type === 'role') { } else if (protection.type === 'role') {
const contains = target.roles.cache.some((r) => protection.roles.includes(r.id)); const contains = target.roles.cache.some((r) => protection.roles.includes(r.id));
if (contains) { if (contains) {

View File

@ -7,7 +7,6 @@ const Component = require("./Component.js");
// Imports to enable JSDocs typing // Imports to enable JSDocs typing
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const InteractionWrapper = require("../client/wrappers/InteractionWrapper.js"); const InteractionWrapper = require("../client/wrappers/InteractionWrapper.js");
const CommandOption = require('./CommandOption.js');
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
// const { DiscordClient } = require("../DiscordClient.js"); // const { DiscordClient } = require("../DiscordClient.js");
@ -70,29 +69,9 @@ class Setting extends Component {
this.default = { [this.name]: options.default || {} }; this.default = { [this.name]: options.default || {} };
this.definitions = options.definitions || {}; // Used for the API for field definitions this.definitions = options.definitions || {}; // Used for the API for field definitions
this.commandOptions = []; this.commandOptions = options.commandOptions || [];
this.commandType = options.commandType || 'SUB_COMMAND'; this.commandType = options.commandType || 'SUB_COMMAND';
if (options.commandOptions)
for (const opt of options.commandOptions) {
if (opt instanceof CommandOption) this.commandOptions.push(opt);
else if (opt.name instanceof Array) {
const { name: names, description, type, ...opts } = opt;
for (const name of names) {
// console.log(name);
const index = names.indexOf(name);
let desc = description,
_type = type;
if (description instanceof Array) desc = description[index] || 'Missing description';
if (type instanceof Array) {
_type = type[index];
if (!_type) throw new Error(`Missing type for option ${name} in command ${this.name}`);
}
this.commandOptions.push(new CommandOption({ name, type: _type, description: desc, ...opts }));
}
} else this.commandOptions.push(new CommandOption(opt));
}
this.clientPermissions = options.clientPermissions || []; this.clientPermissions = options.clientPermissions || [];
this.memberPermissions = options.memberPermissions || []; // Idk if we'll end up using this but it's here anyway this.memberPermissions = options.memberPermissions || []; // Idk if we'll end up using this but it's here anyway
@ -125,22 +104,23 @@ class Setting extends Component {
fields.push({ fields.push({
name: `${guild.format(`GENERAL_OPTIONS`)}`, name: `${guild.format(`GENERAL_OPTIONS`)}`,
value: options.map( value: options.map((opt) => opt.usage(guild, true)).map((f) => f.value).join('\n\n')
(opt) => { // value: options.map(
let msg = `**${opt.name} [${opt.type}]:** ${opt.description}`; // (opt) => {
if (opt.choices.length) // let msg = `**${opt.name} [${opt.type}]:** ${opt.description}`;
msg += `\n__${guild.format('GENERAL_CHOICES')}__: ${opt.choices.map((choice) => choice.name).join(', ')}`; // if (opt.choices.length)
if (opt.dependsOn.length) // msg += `\n__${guild.format('GENERAL_CHOICES')}__: ${opt.choices.map((choice) => choice.name).join(', ')}`;
msg += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: opt.dependsOn.join('`, `') })}`; // if (opt.dependsOn.length)
if (opt.minimum !== undefined) // msg += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: opt.dependsOn.join('`, `') })}`;
msg += `\nMIN: \`${opt.minimum}\``; // if (opt.minimum !== undefined)
if (opt.maximum !== undefined) { // msg += `\nMIN: \`${opt.minimum}\``;
const newline = opt.minimum !== undefined ? ', ' : '\n'; // if (opt.maximum !== undefined) {
msg += `${newline}MAX: \`${opt.maximum}\``; // const newline = opt.minimum !== undefined ? ', ' : '\n';
} // msg += `${newline}MAX: \`${opt.maximum}\``;
return msg; // }
} // return msg;
).join('\n\n') // }
// ).join('\n\n')
}); });
} }
@ -203,16 +183,16 @@ class Setting extends Component {
if (!message.length && !index && !embed) throw new Error('Must declare either message, index or embeds'); if (!message.length && !index && !embed) throw new Error('Must declare either message, index or embeds');
const response = await invoker.promptMessage( const response = await invoker.promptMessage(
index ? invoker.format(index, params) : message, index ? invoker.format(index, params) : message,
{ time, editReply: true, embed } { time, editReply: invoker.replied, embed }
); );
if (!response) return { error: true, message: invoker.format('ERR_TIMEOUT') }; if (!response) return { error: true, message: invoker.format('ERR_TIMEOUT') };
const content = response.content.toLowerCase(); const content = response.content.toLowerCase();
if(invoker.channel.permissionsFor(invoker.guild.members.me).has('ManageMessages')) if (invoker.channel.permissionsFor(this.client.user).has('ManageMessages'))
await response.delete().catch(() => null); await response.delete().catch(() => null);
if (['cancel', 'abort', 'exit'].includes(content)) return { if (['cancel', 'abort', 'exit'].includes(content)) return {
error: true, error: true,
message: invoker.format('ERR_CANCEL') content: invoker.format('ERR_CANCEL')
}; };
else if (!content.length) return { error: true, index: 'SETTING_NOCONTENT' }; else if (!content.length) return { error: true, index: 'SETTING_NOCONTENT' };
@ -241,7 +221,8 @@ class Setting extends Component {
for (const param of params) { for (const param of params) {
if (list.includes(param)) { if (list.includes(param)) {
list.splice(list.indexOf(!caseSensitive ? param : param.toLowerCase()), 1); const [removed] = list.splice(list.indexOf(!caseSensitive ? param : param.toLowerCase()), 1);
modified.push(removed);
} else skipped.push(param); } else skipped.push(param);
} }

View File

@ -51,8 +51,10 @@ class Command extends Component {
this.options = []; this.options = [];
if (options.options) for (const opt of options.options) { if (options.options) for (const opt of options.options) {
if (opt instanceof CommandOption) this.options.push(opt); if (opt instanceof CommandOption) {
else if (opt.name instanceof Array) { opt.client = client;
this.options.push(opt);
} else if (opt.name instanceof Array) {
// Allows easy templating of subcommands that share arguments // Allows easy templating of subcommands that share arguments
const { name: names, description, type, ...opts } = opt; const { name: names, description, type, ...opts } = opt;
for (const name of names) { for (const name of names) {
@ -64,9 +66,9 @@ class Command extends Component {
_type = type[index]; _type = type[index];
if (!_type) throw new Error(`Missing type for option ${name} in command ${this.name}`); if (!_type) throw new Error(`Missing type for option ${name} in command ${this.name}`);
} }
this.options.push(new CommandOption({ name, type: _type, description: desc, ...opts })); this.options.push(new CommandOption({ name, type: _type, description: desc, ...opts, client }));
} }
} else this.options.push(new CommandOption(opt)); } else this.options.push(new CommandOption({ ...opt, client }));
} }
this.options.sort((a, b) => { this.options.sort((a, b) => {
@ -107,7 +109,11 @@ class Command extends Component {
const fields = []; const fields = [];
const { guild, subcommand, subcommandGroup } = invoker; const { guild, subcommand, subcommandGroup } = invoker;
const { permissions: { type } } = guild._settings;
let type = null;
const format = (index) => guild ? guild.format(index) : this.client.format(index);
if (guild) ({ permissions: { type } } = guild._settings);
if (this.options.length) { if (this.options.length) {
if (verbose) fields.push(...this.options.map((opt) => opt.usage(guild))); if (verbose) fields.push(...this.options.map((opt) => opt.usage(guild)));
@ -126,7 +132,7 @@ class Command extends Component {
else if (type === 'grant') required = [this.resolveable]; else if (type === 'grant') required = [this.resolveable];
else required = [this.resolveable, ...this.memberPermissions]; else required = [this.resolveable, ...this.memberPermissions];
fields.push({ fields.push({
name: `${guild.format('GENERAL_PERMISSIONS')}`, name: `${format('GENERAL_PERMISSIONS')}`,
value: `\`${required.join('`, `')}\`` value: `\`${required.join('`, `')}\``
}); });
} }
@ -135,7 +141,7 @@ class Command extends Component {
author: { author: {
name: `${this.name} [module:${this.module.name}]` name: `${this.name} [module:${this.module.name}]`
}, },
description: guild.format(`COMMAND_${this.name.toUpperCase()}_HELP`), description: format(`COMMAND_${this.name.toUpperCase()}_HELP`),
fields fields
}); });

View File

@ -4,6 +4,7 @@ class ModerationCommand extends SlashCommand {
constructor(client, opts) { constructor(client, opts) {
const flag = true;
let baseOptions = [ let baseOptions = [
{ {
name: 'users', name: 'users',
@ -13,28 +14,32 @@ class ModerationCommand extends SlashCommand {
strict: true strict: true
}, { }, {
name: 'points', name: 'points',
type: 'INTEGER', type: 'POINTS',
description: 'The amount of points to assign to the infraction', description: 'The amount of points to assign to the infraction',
minimum: 0, minimum: 0,
maximum: 100 maximum: 100
}, { }, {
name: 'expiration', name: 'expiration',
type: 'TIME', type: 'TIME',
description: 'How long until the points expire' description: 'How long until the points expire',
flag
}, { }, {
name: 'prune', name: 'prune',
type: 'INTEGER', type: 'INTEGER',
description: 'How many messages to prune', description: 'How many messages to prune',
minimum: 2, minimum: 2,
maximum: 100 maximum: 100,
flag
}, { }, {
name: 'force', name: 'force',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Whether to override automod' description: 'Whether to override automod',
flag, valueOptional: true, defaultValue: true
}, { }, {
name: 'silent', name: 'silent',
type: 'BOOLEAN', type: 'BOOLEAN',
description: 'Whether the user should receive the infraction' description: 'Whether the user should receive the infraction',
flag, valueOptional: true, defaultValue: true
}, { }, {
name: 'reason', name: 'reason',
type: 'STRING', type: 'STRING',
@ -42,7 +47,6 @@ class ModerationCommand extends SlashCommand {
} }
]; ];
// Probably temporary
if(!opts.memberPermissions) throw new Error(`MISSING PERMS ${opts.name}`); if(!opts.memberPermissions) throw new Error(`MISSING PERMS ${opts.name}`);
if (opts.skipOptions) for (const opt of opts.skipOptions) { if (opts.skipOptions) for (const opt of opts.skipOptions) {

View File

@ -10,26 +10,16 @@ class SettingsCommand extends SlashCommand {
super(client, { super(client, {
...options, ...options,
guildOnly: true, guildOnly: true,
memberPermissions: ['ManageGuild'] memberPermissions: ['ManageGuild'],
});
/*
{
name: 'settings',
description: "Configure the bot's behaviour in your server",
module: 'administration',
options: [ options: [
{
], type: 'SUB_COMMAND',
guildOnly: true name: 'list',
} description: 'List available settings'
*/ }
]
this.options.push(new CommandOption({ });
type: 'SUB_COMMAND',
name: 'list',
description: 'List available settings'
}));
this.build(); this.build();
} }
@ -50,9 +40,12 @@ class SettingsCommand extends SlashCommand {
name: setting.name, name: setting.name,
description: setting.description, description: setting.description,
type: setting.commandType, //'SUB_COMMAND', type: setting.commandType, //'SUB_COMMAND',
options: setting.commandOptions options: setting.commandOptions,
client: this.client
}); });
this.options.push(subCommand); this.options.push(subCommand);
// Overwrite the setting options with the built CommandOption structures
setting.commandOptions = subCommand.options;
} }
// for (const module of modules) { // for (const module of modules) {
@ -89,7 +82,8 @@ class SettingsCommand extends SlashCommand {
if (!setting) return invoker.reply('Something went wrong, could not find setting'); if (!setting) return invoker.reply('Something went wrong, could not find setting');
if (setting.clientPermissions.length) { if (setting.clientPermissions.length) {
const missing = guild.members.me.permissions.missing(setting.clientPermissions); const me = await guild.resolveMember(this.client.user);
const missing = me.permissions.missing(setting.clientPermissions);
if (missing.length) return invoker.reply({ if (missing.length) return invoker.reply({
emoji: 'failure', emoji: 'failure',
index: 'SETTING_MISSING_CLIENTPERMISSIONS', index: 'SETTING_MISSING_CLIENTPERMISSIONS',

View File

@ -197,6 +197,8 @@ module.exports = class FilterUtility {
//Zero width character (UTF-16 8206) //Zero width character (UTF-16 8206)
content = content.replace(//gu, ''); content = content.replace(//gu, '');
content = content.replace(/['"‘’“”]/gu, '');
//Replace the weird letters with their normal text counterparts //Replace the weird letters with their normal text counterparts
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const match = (/[a-z0-9\w\(\)\.\\\/\?!]+/gimu).exec(content); const match = (/[a-z0-9\w\(\)\.\\\/\?!]+/gimu).exec(content);
@ -321,7 +323,9 @@ module.exports = class FilterUtility {
matched: true, matched: true,
_matcher: _word, _matcher: _word,
matcher: `fuzzy [\`${_word}\`, \`${sim}\`, \`${threshold}\`]`, matcher: `fuzzy [\`${_word}\`, \`${sim}\`, \`${threshold}\`]`,
type: 'fuzzy' type: 'fuzzy',
_threshold: threshold,
_sim: sim
}; };
} }

View File

@ -206,7 +206,7 @@ class SettingsMigrator {
stickyrole: result.stickyrole ? { ...result.stickyrole, enabled: Boolean(result.stickyrole.roles.length) } : undefined, stickyrole: result.stickyrole ? { ...result.stickyrole, enabled: Boolean(result.stickyrole.roles.length) } : undefined,
welcomer: result.welcomer, welcomer: result.welcomer,
commands: { disabled: result.disabledCommands, custom: {} }, commands: { disabled: result.disabledCommands, custom: {} },
prefix: result.prefix textcommands: { prefix: result.prefix, enabled: false }
}; };
return settings; return settings;
} }
@ -287,14 +287,15 @@ class SettingsMigrator {
}; };
if (linkfilter) settings.linkfilter = { if (linkfilter) settings.linkfilter = {
enabled: result.linkfilter?.enabled || false, enabled: linkfilter.enabled || false,
silent: result.linkfilter?.silent || false, silent: linkfilter.silent || false,
ignore: result.linkfilter?.channels || [], ignore: linkfilter.channels || [],
bypass: result.linkfilter?.roles || [], bypass: linkfilter.roles || [],
actions: [], actions: [],
whitelist: result.linkfilter?.whitelist === true ? result.linkfilter.filter : [], whitelist: linkfilter.whitelist === true ? linkfilter.filter : [],
blacklist: result.linkfilter?.whitelist === false ? result.linkfilter.filter : [], blacklist: linkfilter.whitelist === false ? linkfilter.filter : [],
greylist: [] greylist: [],
whitelistMode: linkfilter.whitelist || false
}; };
if (ignore) settings.ignore = { if (ignore) settings.ignore = {
@ -334,7 +335,7 @@ class SettingsMigrator {
}; };
if (autorole) settings.autorole = autorole; if (autorole) settings.autorole = autorole;
if (prefix) settings.prefix = prefix; if (prefix) settings.textcommands = { prefix, enabled: false };
if (welcomer) { if (welcomer) {
settings.welcomer.enabled = welcomer.enabled; settings.welcomer.enabled = welcomer.enabled;
settings.welcomer.message = welcomer.message || null; settings.welcomer.message = welcomer.message || null;
@ -398,7 +399,8 @@ class SettingsMigrator {
silent: false, silent: false,
bypass: result.invitefilter.roles, bypass: result.invitefilter.roles,
enabled: result.invitefilter.enabled, enabled: result.invitefilter.enabled,
actions: [] actions: [],
whitelist: []
}; };
const channels = Object.entries(invitefilter.channels || {}); const channels = Object.entries(invitefilter.channels || {});
for (const [id, value] of channels) { for (const [id, value] of channels) {