aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/dev/eval.ts2
-rw-r--r--src/commands/dev/reload.ts6
-rw-r--r--src/commands/dev/testDuration.ts54
-rw-r--r--src/commands/moderation/kick.ts2
-rw-r--r--src/commands/moderation/mute.ts171
-rw-r--r--src/lib/extensions/discord-akairo/BushClient.ts14
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts106
-rw-r--r--src/lib/extensions/discord.js/BushGuildMember.ts62
-rw-r--r--src/lib/models/Ban.ts10
-rw-r--r--src/lib/models/Guild.ts8
-rw-r--r--src/lib/models/ModLog.ts11
-rw-r--r--src/lib/models/Mute.ts10
-rw-r--r--src/lib/models/PunishmentRole.ts10
-rw-r--r--src/lib/utils/BushConstants.ts24
-rw-r--r--src/listeners/commands/commandError.ts17
-rw-r--r--src/listeners/commands/commandMissingPermissions.ts12
-rw-r--r--src/listeners/commands/slashCommandError.ts17
17 files changed, 338 insertions, 198 deletions
diff --git a/src/commands/dev/eval.ts b/src/commands/dev/eval.ts
index 4896945..ebf055b 100644
--- a/src/commands/dev/eval.ts
+++ b/src/commands/dev/eval.ts
@@ -259,7 +259,7 @@ export default class EvalCommand extends BushCommand {
} else {
embed.addField('📥 Input', await this.client.util.codeblock(inputJS, 1024, 'js'));
}
- embed.addField('📤 Output', await this.client.util.codeblock(e?.stack, 1024, 'js'));
+ embed.addField('📤 Output', await this.client.util.codeblock(typeof e === 'object' ? e?.stack : e, 1024, 'js'));
}
if (!args.silent && !message.util.isSlash) {
await message.util.reply({ embeds: [embed], ephemeral: args.silent });
diff --git a/src/commands/dev/reload.ts b/src/commands/dev/reload.ts
index d932816..4a69098 100644
--- a/src/commands/dev/reload.ts
+++ b/src/commands/dev/reload.ts
@@ -46,7 +46,11 @@ export default class ReloadCommand extends BushCommand {
return message.util.send(`🔁 Successfully reloaded! (${new Date().getTime() - s.getTime()}ms)`);
} catch (e) {
return message.util.send(
- `An error occurred while reloading:\n${await this.client.util.codeblock(e.stack, 2048 - 34, 'js')}`
+ `An error occurred while reloading:\n${await this.client.util.codeblock(
+ typeof e === 'object' ? e?.stack : e,
+ 2048 - 34,
+ 'js'
+ )}`
);
}
}
diff --git a/src/commands/dev/testDuration.ts b/src/commands/dev/testDuration.ts
new file mode 100644
index 0000000..bf30840
--- /dev/null
+++ b/src/commands/dev/testDuration.ts
@@ -0,0 +1,54 @@
+import { stripIndents } from 'common-tags';
+import { Message } from 'discord.js';
+import { BushCommand } from '../../lib/extensions/discord-akairo/BushCommand';
+import { BushSlashMessage } from '../../lib/extensions/discord-akairo/BushSlashMessage';
+
+export default class TestDurationCommand extends BushCommand {
+ public constructor() {
+ super('testduration', {
+ aliases: ['testduration'],
+ category: 'dev',
+ description: {
+ content: 'Tests duration parsing.',
+ usage: 'testduration [reason]',
+ examples: ['testduration']
+ },
+ args: [
+ {
+ id: 'reason',
+ type: 'contentWithDuration',
+ match: 'rest',
+ prompt: {
+ start: 'Enter text and a duration here.',
+ retry: '{error} Error parsing duration and text.',
+ optional: true
+ }
+ }
+ ],
+ slash: true,
+ slashOptions: [
+ {
+ type: 'STRING',
+ name: 'reason',
+ description: 'Enter text and a duration here.',
+ required: false
+ }
+ ],
+ hidden: true,
+ ownerOnly: true
+ });
+ }
+
+ async exec(
+ message: Message | BushSlashMessage,
+ { reason }: { reason?: { duration: number; contentWithoutTime: string } }
+ ): Promise<unknown> {
+ const rawDuration = reason.duration;
+ const text = reason.contentWithoutTime;
+ const humanizedDuration = this.client.util.humanizeDuration(rawDuration);
+ return await message.util.reply(stripIndents`
+ **rawDuration:** ${rawDuration}
+ **text:** ${text}
+ **humanizedDuration:** ${humanizedDuration}`);
+ }
+}
diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts
index df538bc..f88819b 100644
--- a/src/commands/moderation/kick.ts
+++ b/src/commands/moderation/kick.ts
@@ -76,7 +76,7 @@ export default class KickCommand extends BushCommand {
// });
// await modlogEnry.save();
// } catch (e) {
- // this.client.console.error(`KickCommand`, `Error saving to database. ${e?.stack}`);
+ // this.client.console.error(`KickCommand`, `Error saving to database. ${typeof e === 'object' ? e?.stack : e}`);
// yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`;
// return;
// }
diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts
index ffad432..8ac77a7 100644
--- a/src/commands/moderation/mute.ts
+++ b/src/commands/moderation/mute.ts
@@ -53,136 +53,83 @@ export default class MuteCommand extends BushCommand {
slash: true
});
}
- // async *genResponses(
- // message: Message | CommandInteraction,
- // user: User,
- // reason?: string,
- // time?: number
- // ): AsyncIterable<string> {
- // const duration = moment.duration(time);
- // let modlogEnry: ModLog;
- // let muteEntry: Mute;
- // // Create guild entry so postgres doesn't get mad when I try and add a modlog entry
- // await Guild.findOrCreate({
- // where: {
- // id: message.guild.id
- // },
- // defaults: {
- // id: message.guild.id
- // }
- // });
- // try {
- // const muteRole = (await Guild.findByPk(message.guild.id)).get('muteRole');
- // try {
- // if (time) {
- // modlogEnry = ModLog.build({
- // user: user.id,
- // guild: message.guild.id,
- // reason,
- // type: ModLogType.TEMP_MUTE,
- // duration: duration.asMilliseconds(),
- // moderator: message instanceof CommandInteraction ? message.user.id : message.author.id
- // });
- // muteEntry = Mute.build({
- // user: user.id,
- // guild: message.guild.id,
- // reason,
- // expires: new Date(new Date().getTime() + duration.asMilliseconds()),
- // modlog: modlogEnry.id
- // });
- // } else {
- // modlogEnry = ModLog.build({
- // user: user.id,
- // guild: message.guild.id,
- // reason,
- // type: ModLogType.MUTE,
- // moderator: message instanceof CommandInteraction ? message.user.id : message.author.id
- // });
- // muteEntry = Mute.build({
- // user: user.id,
- // guild: message.guild.id,
- // reason,
- // modlog: modlogEnry.id
- // });
- // }
- // await modlogEnry.save();
- // await muteEntry.save();
- // } catch (e) {
- // this.client.console.error(`MuteCommand`, `Error saving to database. ${e?.stack}`);
- // yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`;
- // return;
- // }
- // try {
- // await user.send(
- // `You were muted in ${message.guild.name} ${time ? `for ${duration.humanize()}` : 'permanently'} with reason \`${
- // reason || 'No reason given'
- // }\``
- // );
- // } catch (e) {
- // yield `${this.client.util.emojis.warn} Unable to dm user`;
- // }
- // await (
- // await message.guild.members.fetch(user)
- // ).roles.add(
- // muteRole,
- // `Muted by ${message instanceof CommandInteraction ? message.user.tag : message.author.tag} with ${
- // reason ? `reason ${reason}` : 'no reason'
- // }`
- // );
- // yield `${this.client.util.emojis.success} muted <@!${user.id}> ${
- // time ? `for ${duration.humanize()}` : 'permanently'
- // } with reason \`${reason || 'No reason given'}\``;
- // } catch {
- // yield `${this.client.util.emojis.error} Error muting :/`;
- // await muteEntry.destroy();
- // await modlogEnry.destroy();
- // return;
- // }
- // }
async exec(
message: BushMessage,
{ user, reason }: { user: BushUser; reason?: { duration: number; contentWithoutTime: string } }
): Promise<unknown> {
- return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`);
- // this.client.console.debug(reason);
-
- // if (typeof time === 'string') {
- // time = (await Argument.cast('duration', this.client.commandHandler.resolver, message, time)) as number;
- // }
- // for await (const response of this.genResponses(message, user, reason.join(' '), time)) {
- // await message.util.sendNew(response);
- // }
-
+ const error = this.client.util.emojis.error;
const member = message.guild.members.cache.get(user.id) as BushGuildMember;
- if (!this.client.util.moderatorCanModerateUser(message.member, member)) {
- return message.util.reply({
- content: `${this.client.util.emojis.error} You cannot mute **${member.user.tag}**.`
- });
+ const canModerateResponse = this.client.util.moderationPermissionCheck(message.member, member);
+ const victimBoldTag = `**${member.user.tag}**`;
+ switch (canModerateResponse) {
+ case 'moderator':
+ return message.util.reply(`${error} You cannot mute ${victimBoldTag} because they are a moderator.`);
+ case 'user hierarchy':
+ return message.util.reply(
+ `${error} You cannot mute ${victimBoldTag} because they have higher or equal role hierarchy as you do.`
+ );
+ case 'client hierarchy':
+ return message.util.reply(
+ `${error} You cannot mute ${victimBoldTag} because they have higher or equal role hierarchy as I do.`
+ );
+ case 'self':
+ return message.util.reply(`${error} You cannot mute yourself.`);
}
- const time =
- typeof reason === 'string'
- ? //@ts-ignore: you are unreachable bitch
- await Argument.cast('duration', this.client.commandHandler.resolver, message, reason)
- : reason.duration;
+ let time;
+ if (reason) {
+ time =
+ typeof reason === 'string'
+ ? await Argument.cast('duration', this.client.commandHandler.resolver, message, reason)
+ : reason.duration;
+ }
const parsedReason = reason.contentWithoutTime;
const response = await member.mute({
reason: parsedReason,
moderator: message.author,
- duration: time,
- createModLogEntry: true
+ duration: time
});
switch (response) {
- case 'success':
- return message.util.reply(`${this.client.util.emojis.success} Successfully muted **${member.user.tag}**.`);
+ case 'missing permissions':
+ return message.util.reply(
+ `${error} Could not mute ${victimBoldTag} because I am missing the \`Manage Roles\` permission.`
+ );
case 'no mute role':
return message.util.reply(
- `${this.client.util.emojis.error} Could not mute **${
- member.user.tag
- }**, you must set a mute role with ${message.guild.getSetting('prefix')}.`
+ `${error} Could not mute ${victimBoldTag}, you must set a mute role with \`${message.guild.getSetting(
+ 'prefix'
+ )}muterole\`.`
);
+ case 'invalid mute role':
+ return message.util.reply(
+ `${error} Could not mute ${victimBoldTag} because the current mute role no longer exists. Please set a new mute role with \`${message.guild.getSetting(
+ 'prefix'
+ )}muterole\`.`
+ );
+ case 'mute role not manageable':
+ return message.util.reply(
+ `${error} Could not mute ${victimBoldTag} because I cannot assign the current mute role, either change the role's position or set a new mute role with \`${message.guild.getSetting(
+ 'prefix'
+ )}muterole\`.`
+ );
+ case 'error giving mute role':
+ return message.util.reply(`${error} Could not mute ${victimBoldTag}, there was an error assigning them the mute role.`);
+ case 'error creating modlog entry':
+ return message.util.reply(
+ `${error} While muting ${victimBoldTag}, there was an error creating a modlog entry, please report this to my developers.`
+ );
+ case 'error creating mute entry':
+ return message.util.reply(
+ `${error} While muting ${victimBoldTag}, there was an error creating a mute entry, please report this to my developers.`
+ );
+ case 'failed to dm':
+ return message.util.reply(
+ `${this.client.util.emojis.warn} Muted **${member.user.tag}** however I could not send them a dm.`
+ );
+ case 'success':
+ return message.util.reply(`${this.client.util.emojis.success} Successfully muted **${member.user.tag}**.`);
}
}
}
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts
index 6911573..ed5c90a 100644
--- a/src/lib/extensions/discord-akairo/BushClient.ts
+++ b/src/lib/extensions/discord-akairo/BushClient.ts
@@ -216,7 +216,11 @@ export class BushClient extends AkairoClient {
loaders[loader].loadAll();
await this.logger.success('Startup', `Successfully loaded <<${loader}>>.`, false);
} catch (e) {
- await this.logger.error('Startup', `Unable to load loader <<${loader}>> with error:\n${e?.stack}`, false);
+ await this.logger.error(
+ 'Startup',
+ `Unable to load loader <<${loader}>> with error:\n${typeof e === 'object' ? e?.stack : e}`,
+ false
+ );
}
}
await this.dbPreInit();
@@ -237,8 +241,12 @@ export class BushClient extends AkairoClient {
Models.StickyRole.initModel(this.db);
await this.db.sync({ alter: true }); // Sync all tables to fix everything if updated
await this.console.success('Startup', `Successfully connected to <<database>>.`, false);
- } catch (error) {
- await this.console.error('Startup', `Failed to connect to <<database>> with error:\n` + error?.stack, false);
+ } catch (e) {
+ await this.console.error(
+ 'Startup',
+ `Failed to connect to <<database>> with error:\n` + typeof e === 'object' ? e?.stack : e,
+ false
+ );
}
}
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
index 9289598..5a22efc 100644
--- a/src/lib/extensions/discord-akairo/BushClientUtil.ts
+++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts
@@ -24,8 +24,9 @@ import {
WebhookEditMessageOptions
} from 'discord.js';
import got from 'got';
+import humanizeDuration from 'humanize-duration';
import { promisify } from 'util';
-import { Global, Guild, ModLog, ModLogType } from '../../models';
+import { Ban, Global, Guild, ModLog, ModLogType, Mute, PunishmentRole } from '../../models';
import { BushCache } from '../../utils/BushCache';
import { BushConstants } from '../../utils/BushConstants';
import { BushGuildResolvable } from '../discord.js/BushCommandInteraction';
@@ -302,7 +303,7 @@ export class BushClientUtil extends ClientUtil {
});
const filter = (interaction: ButtonInteraction) =>
interaction.customID.startsWith('paginate_') && interaction.message == msg;
- const collector = msg.createMessageComponentInteractionCollector({ filter, time: 300000 });
+ const collector = msg.createMessageComponentCollector({ filter, time: 300000 });
collector.on('collect', async (interaction: MessageComponentInteraction) => {
if (interaction.user.id == message.author.id || this.client.config.owners.includes(interaction.user.id)) {
switch (interaction.customID) {
@@ -391,7 +392,7 @@ export class BushClientUtil extends ClientUtil {
updateOptions();
const msg = await message.util.reply(options as MessageOptions & { split?: false });
const filter = (interaction: ButtonInteraction) => interaction.customID == 'paginate__stop' && interaction.message == msg;
- const collector = msg.createMessageComponentInteractionCollector({ filter, time: 300000 });
+ const collector = msg.createMessageComponentCollector({ filter, time: 300000 });
collector.on('collect', async (interaction: MessageComponentInteraction) => {
if (interaction.user.id == message.author.id || this.client.config.owners.includes(interaction.user.id)) {
await interaction.deferUpdate().catch(() => undefined);
@@ -530,23 +531,36 @@ export class BushClientUtil extends ClientUtil {
return newArray;
}
- public parseDuration(content: string): { duration: number; contentWithoutTime: string } {
+ public parseDuration(content: string, remove = true): { duration: number; contentWithoutTime: string } {
if (!content) return { duration: 0, contentWithoutTime: null };
- let duration = 0,
- contentWithoutTime = content;
+ let duration = 0;
+ // Try to reduce false positives by requiring a space before the duration, this makes sure it still matches if it is
+ // in the beginning of the argument
+ let contentWithoutTime = ` ${content}`;
- const regexString = Object.entries(BushConstants.TimeUnits)
- .map(([name, { label }]) => String.raw`(?:(?<${name}>-?(?:\d+)?\.?\d+) *${label})?`)
- .join('\\s*');
- const match = new RegExp(`^${regexString}$`, 'im').exec(content);
- if (!match) return null;
+ for (const unit in BushConstants.TimeUnits) {
+ const regex = BushConstants.TimeUnits[unit].match;
+ const match = regex.exec(contentWithoutTime);
+ const value = Number(match?.groups?.[unit] || 0);
+ // this.client.console.debug(unit + ': ' + value);
+ duration += value * BushConstants.TimeUnits[unit].value;
- for (const key in match.groups) {
- contentWithoutTime = contentWithoutTime.replace(match.groups[key], '');
- const value = Number(match.groups[key] || 0);
- duration += value * BushConstants.TimeUnits[key].value;
+ if (remove) contentWithoutTime = contentWithoutTime.replace(regex, '');
+ // this.client.console.debug(contentWithoutTime);
}
+ //^(?:(?<years>-?(?:\d+)?\.?\d+) *(?:years?|y))?\s*(?:(?<months>-?(?:\d+)?\.?\d+) *(?:months?|mon|mo?))?\s*(?:(?<weeks>-?(?:\d+)?\.?\d+) *(?:weeks?|w))?\s*(?:(?<days>-?(?:\d+)?\.?\d+) *(?:days?|d))?\s*(?:(?<hours>-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))?\s*(?:(?<minutes>-?(?:\d+)?\\.?\\d+) *(?:minutes?|mins?))?\s*(?:(?<seconds>-?(?:\d+)?\\.?\d+) *(?:seconds?|secs?|s))?\s*(?:(?<milliseconds>-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))?$
+ // const regexString = Object.entries(BushConstants.TimeUnits)
+ // .map(([name, { label }]) => String.raw`(?: (?<${name}>-?(?:\d+)?\.?\d+) *${label})`)
+ // .join(' |');
+ // const match = new RegExp(`^${regexString}$`, 'img').exec(' ' + content + ' ');
+ // if (!match) return null;
+ // console.
+ // const contentWithoutTime = content.replace(new RegExp(`^${regexString}$`, 'img'), '');
+ // for (const key in match.groups) {
+ // const value = Number(match.groups[key] || 0);
+ // duration += value * BushConstants.TimeUnits[key].value;
+ // }
return { duration, contentWithoutTime };
}
@@ -555,11 +569,20 @@ export class BushClientUtil extends ClientUtil {
* Checks if a moderator can perform a moderation action on another user.
* @param moderator - The person trying to perform the action.
* @param victim - The person getting punished.
+ * @param checkModerator - Whether or not to check if the victim is a moderator.
*/
- public moderatorCanModerateUser(moderator: BushGuildMember, victim: BushGuildMember): boolean {
- throw 'not implemented';
+ public moderationPermissionCheck(
+ moderator: BushGuildMember,
+ victim: BushGuildMember,
+ checkModerator = true
+ ): true | 'user hierarchy' | 'client hierarchy' | 'moderator' | 'self' {
if (moderator.guild.id !== victim.guild.id) throw 'wtf';
- if (moderator.guild.ownerID === moderator.id) return true;
+ const isOwner = moderator.guild.ownerID === moderator.id;
+ if (moderator.id === victim.id) return 'self';
+ if (moderator.roles.highest.position <= victim.roles.highest.position && !isOwner) return 'user hierarchy';
+ if (victim.roles.highest.position >= victim.guild.me.roles.highest.position) return 'client hierarchy';
+ if (checkModerator && victim.permissions.has('MANAGE_MESSAGES')) return 'moderator';
+ return true;
}
public async createModLogEntry(options: {
@@ -569,10 +592,11 @@ export class BushClientUtil extends ClientUtil {
reason: string;
duration: number;
guild: BushGuildResolvable;
- }): Promise<void> {
+ }): Promise<ModLog> {
const user = this.client.users.resolveID(options.user);
const moderator = this.client.users.resolveID(options.moderator);
const guild = this.client.guilds.resolveID(options.guild);
+ const duration = options.duration || null;
// If guild does not exist create it so the modlog can reference a guild.
await Guild.findOrCreate({
@@ -589,9 +613,49 @@ export class BushClientUtil extends ClientUtil {
user,
moderator,
reason: options.reason,
- duration: options.duration,
+ duration: duration,
guild
});
- await modLogEntry.save();
+ return modLogEntry.save().catch((err) => {
+ this.client.console.error('createModLogEntry', err);
+ return null;
+ });
+ }
+
+ public async createPunishmentEntry(options: {
+ type: 'mute' | 'ban' | 'role';
+ user: BushGuildMemberResolvable;
+ duration: number;
+ guild: BushGuildResolvable;
+ modlog: string;
+ }): Promise<Mute | Ban | PunishmentRole> {
+ let dbModel: typeof Mute | typeof Ban | typeof PunishmentRole;
+ switch (options.type) {
+ case 'mute':
+ dbModel = Mute;
+ break;
+ case 'ban':
+ dbModel = Ban;
+ break;
+ case 'role':
+ dbModel = PunishmentRole;
+ break;
+ default:
+ throw 'choose a valid punishment entry type';
+ }
+
+ const expires = options.duration ? new Date(new Date().getTime() + options.duration) : null;
+ const user = this.client.users.resolveID(options.user);
+ const guild = this.client.guilds.resolveID(options.guild);
+
+ const entry = dbModel.build({ user, guild, expires, modlog: options.modlog });
+ return await entry.save().catch((err) => {
+ this.client.console.error('createPunishmentEntry', err);
+ return null;
+ });
+ }
+
+ public humanizeDuration(duration: number): string {
+ return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2 });
}
}
diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts
index 59dc777..2fefcdd 100644
--- a/src/lib/extensions/discord.js/BushGuildMember.ts
+++ b/src/lib/extensions/discord.js/BushGuildMember.ts
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { GuildMember } from 'discord.js';
+import { ModLogType } from '../../models';
import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient';
import { BushGuild } from './BushGuild';
import { BushUser } from './BushUser';
@@ -7,7 +8,6 @@ import { BushUser } from './BushUser';
interface BushPunishmentOptions {
reason?: string;
moderator: BushUserResolvable;
- createModLogEntry?: boolean;
}
interface BushTimedPunishmentOptions extends BushPunishmentOptions {
@@ -18,7 +18,16 @@ type PunishmentResponse = 'success';
type WarnResponse = PunishmentResponse;
-type MuteResponse = PunishmentResponse | 'no mute role';
+type MuteResponse =
+ | PunishmentResponse
+ | 'missing permissions'
+ | 'no mute role'
+ | 'invalid mute role'
+ | 'mute role not manageable'
+ | 'error giving mute role'
+ | 'error creating modlog entry'
+ | 'error creating mute entry'
+ | 'failed to dm';
type UnmuteResponse = PunishmentResponse;
@@ -44,7 +53,54 @@ export class BushGuildMember extends GuildMember {
}
public async mute(options: BushTimedPunishmentOptions): Promise<MuteResponse> {
- throw 'not implemented';
+ //checks
+ if (!this.guild.me.permissions.has('MANAGE_ROLES')) return 'missing permissions';
+ const muteRoleID = await this.guild.getSetting('muteRole');
+ if (!muteRoleID) return 'no mute role';
+ const muteRole = this.guild.roles.cache.get(muteRoleID);
+ if (!muteRole) return 'invalid mute role';
+ if (muteRole.position >= this.guild.me.roles.highest.position || muteRole.managed) return 'mute role not manageable';
+
+ //add role
+ const success = await this.roles.add(muteRole).catch(() => null);
+ if (!success) return 'error giving mute role';
+
+ //add modlog entry
+ const modlog = await this.client.util
+ .createModLogEntry({
+ type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE,
+ user: this,
+ moderator: options.moderator,
+ reason: options.reason,
+ duration: options.duration,
+ guild: this.guild
+ })
+ .catch(() => null);
+ if (!modlog) return 'error creating modlog entry';
+
+ // add punishment entry so they can be unmuted later
+ const mute = await this.client.util
+ .createPunishmentEntry({
+ type: 'mute',
+ user: this,
+ guild: this.guild,
+ duration: options.duration,
+ modlog: modlog.id
+ })
+ .catch(() => null);
+ if (!mute) return 'error creating mute entry';
+
+ //dm user
+ const ending = this.guild.getSetting('punishmentEnding');
+ const dmSuccess = await this.send({
+ content: `You have been muted ${
+ options.duration ? 'for ' + this.client.util.humanizeDuration(options.duration) : 'permanently'
+ } in **${this.guild}** for **${options.reason || 'No reason provided'}**.${ending ? `\n\n${ending}` : ''}`
+ }).catch(() => null);
+
+ if (!dmSuccess) return 'failed to dm';
+
+ return 'success';
}
public async unmute(options: BushPunishmentOptions): Promise<UnmuteResponse> {
diff --git a/src/lib/models/Ban.ts b/src/lib/models/Ban.ts
index f4463b8..54ca6ae 100644
--- a/src/lib/models/Ban.ts
+++ b/src/lib/models/Ban.ts
@@ -7,7 +7,6 @@ export interface BanModel {
id: string;
user: string;
guild: string;
- reason: string;
expires: Date;
modlog: string;
}
@@ -15,7 +14,6 @@ export interface BanModelCreationAttributes {
id?: string;
user: string;
guild: string;
- reason?: string;
expires?: Date;
modlog: string;
}
@@ -34,10 +32,6 @@ export class Ban extends BaseModel<BanModel, BanModelCreationAttributes> impleme
*/
guild: Snowflake;
/**
- * The reason they are banned (optional)
- */
- reason: string | null;
- /**
* The date at which this ban expires and should be unbanned (optional)
*/
expires: Date | null;
@@ -71,10 +65,6 @@ export class Ban extends BaseModel<BanModel, BanModelCreationAttributes> impleme
type: DataTypes.DATE,
allowNull: true
},
- reason: {
- type: DataTypes.STRING,
- allowNull: true
- },
modlog: {
type: DataTypes.STRING,
allowNull: false,
diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts
index 303335b..0fc3413 100644
--- a/src/lib/models/Guild.ts
+++ b/src/lib/models/Guild.ts
@@ -10,11 +10,12 @@ export interface GuildModel {
blacklistedChannels: Snowflake[];
welcomeChannel: Snowflake;
muteRole: Snowflake;
+ punishmentEnding: string;
}
export type GuildModelCreationAttributes = Optional<
GuildModel,
- 'prefix' | 'autoPublishChannels' | 'blacklistedChannels' | 'welcomeChannel' | 'muteRole'
+ 'prefix' | 'autoPublishChannels' | 'blacklistedChannels' | 'welcomeChannel' | 'muteRole' | 'punishmentEnding'
>;
export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> implements GuildModel {
@@ -24,6 +25,7 @@ export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> i
blacklistedChannels: Snowflake[];
welcomeChannel: Snowflake;
muteRole: Snowflake;
+ punishmentEnding: string;
static initModel(sequelize: Sequelize, client: BushClient): void {
Guild.init(
@@ -64,6 +66,10 @@ export class Guild extends BaseModel<GuildModel, GuildModelCreationAttributes> i
muteRole: {
type: DataTypes.STRING,
allowNull: true
+ },
+ punishmentEnding: {
+ type: DataTypes.TEXT,
+ allowNull: true
}
},
{ sequelize: sequelize }
diff --git a/src/lib/models/ModLog.ts b/src/lib/models/ModLog.ts
index 1d850d9..6261794 100644
--- a/src/lib/models/ModLog.ts
+++ b/src/lib/models/ModLog.ts
@@ -4,14 +4,17 @@ import { v4 as uuidv4 } from 'uuid';
import { BaseModel } from './BaseModel';
export enum ModLogType {
- BAN = 'BAN',
+ PERM_BAN = 'PERM_BAN',
TEMP_BAN = 'TEMP_BAN',
+ UNBAN = 'UNBAN',
KICK = 'KICK',
- MUTE = 'MUTE',
+ PERM_MUTE = 'PERM_MUTE',
TEMP_MUTE = 'TEMP_MUTE',
+ UNMUTE = 'UNMUTE',
WARN = 'WARN',
- PUNISHMENT_ROLE = 'PUNISHMENT_ROLE',
- TEMP_PUNISHMENT_ROLE = 'TEMP_PUNISHMENT_ROLE'
+ PERM_PUNISHMENT_ROLE = 'PERM_PUNISHMENT_ROLE',
+ TEMP_PUNISHMENT_ROLE = 'TEMP_PUNISHMENT_ROLE',
+ REMOVE_PUNISHMENT_ROLE = 'REMOVE_PUNISHMENT_ROLE'
}
export interface ModLogModel {
diff --git a/src/lib/models/Mute.ts b/src/lib/models/Mute.ts
index 273d5b1..71a32e3 100644
--- a/src/lib/models/Mute.ts
+++ b/src/lib/models/Mute.ts
@@ -7,7 +7,6 @@ export interface MuteModel {
id: string;
user: string;
guild: string;
- reason: string;
expires: Date;
modlog: string;
}
@@ -15,7 +14,6 @@ export interface MuteModelCreationAttributes {
id?: string;
user: string;
guild: string;
- reason?: string;
expires?: Date;
modlog: string;
}
@@ -34,10 +32,6 @@ export class Mute extends BaseModel<MuteModel, MuteModelCreationAttributes> impl
*/
guild: Snowflake;
/**
- * The reason they are muted (optional)
- */
- reason: string | null;
- /**
* The date at which this Mute expires and should be unmuted (optional)
*/
expires: Date | null;
@@ -71,10 +65,6 @@ export class Mute extends BaseModel<MuteModel, MuteModelCreationAttributes> impl
type: DataTypes.DATE,
allowNull: true
},
- reason: {
- type: DataTypes.STRING,
- allowNull: true
- },
modlog: {
type: DataTypes.STRING,
allowNull: false,
diff --git a/src/lib/models/PunishmentRole.ts b/src/lib/models/PunishmentRole.ts
index 3326dca..927cf28 100644
--- a/src/lib/models/PunishmentRole.ts
+++ b/src/lib/models/PunishmentRole.ts
@@ -7,7 +7,6 @@ export interface PunishmentRoleModel {
id: string;
user: string;
guild: string;
- reason: string;
expires: Date;
modlog: string;
}
@@ -15,7 +14,6 @@ export interface PunishmentRoleModelCreationAttributes {
id?: string;
user: string;
guild: string;
- reason?: string;
expires?: Date;
modlog: string;
}
@@ -37,10 +35,6 @@ export class PunishmentRole
*/
guild: Snowflake;
/**
- * The reason they received a role (optional)
- */
- reason: string | null;
- /**
* The date at which this role expires and should be removed (optional)
*/
expires: Date | null;
@@ -74,10 +68,6 @@ export class PunishmentRole
type: DataTypes.DATE,
allowNull: true
},
- reason: {
- type: DataTypes.STRING,
- allowNull: true
- },
modlog: {
type: DataTypes.STRING,
allowNull: false,
diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts
index 0e3f6bb..7e6013d 100644
--- a/src/lib/utils/BushConstants.ts
+++ b/src/lib/utils/BushConstants.ts
@@ -1,36 +1,36 @@
export class BushConstants {
- // Stolen from @Mzato0001 (pr to discord akairo that hasn't been merged yet)
- public static TimeUnits = {
+ // Somewhat stolen from @Mzato0001
+ public static TimeUnits: { [key: string]: { match: RegExp; value: number } } = {
years: {
- label: '(?:years?|y)',
- value: 1000 * 60 * 60 * 24 * 365
+ match: / (?:(?<years>-?(?:\d+)?\.?\d+) *(?:years?|y))/im,
+ value: 1000 * 60 * 60 * 24 * 365.25 //leap years
},
months: {
- label: '(?:months?|mon|mo?)',
- value: 1000 * 60 * 60 * 24 * 30
+ match: / (?:(?<months>-?(?:\d+)?\.?\d+) *(?:months?|mon|mo?))/im,
+ value: 1000 * 60 * 60 * 24 * 30.4375 // average of days in months including leap years
},
weeks: {
- label: '(?:weeks?|w)',
+ match: / (?:(?<weeks>-?(?:\d+)?\.?\d+) *(?:weeks?|w))/im,
value: 1000 * 60 * 60 * 24 * 7
},
days: {
- label: '(?:days?|d)',
+ match: / (?:(?<days>-?(?:\d+)?\.?\d+) *(?:days?|d))/im,
value: 1000 * 60 * 60 * 24
},
hours: {
- label: '(?:hours?|hrs?|h)',
+ match: / (?:(?<hours>-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))/im,
value: 1000 * 60 * 60
},
minutes: {
- label: '(?:minutes?|mins?)',
+ match: / (?:(?<minutes>-?(?:\d+)?\.?\d+) *(?:minutes?|mins?))/im,
value: 1000 * 60
},
seconds: {
- label: '(?:seconds?|secs?|s)',
+ match: / (?:(?<seconds>-?(?:\d+)?\.?\d+) *(?:seconds?|secs?|s))/im,
value: 1000
},
milliseconds: {
- label: '(?:milliseconds?|msecs?|ms)',
+ match: / (?:(?<milliseconds>-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))/im,
value: 1
}
};
diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts
index 3cb5046..20132dd 100644
--- a/src/listeners/commands/commandError.ts
+++ b/src/listeners/commands/commandError.ts
@@ -22,7 +22,7 @@ export default class CommandErrorListener extends BushListener {
**Channel:** ${message.channel} (${message.channel?.id})
**Message:** [link](${message.url})`
)
- .addField('Error', await this.client.util.codeblock(`${error?.stack}`, 1024, 'js'))
+ .addField('Error', await this.client.util.codeblock(`${typeof error === 'object' ? error?.stack : error}`, 1024, 'js'))
.setColor(this.client.util.colors.error)
.setTimestamp();
@@ -41,7 +41,10 @@ export default class CommandErrorListener extends BushListener {
);
await message.util.send({ embeds: [errorUserEmbed] }).catch((e) => {
const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name;
- this.client.console.warn('CommandError', `Failed to send user error embed in <<${channel}>>:\n` + e?.stack);
+ this.client.console.warn(
+ 'CommandError',
+ `Failed to send user error embed in <<${channel}>>:\n` + typeof e === 'object' ? e?.stack : e
+ );
});
} else {
const errorDevEmbed = new MessageEmbed()
@@ -51,7 +54,10 @@ export default class CommandErrorListener extends BushListener {
.setDescription(await this.client.util.codeblock(`${error?.stack}`, 2048, 'js'));
await message.util.send({ embeds: [errorDevEmbed] }).catch((e) => {
const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name;
- this.client.console.warn('CommandError', `Failed to send owner error stack in <<${channel}>>.` + e?.stack);
+ this.client.console.warn(
+ 'CommandError',
+ `Failed to send owner error stack in <<${channel}>>.` + typeof e === 'object' ? e?.stack : e
+ );
});
}
}
@@ -59,7 +65,10 @@ export default class CommandErrorListener extends BushListener {
this.client.console.error(
'CommandError',
`an error occurred with the <<${command}>> command in <<${channel}>> triggered by <<${message?.author?.tag}>>:\n` +
- error?.stack,
+ typeof error ===
+ 'object'
+ ? error?.stack
+ : error,
false
);
}
diff --git a/src/listeners/commands/commandMissingPermissions.ts b/src/listeners/commands/commandMissingPermissions.ts
index e3420ae..bef8e9c 100644
--- a/src/listeners/commands/commandMissingPermissions.ts
+++ b/src/listeners/commands/commandMissingPermissions.ts
@@ -1,3 +1,4 @@
+import { PermissionString } from 'discord.js';
import { BushCommand } from '../../lib/extensions/discord-akairo/BushCommand';
import { BushListener } from '../../lib/extensions/discord-akairo/BushListener';
import { BushMessage } from '../../lib/extensions/discord.js/BushMessage';
@@ -15,8 +16,17 @@ export default class CommandMissingPermissionsListener extends BushListener {
message: BushMessage,
command: BushCommand | null | undefined,
type: 'client' | 'user',
- missing: Array<string>
+ missing: Array<PermissionString>
): Promise<void> {
+ this.client.console.debug(message.guild.me.permissions.toArray());
+ missing.forEach((permission) => {
+ this.client.console.debug(message.guild.me.permissions.has(permission));
+ });
+ message.guild.me.permissions;
+ this.client.console.debug(type);
+ this.client.console.debug(command.clientPermissions);
+ this.client.console.debug(command.userPermissions);
+ this.client.console.debug(missing);
const niceMissing = [];
missing.forEach((missing) => {
if (this.client.consts.mappings.permissions[missing]) {
diff --git a/src/listeners/commands/slashCommandError.ts b/src/listeners/commands/slashCommandError.ts
index 767bd3d..b172670 100644
--- a/src/listeners/commands/slashCommandError.ts
+++ b/src/listeners/commands/slashCommandError.ts
@@ -21,7 +21,7 @@ export default class SlashCommandErrorListener extends BushListener {
**Channel:** ${message.channel || message.interaction.user?.tag} ${message.channel ? `(${message.channel?.id})` : ''}
**Message:** [link](https://discord.com/${message.guild?.id}/${message.channel?.id}/${message.id})`
)
- .addField('Error', await this.client.util.codeblock(`${error?.stack}`, 1024, 'js'))
+ .addField('Error', await this.client.util.codeblock(`${typeof error === 'object' ? error?.stack : error}`, 1024, 'js'))
.setColor(this.client.util.colors.error)
.setTimestamp();
@@ -40,7 +40,10 @@ export default class SlashCommandErrorListener extends BushListener {
`Oh no! While running the command \`${command.id}\`, an error occurred. Please give the developers code \`${errorNo}\`.`
);
await message.util.send({ embeds: [errorUserEmbed] }).catch((e) => {
- this.client.console.warn('SlashError', `Failed to send user error embed in <<${channel}>>:\n` + e?.stack);
+ this.client.console.warn(
+ 'SlashError',
+ `Failed to send user error embed in <<${channel}>>:\n` + typeof e === 'object' ? e?.stack : e
+ );
});
} else {
const errorDevEmbed = new MessageEmbed()
@@ -49,7 +52,10 @@ export default class SlashCommandErrorListener extends BushListener {
.setTimestamp()
.setDescription(await this.client.util.codeblock(`${error?.stack}`, 2048, 'js'));
await message.util.send({ embeds: [errorDevEmbed] }).catch((e) => {
- this.client.console.warn('SlashError', `Failed to send owner error stack in <<${channel}>>.` + e?.stack);
+ this.client.console.warn(
+ 'SlashError',
+ `Failed to send owner error stack in <<${channel}>>.` + typeof e === 'object' ? e?.stack : e
+ );
});
}
}
@@ -57,7 +63,10 @@ export default class SlashCommandErrorListener extends BushListener {
this.client.console.error(
'SlashError',
`an error occurred with the <<${command}>> command in <<${channel}>> triggered by <<${message?.author?.tag}>>:\n` +
- error?.stack,
+ typeof error ===
+ 'object'
+ ? error?.stack
+ : error,
false
);
}