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",
"scripts": {
"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",
"update": "git pull && cd api && yarn update",
"test": "jest --detectOpenHandles",

View File

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

View File

@ -44,6 +44,9 @@ Global permissions
[COMMAND_PERMISSIONS_SHOW_TITLE]
Granted permissions
[COMMAND_PERMISSIONS_DESC]
Showing permissions {forin} **{target}**
[COMMAND_PERMISSIONS_NO_PERMS]
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]
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]
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
//History Command
[COMMAND_HISTORY_HELP]
Display moderation history in the server.
Narrow the search down by using the parameters below.
[COMMAND_HISTORY_HISTORY]
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}
//target{plural}:
[COMMAND_HISTORY_SUCCESSMODERATOR]
by {moderator}
[COMMAND_HISTORY_NO_EXPORT_PERMS]
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]
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.
// Poll command
[COMMAND_POLL_HELP]
Have the bot send a poll message in a channel with an optional duration.
[COMMAND_POLL_QUESTIONS]
Please respond with question {number}.

View File

@ -20,6 +20,9 @@ Required Permissions
Command takes no arguments
// Generic setting field names
[GENERAL_PREFIX]
》 Prefix
[GENERAL_STATUS]
》 Status
@ -131,3 +134,11 @@ switch({toggle}) {
'off';
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.
[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]
an error occured
[INFRACTION_PROTECTIONPOSITIONERROR_SAME]
they have the same role position as you
[INFRACTION_PROTECTIONPOSITIONERROR]
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]
This command can only be run in servers.
[O_COMMANDHANDLER_GUILDONLY_OPT]
The option `{option}` is only valid in servers.
[O_COMMANDHANDLER_TYPEINTEGER]
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]
The command option {option} requires command resolveables (e.g. `command:ping`, `command:history`)
[O_COMMANDHANDLER_TYPECOMMAND]
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]
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]
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]
The command option {option} requires users.
@ -80,3 +92,14 @@ Command Error
[O_COMMANDHANDLER_COMMAND_NORESPONSE]
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]
Configure which set of permissions the bot works with.

View File

@ -94,15 +94,17 @@ class ApiClientUtil {
const { guildId } = message;
const evalFunc = (client, { guildId }) => {
const guild = client.guilds.cache.get(guildId);
if (!guild) return null;
const wrapper = new client.wrapperClasses.GuildWrapper(client, guild);
return wrapper.toJSON();
try {
const wrapper = client.getGuildWrapper(guildId);
return wrapper.toJSON();
} catch {
return null;
}
};
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 guild = result.find((elem) => elem !== undefined);
const guild = result.find((elem) => elem !== null);
return guild;
}

View File

@ -70,7 +70,7 @@ class Logger {
const maximumCharacters = Math.max(...Constants.Types.map((t) => t.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, '');
console.log(text); //eslint-disable-line no-console
@ -86,6 +86,7 @@ class Logger {
webhook(text, type) {
if (!this._webhook) return;
const message = text.replace(new RegExp(process.env.DISCORD_TOKEN, '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 path = require('path');
const hash = require('object-hash');
const { inspect } = require('util');
class SlashCommandManager {
@ -43,7 +44,7 @@ class SlashCommandManager {
this.client.logger.write('info', `Commands hash: ${cmdHash}, ${guilds.length} out of date`);
if (!guilds.length) return;
const promises = [];
//console.log(JSON.stringify(commands));
//fs.writeFileSync(path.join(process.cwd(), 'commands.json'), JSON.stringify(commands));
for(const guild of guilds) {
promises.push(this.rest.put(
Routes.applicationGuildCommands(clientId, guild),
@ -72,6 +73,10 @@ class SlashCommandManager {
str += `${command.name}: `;
const options = Object.keys(invalid[key].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 += `\n\n`;

View File

@ -91,8 +91,8 @@ class DiscordClient extends Client {
// this.logger.error(`Uncaught exception:\n${err.stack || err}`);
// });
process.on('unhandledRejection', (err, reason) => {
this.logger.error(`Unhandled rejection:\n${err?.stack || err}\n${inspect(reason)}`);
process.on('unhandledRejection', (err) => {
this.logger.error(`Unhandled rejection:\n${err?.stack || err}`);
});
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) {
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 (!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] = [];
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 (!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
if (!this.sendQueue[channel.id]) this.sendQueue[channel.id] = [];
@ -173,7 +173,7 @@ class RateLimiter {
return new Promise((resolve, reject) => {
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 (limit === null) limit = 15;

View File

@ -346,7 +346,7 @@ class Resolver {
const match = resolveable.match(id);
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);
} else if (name.test(resolveable)) {

View File

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

View File

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

View File

@ -118,7 +118,7 @@ class InvokerWrapper {
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)
.then((msg) => {
if (opts.delete) msg.delete();

View File

@ -90,6 +90,7 @@ class MessageWrapper {
async editReply(options) {
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);
}

View File

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

View File

@ -39,7 +39,8 @@ class ImportCommand extends SlashCommand {
}, {
name: 'overwrite',
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']

View File

@ -46,7 +46,7 @@ class ModstatsCommand extends SlashCommand {
const data = await this.client.mongodb.infractions.find(query, { projection: { executor: 1, type: 1 } });
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]) {
const user = await this.client.resolveUser(log.executor);
result[log.executor] = { name: user.tag };

View File

@ -27,7 +27,8 @@ class PermissionsCommand extends SlashCommand {
name: 'channel',
description: 'The channel(s) in which this permission is granted to user or role',
type: 'TEXT_CHANNELS',
dependsOn: ['permission']
dependsOn: ['permission'],
flag: true
},
{
name: 'role',
@ -64,6 +65,7 @@ class PermissionsCommand extends SlashCommand {
name: 'channel',
description: 'The channel(s) to reset',
type: 'TEXT_CHANNELS',
flag: true
},
{
name: 'role',
@ -233,6 +235,7 @@ class PermissionsCommand extends SlashCommand {
const fields = [];
let update = false;
const target = member?.value || role?.value || channel?.value;
if (member || role) {
@ -340,6 +343,7 @@ class PermissionsCommand extends SlashCommand {
if(!fields.length) return { index: 'COMMAND_PERMISSIONS_NO_PERMS' };
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'),
fields: [ ]
};

View File

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

View File

@ -13,16 +13,20 @@ class EvalCommand extends Command {
name: 'eval',
aliases: ['e', 'evaluate'],
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
params = params.join(' ');
if (args.async) params = `(async () => {${params}})()`;
let params = code.value;
if (async?.value) params = `(async () => {${params}})()`;
const { guild, author, member, client } = invoker; //eslint-disable-line no-unused-vars
let response = null;

View File

@ -17,7 +17,8 @@ class StatsCommand extends SlashCommand {
name: 'log',
type: 'BOOLEAN',
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'],

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,8 @@ class HistoryCommand extends SlashCommand {
options: [{
name: ['before', 'after'],
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'],
description: [
@ -33,26 +34,35 @@ class HistoryCommand extends SlashCommand {
'Export the list of infractions',
'DM the command response'
],
type: 'BOOLEAN'
type: 'BOOLEAN',
flag: true, valueOptional: true, defaultValue: true
}, {
name: 'type',
description: 'Filter infractions by type',
choices: Infractions.map((inf) => {
return { name: inf.toLowerCase(), value: inf };
})
}),
flag: true
}, {
name: ['pagesize', 'page'],
description: ['Amount of infractions to list per page', 'Page to select'],
type: 'INTEGER',
minimum: 1
minimum: 1,
flag: true
}, {
name: ['user', 'moderator'], //
description: ['User whose infractions to query, overrides channel if both are given', 'Query by moderator'],
name: 'user',
description: 'User whose infractions to query, overrides channel if both are given',
type: 'USER'
}, {
name: 'moderator',
description: 'Query by moderator',
type: 'USER',
flag: true
}, {
name: 'channel',
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
});
const me = await guild.resolveMember(this.client.user);
const embed = {
author: {
name: 'Infraction History',
@ -104,7 +115,7 @@ class HistoryCommand extends SlashCommand {
footer: {
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) {
@ -180,12 +191,18 @@ class HistoryCommand extends SlashCommand {
const type = invoker.format('COMMAND_HISTORY_SUCCESSTYPE', { old: oldest?.value || false }, { code: true });
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 {
content: invoker.format('COMMAND_HISTORY_SUCCESS', {
targets: user || channel ? 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(' ')
}) : '',
targets,
type
}),
emoji: 'success',

View File

@ -27,8 +27,9 @@ class MuteCommand extends ModerationCommand {
const { guild } = interaction;
const settings = await guild.settings();
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' } });
else if (!guild.members.me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } });
const me = await guild.resolveMember(this.client.user);
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, {
targets: users.value,

View File

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

View File

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

View File

@ -22,7 +22,8 @@ class ResolveCommand extends SlashCommand {
}, {
name: 'notify',
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
});
}

View File

@ -27,7 +27,7 @@ class StaffCommand extends SlashCommand {
// const role = await guild.resolveRole(staff.role);
// 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 }),
allowedMentions: { parse: ['roles'] } // roles: [staff.role],
});

View File

@ -23,8 +23,9 @@ class UnmuteCommand extends ModerationCommand {
const { guild } = interaction;
const settings = await guild.settings();
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' } });
else if (!guild.members.me.permissions.has('ManageRoles')) throw new CommandError(interaction, { index: 'INHIBITOR_CLIENTPERMISSIONS_ERROR', params: { command: this.name, missing: 'ManageRoles' } });
const me = await guild.resolveMember(this.client.user);
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, {
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) {
super(client, {
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',
choices: [16, 32, 64, 128, 256, 512, 1024, 2048].map((i) => {
return { name: `${i}`, value: `${i}` };
})
}),
flag: true
}, {
name: 'format',
description: 'Image format',
// type: 'STRING'
choices: ['webp', 'png', 'jpeg', 'jpg', 'gif'].map((i) => {
return { name: i, value: i };
})
}),
flag: true
}, {
name: 'user',
description: 'Use this for the user\'s global avatar',
@ -28,7 +30,8 @@ class AvatarCommand extends SlashCommand {
}, {
name: 'member',
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 _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']);
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 } });
@ -65,10 +65,10 @@ class PollCommand extends SlashCommand {
for (let i = 0; i < choices.value; i++) {
const response = await invoker.promptMessage({
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(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;
if (content.toLowerCase() === 'stop') break;
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 { selfrole } = await guild.settings();
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 tooHigh = roles?.value.filter((r) => r.position > ownHighest.position);

View File

@ -13,7 +13,7 @@ class ClientPermissions extends Inhibitor {
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 });
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();
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;
new Infraction(this.client, {
type: 'BAN',
@ -37,10 +37,10 @@ class AuditLogObserver extends Observer {
}).handle();
}
async guildBanRemove({ guild, user, guildWrapper: wrapper }) {
async guildBanRemove({ user, guildWrapper: wrapper }) {
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.
const audit = await this._fetchFirstEntry(guild, user, 'MemberBanRemove');
const audit = await this._fetchFirstEntry(wrapper, user, 'MemberBanRemove');
if (!audit) return undefined;
new Infraction(this.client, {
type: 'UNBAN',
@ -56,7 +56,7 @@ class AuditLogObserver extends Observer {
const { guildWrapper: wrapper } = member;
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.
const audit = await this._fetchFirstEntry(member.guild, member.user, 'MemberKick');
const audit = await this._fetchFirstEntry(wrapper, member.user, 'MemberKick');
if (!audit) return undefined;
new Infraction(this.client, {
type: 'KICK',
@ -123,7 +123,7 @@ class AuditLogObserver extends Observer {
const mutedRole = settings.mute.role;
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;
let type = null;
@ -148,7 +148,8 @@ class AuditLogObserver extends Observer {
}
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];
const audit = await guild.fetchAuditLogs({ limit: 1, type });
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;
const missing = channel.permissionsFor(guild.members.me).missing('ManageMessages');
const missing = channel.permissionsFor(this.client.user).missing('ManageMessages');
if (missing.length) {
this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'word', permissions: missing });
return;
@ -432,7 +432,7 @@ module.exports = class AutoModeration extends Observer {
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) {
this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'link', permissions: missing });
return;
@ -454,9 +454,10 @@ module.exports = class AutoModeration extends Observer {
let log = `${guild.name} Link filter debug:`;
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
if (domain.toLowerCase() === 'discord.gg') continue;
if (domain === 'discord.gg') continue;
log += `\nMatched link ${match}: `;
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 settings = await wrapper.settings();
const { invitefilter: setting } = settings;
const { bypass, ignore, actions, silent, enabled, whitelist } = setting;
const { bypass, ignore, actions, silent, enabled, whitelist = [] } = setting;
if (!enabled) return;
const roles = member?.roles.cache.map((r) => r.id) || [];
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) {
this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'invite', permissions: missing });
return;
@ -613,7 +614,7 @@ module.exports = class AutoModeration extends Observer {
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) {
this.client.emit('filterMissingPermissions', { channel, guild: wrapper, filter: 'mention', permissions: missing });
return;

View File

@ -2,6 +2,9 @@ const { EmbedBuilder, Message, ChannelType, ComponentType, ButtonStyle } = requi
const { Util } = require('../../../utilities');
const { InvokerWrapper, MessageWrapper } = require('../../client/wrappers');
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 {
@ -29,8 +32,12 @@ class CommandHandler extends Observer {
|| message.author.bot
|| message.guild && !message.guild.available) return undefined;
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.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
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] });
await invoker.deferReply();
const response = await this._parseInteraction(interaction, command);
const response = await this._parseInteraction(invoker, command);
if (await this._parseResponse(invoker, response)) return;
try { // Temp logging
@ -91,8 +98,31 @@ class CommandHandler extends Observer {
}
_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;
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}`, {
option: response.option.name, min: response.option.minimum, max: response.option.maximum
});
@ -133,9 +163,7 @@ class CommandHandler extends Observer {
}
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;
const now = Date.now();
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;
if (invoker.subcommandGroup) debugstr += ` ${invoker.subcommandGroup.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);
invoker.command.success(now);
} 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;
const options = {};
@ -193,14 +221,11 @@ class CommandHandler extends Observer {
continue;
}
// const newOption = new CommandOption({
// name: matched.name, type: matched.type,
// minimum: matched.minimum, maximum: matched.maximum,
// _rawValue: option.value,
// dependsOn: matched.dependsOn, dependsOnMode: matched.dependsOnMode
// });
const newOption = matched.clone(option.value);
const parsed = await this._parseOption(interaction, newOption);
if (matched.guildOnly && !guild) return { error: true, params: { option: matched.name }, index: 'O_COMMANDHANDLER_GUILDONLY_OPT' };
const rawValue = matched.plural && typeof option.value === 'string' ? Util.parseQuotes(option.value).map(([x]) => x) : option.value;
const newOption = matched.clone(rawValue, guild, true);
const parsed = await newOption.parse();
// const parsed = await this._parseOption(interaction, newOption);
// console.log(parsed);
if(parsed.error) {
@ -211,7 +236,7 @@ class CommandHandler extends Observer {
break;
}
newOption.value = parsed.value;
// newOption.value = parsed.value;
options[matched.name] = newOption;
}
@ -223,18 +248,6 @@ class CommandHandler extends Observer {
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 {
error: false,
options
@ -244,13 +257,14 @@ class CommandHandler extends Observer {
async _parseMessage(invoker, params) {
const { command, target: message } = invoker;
const { command, target: message, guild } = invoker;
const { subcommands, subcommandGroups } = command;
const args = {};
// console.log(options);
let group = null,
subcommand = null;
// Parse out subcommands
if (subcommandGroups.length || subcommands.length) {
const [first, second, ...rest] = params;
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
// But for now I'm followin discord's structure for commands
if (!group) {
subcommand = command.subcommand(first)?.raw;
subcommand = command.subcommand(first);
params = [];
if (second) params.push(second, ...rest);
} else {
subcommand = command.subcommand(second)?.raw;
subcommand = command.subcommand(second);
params = rest;
}
message.subcommand = subcommand;
@ -275,18 +289,166 @@ class CommandHandler extends Observer {
const activeCommand = subcommand || command;
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) {
const { guild } = interaction;
const types = {
POINTS: (value) => {
return { value };
},
ROLES: async (string) => {
const args = Util.parseQuotes(string).map(([str]) => str);
const roles = await guild.resolveRoles(args);
@ -372,6 +534,7 @@ class CommandHandler extends Observer {
return { error: false, value: parseInt(integer) };
},
BOOLEAN: (boolean) => {
boolean = this.client.resolver.resolveBoolean(boolean);
return { error: false, value: boolean };
},
MEMBER: async (user) => {
@ -430,7 +593,7 @@ class CommandHandler extends Observer {
async _getCommand(message) {
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();
const userWrapper = await this.client.getUserWrapper(message.author.id);

View File

@ -1,5 +1,5 @@
/* eslint-disable no-labels */
const { WebhookClient, AttachmentBuilder } = require('discord.js');
const { WebhookClient, AttachmentBuilder, AuditLogEvent } = require('discord.js');
const { stripIndents } = require('common-tags');
const moment = require('moment');
const { inspect } = require('util');
@ -103,22 +103,23 @@ class GuildLogger extends Observer {
const hook = await guild.getWebhook('messages');
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' });
}
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;
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();
if (log) {
if (thread.id !== log.target.id) return;
actor = log.executor;
}
} 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();
if (log) {
if (thread.id !== log.target.id) return;
@ -178,7 +179,7 @@ class GuildLogger extends Observer {
const hook = await wrapper.getWebhook('messages');
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' });
}
@ -340,13 +341,13 @@ class GuildLogger extends Observer {
const { ignore, bypass } = chatlogs;
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)
return this.client.emit('logError', { guild: wrapper, logger: 'messageLogger', reason: 'MSGLOG_NO_PERMS', params: { missing: missing.join(', ') } });
const hook = await wrapper.getWebhook('messages');
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' });
}
@ -364,6 +365,7 @@ class GuildLogger extends Observer {
content = Util.escapeMarkdown(content);
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(() => {
return false;
});
@ -533,7 +535,7 @@ class GuildLogger extends Observer {
const hook = await wrapper.getWebhook('messages');
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' });
}
@ -710,7 +712,7 @@ class GuildLogger extends Observer {
async memberLeave(member) {
const { guild, guildWrapper: wrapper } = member;
const { guildWrapper: wrapper } = member;
const settings = await wrapper.settings();
const setting = settings.members;
if (!setting.channel || !setting.enabled) return;
@ -732,7 +734,7 @@ class GuildLogger extends Observer {
if (oldMember.nickname === newMember.nickname) return;
const { guild, user, guildWrapper: wrapper } = oldMember;
const { user, guildWrapper: wrapper } = oldMember;
const settings = await wrapper.settings();
const setting = settings.nicknames;
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 settings = await guild.settings();
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({
duration: { $gt: 0 },
@ -79,7 +80,7 @@ class UtilityHook extends Observer {
if (managed) {
await member.roles.add(setting.role, 'automute upon rejoin, type 1');
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) {
@ -100,7 +101,8 @@ class UtilityHook extends Observer {
const settings = await guild.settings();
const setting = settings.stickyrole;
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 });
if (!data) return;
@ -115,7 +117,8 @@ class UtilityHook extends Observer {
const settings = await guild.settings();
const setting = settings.autorole;
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 = _roles.map((r) => r.id);
@ -153,7 +156,8 @@ class UtilityHook extends Observer {
const { guild } = invite;
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();
guild.invites.set(invite.code, invite);
@ -163,7 +167,8 @@ class UtilityHook extends Observer {
const { guild } = invite;
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();
guild.invites.delete(invite.code);
@ -203,7 +208,8 @@ class UtilityHook extends Observer {
!selfrole.channel || selfrole.channel !== channel.id ||
!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)
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 { Setting, CommandOption } = require("../../../interfaces");
const { Setting } = require("../../../interfaces");
class IgnoreSetting extends Setting {
@ -19,7 +19,7 @@ class IgnoreSetting extends Setting {
bypass: { ARRAY: 'GUILD_ROLE' }
},
commandOptions: [
new CommandOption({
{
name: 'list',
description: 'List to act on',
type: 'STRING',
@ -27,9 +27,9 @@ class IgnoreSetting extends Setting {
{ name: 'channels', value: 'channels' },
{ name: 'bypass', value: 'bypass' }
],
dependsOn: ['method']
}),
new CommandOption({
dependsOn: ['method']//, valueAsAlias: true, flag: true
},
{
name: 'method',
description: 'Method of modifying',
type: 'STRING',
@ -39,8 +39,8 @@ class IgnoreSetting extends Setting {
{ name: 'set', value: 'set' },
{ 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 {
@ -16,15 +16,16 @@ class PermissionType extends Setting {
type: 'STRING'
},
commandOptions: [
new CommandOption({
{
type: 'STRING',
name: 'type',
description: 'Where to read permissions from',
choices: [
{ name: 'discord', value: 'discord' },
{ name: 'both', value: 'both' },
{ name: 'grant', value: 'grant' }
]
})
}
]
});

View File

@ -1,5 +1,5 @@
const { Util } = require("../../../../utilities");
const { Setting, CommandOption } = require("../../../interfaces");
const { Setting } = require("../../../interfaces");
class ProtectionSetting extends Setting {
@ -21,7 +21,7 @@ class ProtectionSetting extends Setting {
enabled: 'BOOLEAN'
},
commandOptions: [
new CommandOption({
{
type: 'STRING',
name: 'type',
description: 'Select protection type',
@ -30,8 +30,8 @@ class ProtectionSetting extends Setting {
{ name: 'position', value: 'position' }
],
dependsOn: []
}),
new CommandOption({
},
{
type: 'STRING',
name: 'roles',
description: 'Method of modifying',
@ -42,12 +42,12 @@ class ProtectionSetting extends Setting {
{ name: 'reset', value: 'reset' },
],
dependsOn: []
}),
new CommandOption({
},
{
type: 'BOOLEAN',
name: 'enabled',
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 {
@ -17,11 +17,11 @@ class SilentSetting extends Setting {
enabled: 'BOOLEAN'
},
commandOptions: [
new CommandOption({
type: 'BOOLEAN',
{
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'enabled',
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 = [
'NOTE',
'WARN',
@ -57,13 +57,13 @@ class DmInfraction extends Setting {
}
},
commandOptions: [
new CommandOption({
{
name: 'message',
description: 'Set the message for an infraction type',
type: 'STRING',
dependsOn: ['infraction']
}),
new CommandOption({
},
{
name: 'infraction',
description: 'Choose the infraction for which to modify the message',
type: 'STRING',
@ -71,8 +71,8 @@ class DmInfraction extends Setting {
return { name: inf, value: inf };
}),
dependsOn: ['message']
}),
new CommandOption({
},
{
name: 'infractions',
description: 'Modify the list of infractions that are sent',
type: 'STRING',
@ -82,15 +82,15 @@ class DmInfraction extends Setting {
{ name: 'set', value: 'set' },
{ name: 'reset', value: 'reset' },
]
}),
new CommandOption({
},
{
name: 'enabled',
description: 'Enable or disable the sending of infractions in DMs',
type: 'BOOLEAN'
}),
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'anonymous',
type: 'BOOLEAN',
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
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 {
@ -22,16 +22,16 @@ class MessageLog extends Setting {
types: { ARRAY: 'ERROR_TYPES' } // TODO: Error types
},
commandOptions: [
new CommandOption({
{
name: 'channel',
description: 'Channel in which to output logs',
type: 'TEXT_CHANNEL'
}),
new CommandOption({
},
{
name: 'enabled',
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) {
const { guild } = interaction;
if (opts.enabled?.value === false) setting.channel = null;
if (opts.channel) {
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']);
if (missingPerms.length) return {
error: true,

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces");
const { Setting } = require("../../../interfaces");
class MemberLog extends Setting {
@ -21,26 +21,26 @@ class MemberLog extends Setting {
leave: 'STRING'
},
commandOptions: [
new CommandOption({
{
name: 'enabled',
description: 'Enable/disable member logs',
type: 'BOOLEAN'
}),
new CommandOption({
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'channel',
description: 'Select the log output channel',
type: 'TEXT_CHANNEL'
}),
new CommandOption({
},
{
name: 'join',
description: 'Set the join message',
type: 'STRING'
}),
new CommandOption({
type: 'STRING', flag: true
},
{
name: 'leave',
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');
class MessageLog extends Setting {
@ -26,22 +26,22 @@ class MessageLog extends Setting {
attachments: 'BOOLEAN'
},
commandOptions: [
new CommandOption({
{
name: 'channel',
description: 'Channel in which to output logs',
type: 'TEXT_CHANNEL'
}),
new CommandOption({
},
{
name: 'enabled',
description: 'Toggle logging on or off',
type: 'BOOLEAN'
}),
new CommandOption({
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'attachments',
description: 'Whether to log attachments. PREMIUM TIER 1',
type: 'BOOLEAN'
}),
new CommandOption({
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'list',
description: 'Select which list to modify',
type: 'STRING',
@ -50,8 +50,8 @@ class MessageLog extends Setting {
{ name: 'ignore', value: 'ignore' },
],
dependsOn: ['method']
}),
new CommandOption({
},
{
name: 'method',
description: 'Select which modification method to use',
type: 'STRING',
@ -62,7 +62,7 @@ class MessageLog extends Setting {
{ name: 'reset', value: 'reset' },
],
dependsOn: ['list']
}),
},
]
});
@ -82,7 +82,7 @@ class MessageLog extends Setting {
if (opts.channel) {
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']);
if (missingPerms.length) return {
error: true,

View File

@ -1,5 +1,5 @@
const { Infractions } = require("../../../../constants/Constants");
const { Setting, CommandOption } = require("../../../interfaces");
const { Setting } = require("../../../interfaces");
// [
// 'NOTE',
@ -44,17 +44,17 @@ class ModerationLog extends Setting {
}
},
commandOptions: [
new CommandOption({
{
name: 'enabled',
description: 'Enable/disable member logs',
type: 'BOOLEAN'
}),
new CommandOption({
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'channel',
description: 'Logging channel',
type: 'TEXT_CHANNEL'
}),
new CommandOption({
},
{
name: 'infractions',
description: 'Modify the list of infractions that are sent',
type: 'STRING',
@ -64,10 +64,10 @@ class ModerationLog extends Setting {
{ name: 'set', value: 'set' },
{ name: 'reset', value: 'reset' },
]
}),
},
{
name: 'anonymous',
type: 'BOOLEAN',
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
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 {
@ -16,16 +16,16 @@ class Nicknames extends Setting {
channel: 'GUILD_TEXT'
},
commandOptions: [
new CommandOption({
{
name: 'enabled',
description: 'Toggle logging on or off',
type: 'BOOLEAN'
}),
new CommandOption({
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'channel',
type: 'TEXT_CHANNEL',
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 {
@ -15,16 +15,16 @@ class Voice extends Setting {
channel: 'GUILD_TEXT'
},
commandOptions: [
new CommandOption({
{
name: 'enabled',
description: 'Toggle logging on or off',
type: 'BOOLEAN'
}),
new CommandOption({
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'channel',
type: 'TEXT_CHANNEL',
description: 'Set the channel for voice join/leave logging'
})
}
]
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
const { Setting, CommandOption } = require("../../../interfaces");
const { Setting } = require("../../../interfaces");
const { Util } = require("../../../../utilities");
const INFRACTIONS = ['WARN', 'MUTE', 'KICK', 'SOFTBAN', 'BAN', 'VCMUTE', 'VCKICK', 'VCBAN'];
@ -37,22 +37,22 @@ class ModerationPoints extends Setting {
multiplier: false
},
commandOptions: [
new CommandOption({
{
name: 'points',
description: 'Point value',
type: 'INTEGER',
dependsOn: ['associate', 'type'],
dependsOn: ['associate', 'infraction'],
dependsOnMode: 'OR',
minimum: 0,
maximum: 100
}),
new CommandOption({
},
{
name: 'expire',
description: 'How long the points are counted for',
type: 'TIME',
dependsOn: ['type']
}),
new CommandOption({
dependsOn: ['infraction']
},
{
name: 'infraction',
description: 'Type of infraction',
type: 'STRING',
@ -61,36 +61,36 @@ class ModerationPoints extends Setting {
}),
dependsOn: ['points', 'expire'],
dependsOnMode: 'OR'
}),
new CommandOption({
},
{
name: 'enabled',
description: 'Toggle on or off',
type: 'BOOLEAN'
}),
new CommandOption({
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'multiplier',
description: 'Use points as a multiplier for the expiration',
type: 'BOOLEAN'
}),
new CommandOption({
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true
},
{
name: 'associate',
description: 'Associate a word within a reason to a point value',
type: 'STRING',
dependsOn: ['points']
}),
dependsOn: ['points'], flag: true
},
]
});
}
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 (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 (type && points) setting.points[type.value] = points.value;
if (infraction && points) setting.points[infraction.value] = points.value;
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 { Util } = require("../../../../utilities");
@ -52,28 +52,28 @@ class MuteSetting extends Setting {
permanent: 'BOOLEAN'
},
commandOptions: [
new CommandOption({
type: 'STRING',
{
type: 'STRING', flag: true,
name: 'create',
description: 'Create a mute role, mutually exclusive with role'
}),
new CommandOption({
},
{
type: 'ROLE',
name: 'role',
description: 'Select the role to use for mutes, mutually exclusive with create'
}),
new CommandOption({
type: 'TIME',
},
{
type: 'TIME', flag: true,
name: 'default',
description: 'Set the default duration for mutes'
}),
new CommandOption({
type: 'BOOLEAN',
},
{
type: 'BOOLEAN', flag: true, valueOptional: true, defaultValue: true,
name: 'permanent',
description: 'Whether to allow permanent mutes or fall back to default mute duration'
}),
new CommandOption({
type: 'INTEGER',
},
{
type: 'INTEGER', flag: true,
name: 'type',
description: 'Select the type of mute behaviour',
choices: [ {
@ -89,7 +89,7 @@ class MuteSetting extends Setting {
name: 'Type 3 (Use Discord timeouts)',
value: 3
} ]
})
}
]
});
@ -187,7 +187,8 @@ class MuteSetting extends Setting {
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 {
index: 'SETTING_MUTE_ROLEMISSINGPERMISSION',
error: true
@ -251,7 +252,7 @@ class MuteSetting extends Setting {
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 });
continue;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -53,7 +53,8 @@ class AddroleInfraction extends Infraction {
const { grantable } = await this.guild.settings();
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);
if (filtered.length === 0) {
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'
&& 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();
@ -56,13 +56,14 @@ class BanInfraction extends Infraction {
async verify() {
if (this.target instanceof GuildMember) {
if (!this.member.bannable) return super._fail('C_BAN_CANNOTBEBANNED');
}
const member = await this.guild.resolveMember(this.target.id);
if (member && !member.bannable)
return super._fail('C_BAN_CANNOTBEBANNED');
let alreadyBanned = null;
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
if (alreadyBanned) return super._fail('C_BAN_ALREADYBANNED');

View File

@ -140,9 +140,9 @@ class LockdownInfraction extends Infraction {
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']);
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();
}

View File

@ -48,6 +48,8 @@ class MuteInfraction extends Infraction {
role = await this.client.resolver.resolveRole(setting.role, true, this.guild);
}
const me = await this.guild.resolveMember(this.client.user);
let removed = [];
switch (setting.type) {
case 0:
@ -60,12 +62,12 @@ class MuteInfraction extends Infraction {
break;
case 1:
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);
try {
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).values(),
role
], this._reason);
@ -76,11 +78,11 @@ class MuteInfraction extends Infraction {
break;
case 2:
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);
try {
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);
} catch (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 = {
removedRoles: removed.map((r) => r.id),
muteType: setting.type,
@ -107,7 +111,7 @@ class MuteInfraction extends Infraction {
if (callback) {
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));
@ -134,10 +138,12 @@ class MuteInfraction extends Infraction {
return this._fail('COMMAND_MUTE_INVALIDMUTEROLE', true);
}
}
const me = await this.guild.resolveMember(this.client.user);
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)
} 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();

View File

@ -82,7 +82,8 @@ class NicknameInfraction extends Infraction {
async verify() {
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');
}

View File

@ -53,7 +53,8 @@ class RemoveroleInfraction extends Infraction {
const { grantable } = await this.guild.settings();
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);
if (filtered.length === 0) {
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'
&& 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();
return this._succeed();

View File

@ -95,9 +95,9 @@ class UnlockdownInfraction extends Infraction {
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']);
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();
}

View File

@ -1,5 +1,7 @@
/* eslint-disable camelcase */
const { ChannelType } = require("discord.js");
const Constants = {
CommandOptionTypes: {
SUB_COMMAND: 1,
@ -28,7 +30,8 @@ const Constants = {
ROLE: 8,
MENTIONABLE: 9,
NUMBER: 10,
FLOAT: 10
FLOAT: 10,
POINTS: 4
},
ChannelTypes: {
TEXT_CHANNEL: 0,
@ -36,12 +39,17 @@ const Constants = {
}
};
const PointsReg = /^([-+]?[0-9]+) ?(points|point|pts|pt|p)$/iu;
class CommandOption {
constructor(options = {}) {
this._options = options;
this.name = options.name;
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.required = Boolean(options.required);
@ -52,8 +60,10 @@ class CommandOption {
if (options.options)
for (const opt of options.options) {
// console.log(opt);
if (opt instanceof CommandOption) this.options.push(opt);
else if (opt.name instanceof Array) {
if (opt instanceof CommandOption) {
opt.client = this.client;
this.options.push(opt);
} else if (opt.name instanceof Array) {
const { name: names, description, type, dependsOn, ...opts } = opt;
for (const name of names) {
// console.log(name);
@ -69,9 +79,12 @@ class CommandOption {
if (dependsOn instanceof Array) {
_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.
@ -83,32 +96,51 @@ class CommandOption {
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.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;
// 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
}
usage(guild) {
const name = `${this.name.toUpperCase()} [${this.type}]`;
get guildOnly() {
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;
const format = (...args) => guild ? guild.format(...args) : this.client.format(...args);
if (this.type === 'SUB_COMMAND_GROUP') {
value = this.options.map((opt) => {
const usage = opt.usage(guild);
const usage = opt.usage(guild, true);
return `__${usage.name.replace('》', '').trim()}__\n${usage.value}`;
}).join('\n\n');
} else if (this.type === 'SUB_COMMAND') {
if (!this.options.length) value = guild.format('GENERAL_NO_ARGS');
else value = this.options.map((opt) => opt.usage(guild).value).join('\n');
if (!this.options.length) value = format('GENERAL_NO_ARGS');
else value = this.options.map((opt) => opt.usage(guild, true).value).join('\n');
} 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)
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)
value += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: this.dependsOn.join('`, `') })}`;
value += `\n${format('GENERAL_DEPENDSON', { dependencies: this.dependsOn.join('`, `') })}`;
if (this.minimum !== undefined)
value += `\nMIN: \`${this.minimum}\``;
if (this.maximum !== undefined) {
@ -130,20 +162,268 @@ class CommandOption {
* @return {CommandOption}
* @memberof CommandOption
*/
clone(value) {
clone(_rawValue, guild, slashOption = false) {
return new CommandOption({
name: this.name, type: this.type,
minimum: this.minimum, maximum: this.maximum,
dependsOn: this.dependsOn, dependsOnMode: this.dependsOnMode,
_rawValue: value, strict: this.strict
...this._options, _rawValue, guild, slashOption
});
}
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() {
return {
name: this.name,
type: this.type,
options: []
options: this.options.map((opt) => opt.raw)
};
}

View File

@ -9,6 +9,7 @@ const {
} = require('../../constants');
const { Util } = require('../../utilities');
const { inspect } = require('util');
const Constants = {
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;
return this.client.storageManager.mongodb.infractions.updateOne(filter, this.json)
.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') {
const executorHighest = executor.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');
}
} else if (protection.type === 'role') {
const contains = target.roles.cache.some((r) => protection.roles.includes(r.id));
if (contains) {

View File

@ -7,7 +7,6 @@ const Component = require("./Component.js");
// Imports to enable JSDocs typing
// eslint-disable-next-line no-unused-vars
const InteractionWrapper = require("../client/wrappers/InteractionWrapper.js");
const CommandOption = require('./CommandOption.js');
// eslint-disable-next-line no-unused-vars
// const { DiscordClient } = require("../DiscordClient.js");
@ -70,29 +69,9 @@ class Setting extends Component {
this.default = { [this.name]: options.default || {} };
this.definitions = options.definitions || {}; // Used for the API for field definitions
this.commandOptions = [];
this.commandOptions = options.commandOptions || [];
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.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({
name: `${guild.format(`GENERAL_OPTIONS`)}`,
value: options.map(
(opt) => {
let msg = `**${opt.name} [${opt.type}]:** ${opt.description}`;
if (opt.choices.length)
msg += `\n__${guild.format('GENERAL_CHOICES')}__: ${opt.choices.map((choice) => choice.name).join(', ')}`;
if (opt.dependsOn.length)
msg += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: opt.dependsOn.join('`, `') })}`;
if (opt.minimum !== undefined)
msg += `\nMIN: \`${opt.minimum}\``;
if (opt.maximum !== undefined) {
const newline = opt.minimum !== undefined ? ', ' : '\n';
msg += `${newline}MAX: \`${opt.maximum}\``;
}
return msg;
}
).join('\n\n')
value: options.map((opt) => opt.usage(guild, true)).map((f) => f.value).join('\n\n')
// value: options.map(
// (opt) => {
// let msg = `**${opt.name} [${opt.type}]:** ${opt.description}`;
// if (opt.choices.length)
// msg += `\n__${guild.format('GENERAL_CHOICES')}__: ${opt.choices.map((choice) => choice.name).join(', ')}`;
// if (opt.dependsOn.length)
// msg += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: opt.dependsOn.join('`, `') })}`;
// if (opt.minimum !== undefined)
// msg += `\nMIN: \`${opt.minimum}\``;
// if (opt.maximum !== undefined) {
// const newline = opt.minimum !== undefined ? ', ' : '\n';
// msg += `${newline}MAX: \`${opt.maximum}\``;
// }
// return msg;
// }
// ).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');
const response = await invoker.promptMessage(
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') };
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);
if (['cancel', 'abort', 'exit'].includes(content)) return {
error: true,
message: invoker.format('ERR_CANCEL')
content: invoker.format('ERR_CANCEL')
};
else if (!content.length) return { error: true, index: 'SETTING_NOCONTENT' };
@ -241,7 +221,8 @@ class Setting extends Component {
for (const param of params) {
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);
}

View File

@ -51,8 +51,10 @@ class Command extends Component {
this.options = [];
if (options.options) for (const opt of options.options) {
if (opt instanceof CommandOption) this.options.push(opt);
else if (opt.name instanceof Array) {
if (opt instanceof CommandOption) {
opt.client = client;
this.options.push(opt);
} else if (opt.name instanceof Array) {
// Allows easy templating of subcommands that share arguments
const { name: names, description, type, ...opts } = opt;
for (const name of names) {
@ -64,9 +66,9 @@ class Command extends Component {
_type = type[index];
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) => {
@ -107,7 +109,11 @@ class Command extends Component {
const fields = [];
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 (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 required = [this.resolveable, ...this.memberPermissions];
fields.push({
name: `${guild.format('GENERAL_PERMISSIONS')}`,
name: `${format('GENERAL_PERMISSIONS')}`,
value: `\`${required.join('`, `')}\``
});
}
@ -135,7 +141,7 @@ class Command extends Component {
author: {
name: `${this.name} [module:${this.module.name}]`
},
description: guild.format(`COMMAND_${this.name.toUpperCase()}_HELP`),
description: format(`COMMAND_${this.name.toUpperCase()}_HELP`),
fields
});

View File

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

View File

@ -10,26 +10,16 @@ class SettingsCommand extends SlashCommand {
super(client, {
...options,
guildOnly: true,
memberPermissions: ['ManageGuild']
});
/*
{
name: 'settings',
description: "Configure the bot's behaviour in your server",
module: 'administration',
memberPermissions: ['ManageGuild'],
options: [
],
guildOnly: true
}
*/
this.options.push(new CommandOption({
type: 'SUB_COMMAND',
name: 'list',
description: 'List available settings'
}));
{
type: 'SUB_COMMAND',
name: 'list',
description: 'List available settings'
}
]
});
this.build();
}
@ -50,9 +40,12 @@ class SettingsCommand extends SlashCommand {
name: setting.name,
description: setting.description,
type: setting.commandType, //'SUB_COMMAND',
options: setting.commandOptions
options: setting.commandOptions,
client: this.client
});
this.options.push(subCommand);
// Overwrite the setting options with the built CommandOption structures
setting.commandOptions = subCommand.options;
}
// 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.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({
emoji: 'failure',
index: 'SETTING_MISSING_CLIENTPERMISSIONS',

View File

@ -197,6 +197,8 @@ module.exports = class FilterUtility {
//Zero width character (UTF-16 8206)
content = content.replace(//gu, '');
content = content.replace(/['"‘’“”]/gu, '');
//Replace the weird letters with their normal text counterparts
// eslint-disable-next-line no-useless-escape
const match = (/[a-z0-9\w\(\)\.\\\/\?!]+/gimu).exec(content);
@ -321,7 +323,9 @@ module.exports = class FilterUtility {
matched: true,
_matcher: _word,
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,
welcomer: result.welcomer,
commands: { disabled: result.disabledCommands, custom: {} },
prefix: result.prefix
textcommands: { prefix: result.prefix, enabled: false }
};
return settings;
}
@ -287,14 +287,15 @@ class SettingsMigrator {
};
if (linkfilter) settings.linkfilter = {
enabled: result.linkfilter?.enabled || false,
silent: result.linkfilter?.silent || false,
ignore: result.linkfilter?.channels || [],
bypass: result.linkfilter?.roles || [],
enabled: linkfilter.enabled || false,
silent: linkfilter.silent || false,
ignore: linkfilter.channels || [],
bypass: linkfilter.roles || [],
actions: [],
whitelist: result.linkfilter?.whitelist === true ? result.linkfilter.filter : [],
blacklist: result.linkfilter?.whitelist === false ? result.linkfilter.filter : [],
greylist: []
whitelist: linkfilter.whitelist === true ? linkfilter.filter : [],
blacklist: linkfilter.whitelist === false ? linkfilter.filter : [],
greylist: [],
whitelistMode: linkfilter.whitelist || false
};
if (ignore) settings.ignore = {
@ -334,7 +335,7 @@ class SettingsMigrator {
};
if (autorole) settings.autorole = autorole;
if (prefix) settings.prefix = prefix;
if (prefix) settings.textcommands = { prefix, enabled: false };
if (welcomer) {
settings.welcomer.enabled = welcomer.enabled;
settings.welcomer.message = welcomer.message || null;
@ -398,7 +399,8 @@ class SettingsMigrator {
silent: false,
bypass: result.invitefilter.roles,
enabled: result.invitefilter.enabled,
actions: []
actions: [],
whitelist: []
};
const channels = Object.entries(invitefilter.channels || {});
for (const [id, value] of channels) {