aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/commands/moderation/ban.ts21
-rw-r--r--src/commands/moderation/block.ts22
-rw-r--r--src/commands/moderation/kick.ts2
-rw-r--r--src/commands/moderation/lockdown.ts18
-rw-r--r--src/commands/moderation/mute.ts22
-rw-r--r--src/commands/moderation/role.ts12
-rw-r--r--src/commands/moderation/timeout.ts100
-rw-r--r--src/commands/moderation/unban.ts2
-rw-r--r--src/commands/moderation/unblock.ts6
-rw-r--r--src/commands/moderation/unlockdown.ts20
-rw-r--r--src/commands/moderation/unmute.ts6
-rw-r--r--src/commands/moderation/untimeout.ts100
-rw-r--r--src/commands/moderation/warn.ts4
-rw-r--r--src/lib/common/AutoMod.ts6
-rw-r--r--src/lib/common/util/Moderation.ts4
-rw-r--r--src/lib/extensions/discord-akairo/BushClient.ts10
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts6
-rw-r--r--src/lib/extensions/discord.js/BushClientEvents.d.ts19
-rw-r--r--src/lib/extensions/discord.js/BushGuild.ts62
-rw-r--r--src/lib/extensions/discord.js/BushGuildMember.ts176
-rw-r--r--src/lib/models/ModLog.ts4
-rw-r--r--src/lib/utils/BushConstants.ts2
-rw-r--r--src/listeners/member-custom/bushRemoveTimeout.ts31
-rw-r--r--src/listeners/member-custom/bushTimeout.ts34
-rw-r--r--src/tasks/removeExpiredPunishements.ts6
25 files changed, 564 insertions, 131 deletions
diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts
index 40ac506..f21ccdc 100644
--- a/src/commands/moderation/ban.ts
+++ b/src/commands/moderation/ban.ts
@@ -15,7 +15,7 @@ export default class BanCommand extends BushCommand {
aliases: ['ban', 'force-ban', 'dban'],
category: 'moderation',
description: 'Ban a member from the server.',
- usage: ['ban <member> <reason> [--delete]'],
+ usage: ['ban <member> [reasonAndDuration] [--delete]'],
examples: ['ban ironm00n 1 day commands in #general --delete 7'],
args: [
{
@@ -27,10 +27,10 @@ export default class BanCommand extends BushCommand {
slashType: 'USER'
},
{
- id: 'reason',
+ id: 'reason_and_duration',
description: 'The reason and duration of the ban.',
type: 'contentWithDuration',
- match: 'restContent',
+ match: 'rest',
prompt: 'Why should this user be banned and for how long?',
retry: '{error} Choose a valid ban reason and duration.',
slashType: 'STRING',
@@ -70,12 +70,12 @@ export default class BanCommand extends BushCommand {
message: BushMessage | BushSlashMessage,
args: {
user: ArgType<'user'> | ArgType<'snowflake'>;
- reason: OptionalArgType<'contentWithDuration'>;
+ reason_and_duration: OptionalArgType<'contentWithDuration'>;
days: OptionalArgType<'integer'>;
force: boolean;
}
) {
- if (args.reason && typeof args.reason === 'object') args.reason.duration ??= 0;
+ if (args.reason_and_duration && typeof args.reason_and_duration === 'object') args.reason_and_duration.duration ??= 0;
args.days ??= 0;
if (!message.guild) return message.util.reply(`${util.emojis.error} This command cannot be used in dms.`);
@@ -100,10 +100,13 @@ export default class BanCommand extends BushCommand {
}
let time: number | null;
- if (args.reason) {
- time = typeof args.reason === 'string' ? await util.arg.cast('duration', message, args.reason) : args.reason.duration;
+ if (args.reason_and_duration) {
+ time =
+ typeof args.reason_and_duration === 'string'
+ ? await util.arg.cast('duration', message, args.reason_and_duration)
+ : args.reason_and_duration.duration;
}
- const parsedReason = args.reason?.contentWithoutTime ?? null;
+ const parsedReason = args.reason_and_duration?.contentWithoutTime ?? null;
const responseCode = member
? await member.bushBan({
@@ -120,7 +123,7 @@ export default class BanCommand extends BushCommand {
deleteDays: args.days
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const victim = util.format.input(user.tag);
switch (responseCode) {
case 'missing permissions':
diff --git a/src/commands/moderation/block.ts b/src/commands/moderation/block.ts
index 371975b..d53ffd5 100644
--- a/src/commands/moderation/block.ts
+++ b/src/commands/moderation/block.ts
@@ -17,7 +17,7 @@ export default class BlockCommand extends BushCommand {
aliases: ['block'],
category: 'moderation',
description: 'Prevent a user from using a channel.',
- usage: ['block <member> [reason] [duration]'],
+ usage: ['block <member> [reasonAndDuration]'],
examples: ['block IRONM00N 2h bad jokes'],
args: [
{
@@ -29,7 +29,7 @@ export default class BlockCommand extends BushCommand {
slashType: 'USER'
},
{
- id: 'reason',
+ id: 'reason_and_duration',
description: 'The reason and duration of the block.',
type: 'contentWithDuration',
match: 'rest',
@@ -58,16 +58,20 @@ export default class BlockCommand extends BushCommand {
public override async exec(
message: BushMessage | BushSlashMessage,
- args: { user: ArgType<'user'>; reason: OptionalArgType<'contentWithDuration'> | string; force?: ArgType<'boolean'> }
+ args: {
+ user: ArgType<'user'>;
+ reason_and_duration: OptionalArgType<'contentWithDuration'> | string;
+ force?: ArgType<'boolean'>;
+ }
) {
assert(message.inGuild());
if (!(message.channel instanceof BushTextChannel || message.channel instanceof BushThreadChannel))
return message.util.send(`${util.emojis.error} This command can only be used in text and thread channels.`);
- const reason = args.reason
- ? typeof args.reason === 'string'
- ? await util.arg.cast('contentWithDuration', message, args.reason)
- : args.reason
+ const reason = args.reason_and_duration
+ ? typeof args.reason_and_duration === 'string'
+ ? await util.arg.cast('contentWithDuration', message, args.reason_and_duration)
+ : args.reason_and_duration
: { duration: null, contentWithoutTime: '' };
if (reason.duration === null) reason.duration = 0;
@@ -91,14 +95,14 @@ export default class BlockCommand extends BushCommand {
const parsedReason = reason?.contentWithoutTime ?? '';
- const responseCode = await member.block({
+ const responseCode = await member.bushBlock({
reason: parsedReason,
moderator: message.member,
duration: time ?? 0,
channel: message.channel
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const victim = util.format.input(member.user.tag);
switch (responseCode) {
case 'missing permissions':
diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts
index 587f89e..6d96ae9 100644
--- a/src/commands/moderation/kick.ts
+++ b/src/commands/moderation/kick.ts
@@ -65,7 +65,7 @@ export default class KickCommand extends BushCommand {
moderator: message.member
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const victim = util.format.input(member.user.tag);
switch (responseCode) {
case 'missing permissions':
diff --git a/src/commands/moderation/lockdown.ts b/src/commands/moderation/lockdown.ts
index b9febbd..ae9a62b 100644
--- a/src/commands/moderation/lockdown.ts
+++ b/src/commands/moderation/lockdown.ts
@@ -32,15 +32,6 @@ export default class LockdownCommand extends BushCommand {
optional: true
},
{
- id: 'all',
- description: 'Whether or not to lock all configured channels.',
- match: 'flag',
- flag: '--all',
- prompt: 'Would you like to lockdown all configured channels?',
- slashType: 'BOOLEAN',
- optional: true
- },
- {
id: 'reason',
description: 'The reason for the lockdown.',
type: 'string',
@@ -48,6 +39,15 @@ export default class LockdownCommand extends BushCommand {
prompt: 'What is the reason for the lockdown?',
slashType: 'STRING',
optional: true
+ },
+ {
+ id: 'all',
+ description: 'Whether or not to lock all configured channels.',
+ match: 'flag',
+ flag: '--all',
+ prompt: 'Would you like to lockdown all configured channels?',
+ slashType: 'BOOLEAN',
+ optional: true
}
],
slash: true,
diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts
index eeffc6c..e731e30 100644
--- a/src/commands/moderation/mute.ts
+++ b/src/commands/moderation/mute.ts
@@ -15,7 +15,7 @@ export default class MuteCommand extends BushCommand {
aliases: ['mute'],
category: 'moderation',
description: 'Mute a user.',
- usage: ['mute <member> [reason] [duration]'],
+ usage: ['mute <member> [reasonAndDuration]'],
examples: ['mute ironm00n 1 day commands in #general'],
args: [
{
@@ -27,7 +27,7 @@ export default class MuteCommand extends BushCommand {
slashType: 'USER'
},
{
- id: 'reason',
+ id: 'reason_and_duration',
description: 'The reason and duration of the mute.',
type: 'contentWithDuration',
match: 'rest',
@@ -56,12 +56,16 @@ export default class MuteCommand extends BushCommand {
public override async exec(
message: BushMessage | BushSlashMessage,
- args: { user: ArgType<'user'>; reason: OptionalArgType<'contentWithDuration'> | string; force?: ArgType<'boolean'> }
+ args: {
+ user: ArgType<'user'>;
+ reason_and_duration: OptionalArgType<'contentWithDuration'> | string;
+ force?: ArgType<'boolean'>;
+ }
) {
- const reason = args.reason
- ? typeof args.reason === 'string'
- ? await util.arg.cast('contentWithDuration', message, args.reason)
- : args.reason
+ const reason = args.reason_and_duration
+ ? typeof args.reason_and_duration === 'string'
+ ? await util.arg.cast('contentWithDuration', message, args.reason_and_duration)
+ : args.reason_and_duration
: { duration: null, contentWithoutTime: '' };
if (reason.duration === null) reason.duration = 0;
@@ -85,13 +89,13 @@ export default class MuteCommand extends BushCommand {
const parsedReason = reason?.contentWithoutTime ?? '';
- const responseCode = await member.mute({
+ const responseCode = await member.bushMute({
reason: parsedReason,
moderator: message.member,
duration: time ?? 0
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const prefix = util.prefix(message);
const victim = util.format.input(member.user.tag);
switch (responseCode) {
diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts
index 9fd34b5..6cb8d2f 100644
--- a/src/commands/moderation/role.ts
+++ b/src/commands/moderation/role.ts
@@ -154,20 +154,20 @@ export default class RoleCommand extends BushCommand {
const responseCode =
args.action === 'add'
- ? await args.member.addRole({
+ ? await args.member.bushAddRole({
moderator: message.member!,
addToModlog: shouldLog,
role: args.role,
duration: args.duration
})
- : await args.member.removeRole({
+ : await args.member.bushRemoveRole({
moderator: message.member!,
addToModlog: shouldLog,
role: args.role,
duration: args.duration
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const victim = util.format.input(args.member.user.tag);
switch (responseCode) {
case 'user hierarchy':
@@ -178,11 +178,13 @@ export default class RoleCommand extends BushCommand {
return `${util.emojis.error} <@&${args.role.id}> is higher or equal to my highest role.`;
case 'error creating modlog entry':
return `${util.emojis.error} There was an error creating a modlog entry, please report this to my developers.`;
- case 'error creating role entry' || 'error removing role entry':
+ case 'error creating role entry':
+ case 'error removing role entry':
return `${util.emojis.error} There was an error ${
args.action === 'add' ? 'creating' : 'removing'
} a punishment entry, please report this to my developers.`;
- case 'error adding role' || 'error removing role':
+ case 'error adding role':
+ case 'error removing role':
return `${util.emojis.error} An error occurred while trying to ${args.action} <@&${args.role.id}> ${
args.action === 'add' ? 'to' : 'from'
} ${victim}.`;
diff --git a/src/commands/moderation/timeout.ts b/src/commands/moderation/timeout.ts
new file mode 100644
index 0000000..f187a58
--- /dev/null
+++ b/src/commands/moderation/timeout.ts
@@ -0,0 +1,100 @@
+import { AllowedMentions, BushCommand, Moderation, type ArgType, type BushMessage, type BushSlashMessage } from '#lib';
+import assert from 'assert';
+
+export default class TimeoutCommand extends BushCommand {
+ public constructor() {
+ super('timeout', {
+ aliases: ['timeout'],
+ category: 'moderation',
+ description: 'Timeout a user.',
+ usage: ['timeout <user> <reasonAndDuration>'],
+ examples: ['timeout IRONM00N 2h'],
+ args: [
+ {
+ id: 'user',
+ description: 'The user to timeout.',
+ type: 'user',
+ prompt: 'What user would you like to timeout?',
+ retry: '{error} Choose a valid user to timeout.',
+ slashType: 'USER'
+ },
+ {
+ id: 'reason_and_duration',
+ description: 'The reason and duration of the timeout.',
+ type: 'contentWithDuration',
+ match: 'rest',
+ prompt: 'Why should this user be timed out and for how long?',
+ retry: '{error} Choose a valid timeout reason and duration.',
+ slashType: 'STRING'
+ },
+ {
+ id: 'force',
+ description: 'Override permission checks.',
+ flag: '--force',
+ match: 'flag',
+ optional: true,
+ slashType: false,
+ only: 'text',
+ ownerOnly: true
+ }
+ ],
+ slash: true,
+ channel: 'guild',
+ clientPermissions: (m) => util.clientSendAndPermCheck(m, ['MODERATE_MEMBERS']),
+ userPermissions: ['MODERATE_MEMBERS']
+ });
+ }
+
+ public override async exec(
+ message: BushMessage | BushSlashMessage,
+ args: { user: ArgType<'user'>; reason_and_duration: ArgType<'contentWithDuration'> | string; force?: ArgType<'boolean'> }
+ ) {
+ const reason = args.reason_and_duration
+ ? typeof args.reason_and_duration === 'string'
+ ? await util.arg.cast('contentWithDuration', message, args.reason_and_duration)
+ : args.reason_and_duration
+ : { duration: null, contentWithoutTime: '' };
+
+ if (reason.duration === null || reason.duration < 1)
+ return await message.util.reply(`${util.emojis.error} You must specify a duration for timeouts.`);
+ const member = await message.guild!.members.fetch(args.user.id).catch(() => null);
+ if (!member)
+ return await message.util.reply(`${util.emojis.error} The user you selected is not in the server or is not a valid user.`);
+
+ assert(message.member);
+ const useForce = args.force && message.author.isOwner();
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'timeout', true, useForce);
+
+ if (canModerateResponse !== true) {
+ return message.util.reply(canModerateResponse);
+ }
+
+ const time = reason.duration;
+ const parsedReason = reason.contentWithoutTime ?? '';
+
+ const responseCode = await member.bushTimeout({
+ reason: parsedReason,
+ moderator: message.member,
+ duration: time
+ });
+
+ const responseMessage = (): string => {
+ const victim = util.format.input(member.user.tag);
+ switch (responseCode) {
+ case 'missing permissions':
+ return `${util.emojis.error} Could not timeout ${victim} because I am missing the **Timeout Members** permission.`;
+ case 'duration too long':
+ return `${util.emojis.error} The duration you specified is too long, the longest you can timeout someone for is 28 days.`;
+ case 'error timing out':
+ return `${util.emojis.error} An unknown error occurred while trying to timeout ${victim}.`;
+ case 'error creating modlog entry':
+ return `${util.emojis.error} There was an error creating a modlog entry, please report this to my developers.`;
+ case 'failed to dm':
+ return `${util.emojis.warn} Timed out ${victim} however I could not send them a dm.`;
+ case 'success':
+ return `${util.emojis.success} Successfully timed out ${victim}.`;
+ }
+ };
+ return await message.util.reply({ content: responseMessage(), allowedMentions: AllowedMentions.none() });
+ }
+}
diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts
index 03c60c0..bbce4e0 100644
--- a/src/commands/moderation/unban.ts
+++ b/src/commands/moderation/unban.ts
@@ -44,7 +44,7 @@ export default class UnbanCommand extends BushCommand {
reason
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const victim = util.format.input(user.tag);
switch (responseCode) {
case 'missing permissions':
diff --git a/src/commands/moderation/unblock.ts b/src/commands/moderation/unblock.ts
index f9f069b..4cc8b49 100644
--- a/src/commands/moderation/unblock.ts
+++ b/src/commands/moderation/unblock.ts
@@ -18,7 +18,7 @@ export default class UnblockCommand extends BushCommand {
category: 'moderation',
description: 'Allows a user to use a channel.',
usage: ['unblock <member> [reason]'],
- examples: ['unblock IRONM00N 2h bad jokes'],
+ examples: ['unblock IRONM00N nvm your jokes are funny'],
args: [
{
id: 'user',
@@ -78,13 +78,13 @@ export default class UnblockCommand extends BushCommand {
const parsedReason = args.reason ?? '';
- const responseCode = await member.unblock({
+ const responseCode = await member.bushUnblock({
reason: parsedReason,
moderator: message.member,
channel: message.channel
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const victim = util.format.input(member.user.tag);
switch (responseCode) {
case 'missing permissions':
diff --git a/src/commands/moderation/unlockdown.ts b/src/commands/moderation/unlockdown.ts
index 24af305..5bcea32 100644
--- a/src/commands/moderation/unlockdown.ts
+++ b/src/commands/moderation/unlockdown.ts
@@ -8,7 +8,7 @@ export default class UnlockdownCommand extends BushCommand {
category: 'moderation',
description: 'Allows you to unlockdown a channel or all configured channels.',
usage: ['unlockdown [channel] [reason] [--all]'],
- examples: ['unlockdown', 'unlockdown --all'],
+ examples: ['unlockdown', 'unlockdown raid is over --all'],
args: [
{
id: 'channel',
@@ -20,15 +20,6 @@ export default class UnlockdownCommand extends BushCommand {
optional: true
},
{
- id: 'all',
- description: 'Whether or not to unlock all configured channels.',
- match: 'flag',
- flag: '--all',
- prompt: 'Would you like to unlockdown all configured channels?',
- slashType: 'BOOLEAN',
- optional: true
- },
- {
id: 'reason',
description: 'The reason for the unlock.',
type: 'string',
@@ -36,6 +27,15 @@ export default class UnlockdownCommand extends BushCommand {
prompt: 'What is the reason for the unlock?',
slashType: 'STRING',
optional: true
+ },
+ {
+ id: 'all',
+ description: 'Whether or not to unlock all configured channels.',
+ match: 'flag',
+ flag: '--all',
+ prompt: 'Would you like to unlockdown all configured channels?',
+ slashType: 'BOOLEAN',
+ optional: true
}
],
slash: true,
diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts
index e7a19d6..631f213 100644
--- a/src/commands/moderation/unmute.ts
+++ b/src/commands/moderation/unmute.ts
@@ -16,7 +16,7 @@ export default class UnmuteCommand extends BushCommand {
category: 'moderation',
description: 'unmute a user.',
usage: ['unmute <member> [reason]'],
- examples: ['unmute 322862723090219008 1 day commands in #general'],
+ examples: ['unmute 322862723090219008 you have been forgiven'],
args: [
{
id: 'user',
@@ -70,12 +70,12 @@ export default class UnmuteCommand extends BushCommand {
return message.util.reply(canModerateResponse);
}
- const responseCode = await member.unmute({
+ const responseCode = await member.bushUnmute({
reason,
moderator: message.member
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const prefix = util.prefix(message);
const victim = util.format.input(member.user.tag);
switch (responseCode) {
diff --git a/src/commands/moderation/untimeout.ts b/src/commands/moderation/untimeout.ts
new file mode 100644
index 0000000..b5ea5be
--- /dev/null
+++ b/src/commands/moderation/untimeout.ts
@@ -0,0 +1,100 @@
+import {
+ AllowedMentions,
+ BushCommand,
+ Moderation,
+ type ArgType,
+ type BushMessage,
+ type BushSlashMessage,
+ type OptionalArgType
+} from '#lib';
+import assert from 'assert';
+
+export default class UntimeoutCommand extends BushCommand {
+ public constructor() {
+ super('untimeout', {
+ aliases: ['untimeout', 'remove-timeout'],
+ category: 'moderation',
+ description: 'Removes a timeout from a user.',
+ usage: ['untimeout <user> [reason]'],
+ examples: ['untimeout 1 2'],
+ args: [
+ {
+ id: 'user',
+ description: 'The user to remove a timeout from.',
+ type: 'user',
+ prompt: 'What user would you like to untimeout?',
+ retry: '{error} Choose a valid user to untimeout.',
+ slashType: 'USER'
+ },
+ {
+ id: 'reason',
+ description: 'The reason for removing the timeout.',
+ type: 'string',
+ match: 'rest',
+ prompt: 'Why should this user have their timeout removed?',
+ retry: '{error} Choose a valid reason to remove the timeout.',
+ slashType: 'STRING',
+ optional: true
+ },
+ {
+ id: 'force',
+ description: 'Override permission checks.',
+ flag: '--force',
+ match: 'flag',
+ optional: true,
+ slashType: false,
+ only: 'text',
+ ownerOnly: true
+ }
+ ],
+ slash: true,
+ channel: 'guild',
+ clientPermissions: (m) => util.clientSendAndPermCheck(m, ['MODERATE_MEMBERS']),
+ userPermissions: ['MODERATE_MEMBERS']
+ });
+ }
+
+ public override async exec(
+ message: BushMessage | BushSlashMessage,
+ args: { user: ArgType<'user'>; reason: OptionalArgType<'string'>; force?: ArgType<'boolean'> }
+ ) {
+ assert(message.member);
+
+ const member = await message.guild!.members.fetch(args.user.id).catch(() => null);
+ if (!member)
+ return await message.util.reply(`${util.emojis.error} The user you selected is not in the server or is not a valid user.`);
+
+ if (!member.isCommunicationDisabled()) return message.util.reply(`${util.emojis.error} That user is not timed out.`);
+
+ const useForce = args.force && message.author.isOwner();
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'timeout', true, useForce);
+
+ if (canModerateResponse !== true) {
+ return message.util.reply(canModerateResponse);
+ }
+
+ const responseCode = await member.bushRemoveTimeout({
+ reason: args.reason ?? undefined,
+ moderator: message.member
+ });
+
+ const responseMessage = (): string => {
+ const victim = util.format.input(member.user.tag);
+ switch (responseCode) {
+ case 'missing permissions':
+ return `${util.emojis.error} Could not timeout ${victim} because I am missing the **Timeout Members** permission.`;
+ case 'duration too long':
+ return `${util.emojis.error} The duration you specified is too long, the longest you can timeout someone for is 28 days.`;
+ case 'error removing timeout':
+ return `${util.emojis.error} An unknown error occurred while trying to timeout ${victim}.`;
+ case 'error creating modlog entry':
+ return `${util.emojis.error} There was an error creating a modlog entry, please report this to my developers.`;
+ case 'failed to dm':
+ return `${util.emojis.warn} Timed out ${victim} however I could not send them a dm.`;
+ case 'success':
+ return `${util.emojis.success} Successfully timed out ${victim}.`;
+ }
+ };
+ return await message.util.reply({ content: responseMessage(), allowedMentions: AllowedMentions.none() });
+ }
+}
diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts
index 5093c9b..05b1e36 100644
--- a/src/commands/moderation/warn.ts
+++ b/src/commands/moderation/warn.ts
@@ -67,12 +67,12 @@ export default class WarnCommand extends BushCommand {
return message.util.reply(canModerateResponse);
}
- const { result: response, caseNum } = await member.warn({
+ const { result: response, caseNum } = await member.bushWarn({
reason,
moderator: message.member
});
- const responseMessage = () => {
+ const responseMessage = (): string => {
const victim = util.format.input(member.user.tag);
switch (response) {
case 'error creating modlog entry':
diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts
index cc1bbbd..d7da532 100644
--- a/src/lib/common/AutoMod.ts
+++ b/src/lib/common/AutoMod.ts
@@ -183,7 +183,7 @@ export class AutoMod {
case Severity.WARN: {
color = util.colors.yellow;
void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.message.member?.warn({
+ void this.message.member?.bushWarn({
moderator: this.message.guild!.me!,
reason: `[AutoMod] ${highestOffence.reason}`
});
@@ -193,7 +193,7 @@ export class AutoMod {
case Severity.TEMP_MUTE: {
color = util.colors.orange;
void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.message.member?.mute({
+ void this.message.member?.bushMute({
moderator: this.message.guild!.me!,
reason: `[AutoMod] ${highestOffence.reason}`,
duration: 900_000 // 15 minutes
@@ -204,7 +204,7 @@ export class AutoMod {
case Severity.PERM_MUTE: {
color = util.colors.red;
void this.message.delete().catch((e) => deleteError.bind(this, e));
- void this.message.member?.mute({
+ void this.message.member?.bushMute({
moderator: this.message.guild!.me!,
reason: `[AutoMod] ${highestOffence.reason}`,
duration: 0 // permanent
diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts
index c06ad25..a09930b 100644
--- a/src/lib/common/util/Moderation.ts
+++ b/src/lib/common/util/Moderation.ts
@@ -36,7 +36,9 @@ export class Moderation {
| 'add a punishment role to'
| 'remove a punishment role from'
| 'block'
- | 'unblock',
+ | 'unblock'
+ | 'timeout'
+ | 'untimeout',
checkModerator = true,
force = false
): Promise<true | string> {
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts
index 706b52a..8cbc6fe 100644
--- a/src/lib/extensions/discord-akairo/BushClient.ts
+++ b/src/lib/extensions/discord-akairo/BushClient.ts
@@ -449,19 +449,19 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
export interface BushClient extends EventEmitter, PatchedElements {
on<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this;
- on<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, listener: (...args: any[]) => Awaitable<void>): this;
+ // on<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, listener: (...args: any[]) => Awaitable<void>): this;
once<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this;
- once<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, listener: (...args: any[]) => Awaitable<void>): this;
+ // once<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, listener: (...args: any[]) => Awaitable<void>): this;
emit<K extends keyof BushClientEvents>(event: K, ...args: BushClientEvents[K]): boolean;
- emit<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, ...args: unknown[]): boolean;
+ // emit<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, ...args: unknown[]): boolean;
off<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this;
- off<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, listener: (...args: any[]) => Awaitable<void>): this;
+ // off<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, listener: (...args: any[]) => Awaitable<void>): this;
removeAllListeners<K extends keyof BushClientEvents>(event?: K): this;
- removeAllListeners<S extends string | symbol>(event?: Exclude<S, keyof BushClientEvents>): this;
+ // removeAllListeners<S extends string | symbol>(event?: Exclude<S, keyof BushClientEvents>): this;
}
export interface BushStats {
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
index f20a53e..30cc83f 100644
--- a/src/lib/extensions/discord-akairo/BushClientUtil.ts
+++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts
@@ -515,11 +515,11 @@ export class BushClientUtil extends ClientUtil {
// in the beginning of the argument
let contentWithoutTime = ` ${content}`;
- for (const unit in BushConstants.TimeUnits) {
- const regex = BushConstants.TimeUnits[unit as keyof typeof BushConstants.TimeUnits].match;
+ for (const unit in BushConstants.timeUnits) {
+ const regex = BushConstants.timeUnits[unit as keyof typeof BushConstants.timeUnits].match;
const match = regex.exec(contentWithoutTime);
const value = Number(match?.groups?.[unit]);
- if (!isNaN(value)) duration! += value * BushConstants.TimeUnits[unit as keyof typeof BushConstants.TimeUnits].value;
+ if (!isNaN(value)) duration! += value * BushConstants.timeUnits[unit as keyof typeof BushConstants.timeUnits].value;
if (remove) contentWithoutTime = contentWithoutTime.replace(regex, '');
}
diff --git a/src/lib/extensions/discord.js/BushClientEvents.d.ts b/src/lib/extensions/discord.js/BushClientEvents.d.ts
index b779991..6dc94a1 100644
--- a/src/lib/extensions/discord.js/BushClientEvents.d.ts
+++ b/src/lib/extensions/discord.js/BushClientEvents.d.ts
@@ -227,6 +227,25 @@ export interface BushClientEvents extends AkairoClientEvents {
channel: BushTextChannel | BushNewsChannel | BushThreadChannel,
messages: Collection<Snowflake, BushMessage>
];
+ bushRemoveTimeout: [
+ victim: BushGuildMember,
+ moderator: BushUser,
+ guild: BushGuild,
+ reason: string | undefined,
+ caseID: string,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
+ bushTimeout: [
+ victim: BushGuildMember,
+ moderator: BushUser,
+ guild: BushGuild,
+ reason: string | undefined,
+ caseID: string,
+ duration: number,
+ dmSuccess: boolean,
+ evidence?: string
+ ];
bushUnban: [
victim: BushUser,
moderator: BushUser,
diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts
index b67c71b..d182be4 100644
--- a/src/lib/extensions/discord.js/BushGuild.ts
+++ b/src/lib/extensions/discord.js/BushGuild.ts
@@ -130,6 +130,29 @@ export class BushGuild extends Guild {
}
/**
+ * Sends a message to the guild's specified logging channel
+ * @param logType The corresponding channel that the message will be sent to
+ * @param message The parameters for {@link BushTextChannel.send}
+ */
+ public async sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions) {
+ const logChannel = await this.getLogChannel(logType);
+ if (!logChannel || logChannel.type !== 'GUILD_TEXT') return;
+ if (!logChannel.permissionsFor(this.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS'])) return;
+
+ return await logChannel.send(message).catch(() => null);
+ }
+
+ /**
+ * Sends a formatted error message in a guild's error log channel
+ * @param title The title of the error embed
+ * @param message The description of the error embed
+ */
+ public async error(title: string, message: string) {
+ void client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>'));
+ void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: util.colors.error }] });
+ }
+
+ /**
* Bans a user, dms them, creates a mod log entry, and creates a punishment entry.
* @param options Options for banning the user.
* @returns A string status message of the ban.
@@ -143,7 +166,7 @@ export class BushGuild extends Guild {
const moderator = (await util.resolveNonCachedUser(options.moderator!)) ?? client.user!;
const ret = await (async () => {
- await this.members.cache.get(user.id)?.punishDM('banned', options.reason, options.duration ?? 0);
+ await this.members.cache.get(user.id)?.bushPunishDM('banned', options.reason, options.duration ?? 0);
// ban
const banSuccess = await this.bans
@@ -252,29 +275,6 @@ export class BushGuild extends Guild {
}
/**
- * Sends a message to the guild's specified logging channel
- * @param logType The corresponding channel that the message will be sent to
- * @param message The parameters for {@link BushTextChannel.send}
- */
- public async sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions) {
- const logChannel = await this.getLogChannel(logType);
- if (!logChannel || logChannel.type !== 'GUILD_TEXT') return;
- if (!logChannel.permissionsFor(this.me!.id)?.has(['VIEW_CHANNEL', 'SEND_MESSAGES', 'EMBED_LINKS'])) return;
-
- return await logChannel.send(message).catch(() => null);
- }
-
- /**
- * Sends a formatted error message in a guild's error log channel
- * @param title The title of the error embed
- * @param message The description of the error embed
- */
- public async error(title: string, message: string) {
- void client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>'));
- void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: util.colors.error }] });
- }
-
- /**
* Denies send permissions in specified channels
* @param options The options for locking down the guild
*/
@@ -336,7 +336,7 @@ export class BushGuild extends Guild {
/**
* Options for unbanning a user
*/
-interface BushUnbanOptions {
+export interface BushUnbanOptions {
/**
* The user to unban
*/
@@ -356,7 +356,7 @@ interface BushUnbanOptions {
/**
* Options for banning a user
*/
-interface BushBanOptions {
+export interface BushBanOptions {
/**
* The user to ban
*/
@@ -388,22 +388,22 @@ interface BushBanOptions {
evidence?: string;
}
-type PunishmentResponse = 'success' | 'missing permissions' | 'error creating modlog entry';
+export type PunishmentResponse = 'success' | 'missing permissions' | 'error creating modlog entry';
/**
* Response returned when banning a user
*/
-type BanResponse = PunishmentResponse | 'error banning' | 'error creating ban entry';
+export type BanResponse = PunishmentResponse | 'error banning' | 'error creating ban entry';
/**
* Response returned when unbanning a user
*/
-type UnbanResponse = PunishmentResponse | 'user not banned' | 'error unbanning' | 'error removing ban entry';
+export type UnbanResponse = PunishmentResponse | 'user not banned' | 'error unbanning' | 'error removing ban entry';
/**
* Options for locking down channel(s)
*/
-interface LockdownOptions {
+export interface LockdownOptions {
/**
* The moderator responsible for the lockdown
*/
@@ -433,7 +433,7 @@ interface LockdownOptions {
/**
* Response returned when locking down a channel
*/
-type LockdownResponse =
+export type LockdownResponse =
| `success: ${number}`
| 'all not chosen and no channel specified'
| 'no channels configured'
diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts
index ac8fb54..ffca507 100644
--- a/src/lib/extensions/discord.js/BushGuildMember.ts
+++ b/src/lib/extensions/discord.js/BushGuildMember.ts
@@ -35,7 +35,7 @@ export class BushGuildMember extends GuildMember {
* @param sendFooter Whether or not to send the guild's punishment footer with the dm.
* @returns Whether or not the dm was sent successfully.
*/
- public async punishDM(punishment: string, reason?: string | null, duration?: number, sendFooter = true): Promise<boolean> {
+ public async bushPunishDM(punishment: string, reason?: string | null, duration?: number, sendFooter = true): Promise<boolean> {
const ending = await this.guild.getSetting('punishmentEnding');
const dmEmbed =
ending && ending.length && sendFooter
@@ -56,12 +56,12 @@ export class BushGuildMember extends GuildMember {
* @returns An object with the result of the warning, and the case number of the warn.
* @emits {@link BushClientEvents.bushWarn}
*/
- public async warn(options: BushPunishmentOptions): Promise<{ result: WarnResponse | null; caseNum: number | null }> {
+ public async bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }> {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
- const ret = await (async () => {
+ const ret = await (async (): Promise<{ result: WarnResponse; caseNum: number | null }> => {
// add modlog entry
const result = await Moderation.createModLogEntry(
{
@@ -78,7 +78,7 @@ export class BushGuildMember extends GuildMember {
if (!result || !result.log) return { result: 'error creating modlog entry', caseNum: null };
// dm user
- const dmSuccess = await this.punishDM('warned', options.reason);
+ const dmSuccess = await this.bushPunishDM('warned', options.reason);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return { result: 'failed to dm', caseNum: result.caseNum };
@@ -86,7 +86,7 @@ export class BushGuildMember extends GuildMember {
})();
if (!(['error creating modlog entry'] as const).includes(ret.result))
client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!);
- return ret as { result: WarnResponse | null; caseNum: number | null };
+ return ret;
}
/**
@@ -95,7 +95,7 @@ export class BushGuildMember extends GuildMember {
* @returns A status message for adding the add.
* @emits {@link BushClientEvents.bushPunishRole}
*/
- public async addRole(options: AddRoleOptions): Promise<AddRoleResponse> {
+ public async bushAddRole(options: AddRoleOptions): Promise<AddRoleResponse> {
const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator);
if (ifShouldAddRole !== true) return ifShouldAddRole;
@@ -159,7 +159,7 @@ export class BushGuildMember extends GuildMember {
* @returns A status message for removing the role.
* @emits {@link BushClientEvents.bushPunishRoleRemove}
*/
- public async removeRole(options: RemoveRoleOptions): Promise<RemoveRoleResponse> {
+ public async bushRemoveRole(options: RemoveRoleOptions): Promise<RemoveRoleResponse> {
const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator);
if (ifShouldAddRole !== true) return ifShouldAddRole;
@@ -240,7 +240,7 @@ export class BushGuildMember extends GuildMember {
* @returns A status message for muting the user.
* @emits {@link BushClientEvents.bushMute}
*/
- public async mute(options: BushTimedPunishmentOptions): Promise<MuteResponse> {
+ public async bushMute(options: BushTimedPunishmentOptions): Promise<MuteResponse> {
// checks
if (!this.guild.me!.permissions.has('MANAGE_ROLES')) return 'missing permissions';
const muteRoleID = await this.guild.getSetting('muteRole');
@@ -290,7 +290,7 @@ export class BushGuildMember extends GuildMember {
if (!punishmentEntrySuccess) return 'error creating mute entry';
// dm user
- const dmSuccess = await this.punishDM('muted', options.reason, options.duration ?? 0);
+ const dmSuccess = await this.bushPunishDM('muted', options.reason, options.duration ?? 0);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return 'failed to dm';
@@ -319,7 +319,7 @@ export class BushGuildMember extends GuildMember {
* @returns A status message for unmuting the user.
* @emits {@link BushClientEvents.bushUnmute}
*/
- public async unmute(options: BushPunishmentOptions): Promise<UnmuteResponse> {
+ public async bushUnmute(options: BushPunishmentOptions): Promise<UnmuteResponse> {
// checks
if (!this.guild.me!.permissions.has('MANAGE_ROLES')) return 'missing permissions';
const muteRoleID = await this.guild.getSetting('muteRole');
@@ -365,7 +365,7 @@ export class BushGuildMember extends GuildMember {
if (!removePunishmentEntrySuccess) return 'error removing mute entry';
// dm user
- const dmSuccess = await this.punishDM('unmuted', options.reason, undefined, false);
+ const dmSuccess = await this.bushPunishDM('unmuted', options.reason, undefined, false);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return 'failed to dm';
@@ -402,7 +402,7 @@ export class BushGuildMember extends GuildMember {
const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
const ret = await (async () => {
// dm user
- const dmSuccess = await this.punishDM('kicked', options.reason);
+ const dmSuccess = await this.bushPunishDM('kicked', options.reason);
dmSuccessEvent = dmSuccess;
// kick
@@ -452,7 +452,7 @@ export class BushGuildMember extends GuildMember {
const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
const ret = await (async () => {
// dm user
- const dmSuccess = await this.punishDM('banned', options.reason, options.duration ?? 0);
+ const dmSuccess = await this.bushPunishDM('banned', options.reason, options.duration ?? 0);
dmSuccessEvent = dmSuccess;
// ban
@@ -507,7 +507,7 @@ export class BushGuildMember extends GuildMember {
* Prevents a user from speaking in a channel.
* @param options Options for blocking the user.
*/
- public async block(options: BlockOptions): Promise<BlockResponse> {
+ public async bushBlock(options: BlockOptions): Promise<BlockResponse> {
const _channel = this.guild.channels.resolve(options.channel);
if (!_channel || (!_channel.isText() && !_channel.isThread())) return 'invalid channel';
const channel = _channel as BushGuildTextBasedChannel;
@@ -588,7 +588,7 @@ export class BushGuildMember extends GuildMember {
* Allows a user to speak in a channel.
* @param options Options for unblocking the user.
*/
- public async unblock(options: UnblockOptions): Promise<UnblockResponse> {
+ public async bushUnblock(options: UnblockOptions): Promise<UnblockResponse> {
const _channel = this.guild.channels.resolve(options.channel);
if (!_channel || (!_channel.isText() && !_channel.isThread())) return 'invalid channel';
const channel = _channel as BushGuildTextBasedChannel;
@@ -659,6 +659,122 @@ export class BushGuildMember extends GuildMember {
}
/**
+ * Mutes a user using discord's timeout feature.
+ * @param options Options for timing out the user.
+ */
+ public async bushTimeout(options: BushTimeoutOptions): Promise<TimeoutResponse> {
+ // checks
+ if (!this.guild.me!.permissions.has('MODERATE_MEMBERS')) return 'missing permissions';
+
+ const twentyEightDays = client.consts.timeUnits.days.value * 28;
+ if (options.duration > twentyEightDays) return 'duration too long';
+
+ let caseID: string | undefined = undefined;
+ let dmSuccessEvent: boolean | undefined = undefined;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
+
+ const ret = await (async () => {
+ // timeout
+ const timeoutSuccess = await this.timeout(
+ options.duration,
+ `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`
+ ).catch(() => false);
+ if (!timeoutSuccess) return 'error timing out';
+
+ // add modlog entry
+ const { log: modlog } = await Moderation.createModLogEntry({
+ type: ModLogType.TIMEOUT,
+ user: this,
+ moderator: moderator.id,
+ reason: options.reason,
+ duration: options.duration,
+ guild: this.guild,
+ evidence: options.evidence
+ });
+
+ if (!modlog) return 'error creating modlog entry';
+ caseID = modlog.id;
+
+ // dm user
+ const dmSuccess = await this.bushPunishDM('timed out', options.reason, options.duration);
+ dmSuccessEvent = dmSuccess;
+
+ if (!dmSuccess) return 'failed to dm';
+
+ return 'success';
+ })();
+
+ if (!(['error timing out', 'error creating modlog entry'] as const).includes(ret))
+ client.emit(
+ 'bushTimeout',
+ this,
+ moderator,
+ this.guild,
+ options.reason ?? undefined,
+ caseID!,
+ options.duration ?? 0,
+ dmSuccessEvent!,
+ options.evidence
+ );
+ return ret;
+ }
+
+ /**
+ * Removes a timeout from a user.
+ * @param options Options for removing the timeout.
+ */
+ public async bushRemoveTimeout(options: BushPunishmentOptions): Promise<RemoveTimeoutResponse> {
+ // checks
+ if (!this.guild.me!.permissions.has('MODERATE_MEMBERS')) return 'missing permissions';
+
+ let caseID: string | undefined = undefined;
+ let dmSuccessEvent: boolean | undefined = undefined;
+ const moderator = (await util.resolveNonCachedUser(options.moderator ?? this.guild.me))!;
+
+ const ret = await (async () => {
+ // remove timeout
+ const timeoutSuccess = await this.timeout(null, `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`).catch(
+ () => false
+ );
+ if (!timeoutSuccess) return 'error removing timeout';
+
+ // add modlog entry
+ const { log: modlog } = await Moderation.createModLogEntry({
+ type: ModLogType.REMOVE_TIMEOUT,
+ user: this,
+ moderator: moderator.id,
+ reason: options.reason,
+ guild: this.guild,
+ evidence: options.evidence
+ });
+
+ if (!modlog) return 'error creating modlog entry';
+ caseID = modlog.id;
+
+ // dm user
+ const dmSuccess = await this.bushPunishDM('un timed out', options.reason);
+ dmSuccessEvent = dmSuccess;
+
+ if (!dmSuccess) return 'failed to dm';
+
+ return 'success';
+ })();
+
+ if (!(['error removing timeout', 'error creating modlog entry'] as const).includes(ret))
+ client.emit(
+ 'bushRemoveTimeout',
+ this,
+ moderator,
+ this.guild,
+ options.reason ?? undefined,
+ caseID!,
+ dmSuccessEvent!,
+ options.evidence
+ );
+ return ret;
+ }
+
+ /**
* Whether or not the user is an owner of the bot.
*/
public isOwner(): boolean {
@@ -763,6 +879,16 @@ export interface UnblockOptions extends BushPunishmentOptions {
channel: BushGuildTextChannelResolvable | BushThreadChannelResolvable;
}
+/**
+ * Punishment options for punishments that can be temporary.
+ */
+export interface BushTimeoutOptions extends BushPunishmentOptions {
+ /**
+ * The duration of the punishment.
+ */
+ duration: number;
+}
+
export type PunishmentResponse = 'success' | 'error creating modlog entry' | 'failed to dm';
/**
@@ -774,7 +900,7 @@ export type WarnResponse = PunishmentResponse;
* Response returned when adding a role to a user.
*/
export type AddRoleResponse =
- | PunishmentResponse
+ | Exclude<PunishmentResponse, 'failed to dm'>
| 'user hierarchy'
| 'role managed'
| 'client hierarchy'
@@ -785,7 +911,7 @@ export type AddRoleResponse =
* Response returned when removing a role from a user.
*/
export type RemoveRoleResponse =
- | PunishmentResponse
+ | Exclude<PunishmentResponse, 'failed to dm'>
| 'user hierarchy'
| 'role managed'
| 'client hierarchy'
@@ -846,11 +972,17 @@ export type UnblockResponse =
| 'missing permissions'
| 'error unblocking';
-export type PartialBushGuildMember = Partialize<
- BushGuildMember,
- 'joinedAt' | 'joinedTimestamp',
- 'warn' | 'addRole' | 'removeRole' | 'mute' | 'unmute' | 'bushKick' | 'bushBan' | 'isOwner' | 'isSuperUser' | 'block'
->;
+/**
+ * Response returned when timing out a user.
+ */
+export type TimeoutResponse = PunishmentResponse | 'missing permissions' | 'duration too long' | 'error timing out';
+
+/**
+ * Response returned when removing a timeout from a user.
+ */
+export type RemoveTimeoutResponse = PunishmentResponse | 'missing permissions' | 'duration too long' | 'error removing timeout';
+
+export type PartialBushGuildMember = Partialize<BushGuildMember, 'joinedAt' | 'joinedTimestamp'>;
/**
* @typedef {BushClientEvents} VSCodePleaseDontRemove
diff --git a/src/lib/models/ModLog.ts b/src/lib/models/ModLog.ts
index e19ddd4..7ddd64c 100644
--- a/src/lib/models/ModLog.ts
+++ b/src/lib/models/ModLog.ts
@@ -19,7 +19,9 @@ export enum ModLogType {
REMOVE_PUNISHMENT_ROLE = 'REMOVE_PUNISHMENT_ROLE',
PERM_CHANNEL_BLOCK = 'PERM_CHANNEL_BLOCK',
TEMP_CHANNEL_BLOCK = 'TEMP_CHANNEL_BLOCK',
- CHANNEL_UNBLOCK = 'CHANNEL_UNBLOCK'
+ CHANNEL_UNBLOCK = 'CHANNEL_UNBLOCK',
+ TIMEOUT = 'TIMEOUT',
+ REMOVE_TIMEOUT = 'REMOVE_TIMEOUT'
}
export interface ModLogModel {
diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts
index 4f5fb87..306f580 100644
--- a/src/lib/utils/BushConstants.ts
+++ b/src/lib/utils/BushConstants.ts
@@ -49,7 +49,7 @@ export class BushConstants {
} as const);
// Somewhat stolen from @Mzato0001
- public static TimeUnits = BushClientUtil.deepFreeze({
+ public static timeUnits = BushClientUtil.deepFreeze({
milliseconds: {
match: / (?:(?<milliseconds>-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))/im,
value: 1
diff --git a/src/listeners/member-custom/bushRemoveTimeout.ts b/src/listeners/member-custom/bushRemoveTimeout.ts
new file mode 100644
index 0000000..30a3ab4
--- /dev/null
+++ b/src/listeners/member-custom/bushRemoveTimeout.ts
@@ -0,0 +1,31 @@
+import { BushListener, type BushClientEvents } from '#lib';
+import { GuildMember, MessageEmbed } from 'discord.js';
+
+export default class BushRemoveTimeoutListener extends BushListener {
+ public constructor() {
+ super('bushRemoveTimeout', {
+ emitter: 'client',
+ event: 'bushRemoveTimeout',
+ category: 'member-custom'
+ });
+ }
+
+ public override async exec(...[victim, moderator, guild, reason, caseID, dmSuccess]: BushClientEvents['bushRemoveTimeout']) {
+ const logChannel = await guild.getLogChannel('moderation');
+ if (!logChannel) return;
+ const user = victim instanceof GuildMember ? victim.user : victim;
+
+ const logEmbed = new MessageEmbed()
+ .setColor(util.colors.discord.GREEN)
+ .setTimestamp()
+ .setFooter({ text: `CaseID: ${caseID}` })
+ .setAuthor({ name: user.tag, iconURL: user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined })
+ .addField('**Action**', `${'Remove Timeout'}`)
+ .addField('**User**', `${user} (${user.tag})`)
+ .addField('**Moderator**', `${moderator} (${moderator.tag})`)
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ .addField('**Reason**', `${reason || '[No Reason Provided]'}`);
+ if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.');
+ return await logChannel.send({ embeds: [logEmbed] });
+ }
+}
diff --git a/src/listeners/member-custom/bushTimeout.ts b/src/listeners/member-custom/bushTimeout.ts
new file mode 100644
index 0000000..63ba41d
--- /dev/null
+++ b/src/listeners/member-custom/bushTimeout.ts
@@ -0,0 +1,34 @@
+import { BushListener, type BushClientEvents } from '#lib';
+import { GuildMember, MessageEmbed } from 'discord.js';
+
+export default class BushTimeoutListener extends BushListener {
+ public constructor() {
+ super('bushTimeout', {
+ emitter: 'client',
+ event: 'bushTimeout',
+ category: 'member-custom'
+ });
+ }
+
+ public override async exec(
+ ...[victim, moderator, guild, reason, caseID, duration, dmSuccess]: BushClientEvents['bushTimeout']
+ ) {
+ const logChannel = await guild.getLogChannel('moderation');
+ if (!logChannel) return;
+ const user = victim instanceof GuildMember ? victim.user : victim;
+
+ const logEmbed = new MessageEmbed()
+ .setColor(util.colors.discord.ORANGE)
+ .setTimestamp()
+ .setFooter({ text: `CaseID: ${caseID}` })
+ .setAuthor({ name: user.tag, iconURL: user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined })
+ .addField('**Action**', `${'Timeout'}`)
+ .addField('**User**', `${user} (${user.tag})`)
+ .addField('**Moderator**', `${moderator} (${moderator.tag})`)
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ .addField('**Reason**', `${reason || '[No Reason Provided]'}`)
+ .addField('**Duration**', `${util.humanizeDuration(duration) || duration}`);
+ if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.');
+ return await logChannel.send({ embeds: [logEmbed] });
+ }
+}
diff --git a/src/tasks/removeExpiredPunishements.ts b/src/tasks/removeExpiredPunishements.ts
index 3c18ebc..7b27b07 100644
--- a/src/tasks/removeExpiredPunishements.ts
+++ b/src/tasks/removeExpiredPunishements.ts
@@ -45,7 +45,7 @@ export default class RemoveExpiredPunishmentsTask extends BushTask {
await entry.destroy(); // channel overrides are removed when the member leaves the guild
continue;
}
- const result = await member.unblock({ reason: 'Punishment expired.', channel: entry.extraInfo });
+ const result = await member.bushUnblock({ reason: 'Punishment expired.', channel: entry.extraInfo });
if (['success', 'user not blocked'].includes(result)) await entry.destroy();
else throw new Error(result);
void client.logger.verbose(`removeExpiredPunishments`, `Unblocked ${entry.user}.`);
@@ -53,7 +53,7 @@ export default class RemoveExpiredPunishmentsTask extends BushTask {
}
case ActivePunishmentType.MUTE: {
if (!member) continue;
- const result = await member.unmute({ reason: 'Punishment expired.' });
+ const result = await member.bushUnmute({ reason: 'Punishment expired.' });
if (['success', 'failed to dm'].includes(result)) await entry.destroy();
else throw new Error(result);
void client.logger.verbose(`removeExpiredPunishments`, `Unmuted ${entry.user}.`);
@@ -63,7 +63,7 @@ export default class RemoveExpiredPunishmentsTask extends BushTask {
if (!member) continue;
const role = guild?.roles?.cache?.get(entry.extraInfo);
if (!role) throw new Error(`Cannot unmute ${member.user.tag} because I cannot find the mute role.`);
- const result = await member.removeRole({
+ const result = await member.bushRemoveRole({
reason: 'Punishment expired.',
role: role,
addToModlog: true