aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/lib/common/util/Moderation.ts3
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts21
-rw-r--r--src/listeners/member-custom/bushBan.ts3
-rw-r--r--src/listeners/member-custom/bushBlock.ts3
-rw-r--r--src/listeners/member-custom/bushKick.ts3
-rw-r--r--src/listeners/member-custom/bushMute.ts3
-rw-r--r--src/listeners/member-custom/bushPunishRole.ts3
-rw-r--r--src/listeners/member-custom/bushPunishRoleRemove.ts3
-rw-r--r--src/listeners/member-custom/bushRemoveTimeout.ts3
-rw-r--r--src/listeners/member-custom/bushTimeout.ts3
-rw-r--r--src/listeners/member-custom/bushUnban.ts3
-rw-r--r--src/listeners/member-custom/bushUnblock.ts3
-rw-r--r--src/listeners/member-custom/bushUnmute.ts3
-rw-r--r--src/listeners/member-custom/bushWarn.ts3
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncBan.ts65
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncKick.ts65
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncTimeout.ts71
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncUnban.ts65
18 files changed, 291 insertions, 35 deletions
diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts
index c018852..e5cb872 100644
--- a/src/lib/common/util/Moderation.ts
+++ b/src/lib/common/util/Moderation.ts
@@ -93,8 +93,7 @@ export class Moderation {
const user = (await util.resolveNonCachedUser(options.user))!.id;
const moderator = (await util.resolveNonCachedUser(options.moderator))!.id;
const guild = client.guilds.resolveId(options.guild)!;
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const duration = options.duration || undefined;
+ const duration = options.duration ? options.duration : undefined;
// If guild does not exist create it so the modlog can reference a guild.
await Guild.findOrCreate({
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
index d7416bb..9a8e408 100644
--- a/src/lib/extensions/discord-akairo/BushClientUtil.ts
+++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts
@@ -682,11 +682,11 @@ export class BushClientUtil extends ClientUtil {
.filter(
(p, i, arr) =>
typeof Object.getOwnPropertyDescriptor(obj_, p)?.['get'] !== 'function' && // ignore getters
- typeof Object.getOwnPropertyDescriptor(obj_, p)?.['set'] !== 'function' && // ignore setters
- typeof obj_[p] === 'function' && //only the methods
- p !== 'constructor' && //not the constructor
- (i == 0 || p !== arr[i - 1]) && //not overriding in this prototype
- props.indexOf(p) === -1 //not overridden in a child
+ typeof Object.getOwnPropertyDescriptor(obj_, p)?.['set'] !== 'function' && // ignore setters
+ typeof obj_[p] === 'function' && // only the methods
+ p !== 'constructor' && // not the constructor
+ (i == 0 || p !== arr[i - 1]) && // not overriding in this prototype
+ props.indexOf(p) === -1 // not overridden in a child
);
const reg = /\(([\s\S]*?)\)/;
@@ -705,8 +705,8 @@ export class BushClientUtil extends ClientUtil {
)
);
} while (
- (obj_ = Object.getPrototypeOf(obj_)) && //walk-up the prototype chain
- Object.getPrototypeOf(obj_) //not the the Object prototype methods (hasOwnProperty, etc...)
+ (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain
+ Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...)
);
return props.join('\n');
@@ -801,8 +801,6 @@ export class BushClientUtil extends ClientUtil {
: message.util.parsed?.prefix ?? client.config.prefix;
}
- // public retryAsync<P extends [], R>(func: (...args: P) => R, repeatFreq: number, numRepeat: number): R | Promise<null> {}
-
/**
* Recursively apply provided options operations on object
* and all of the object properties that are either object or function.
@@ -849,6 +847,11 @@ export class BushClientUtil extends ClientUtil {
return deepLock;
}
+ public get time(): Record<keyof typeof client.constants.timeUnits, number> {
+ const values = Object.entries(client.constants.timeUnits).map(([key, value]) => [key, value.value]);
+ return Object.fromEntries(values);
+ }
+
/**
* A wrapper for the Argument class that adds custom typings.
*/
diff --git a/src/listeners/member-custom/bushBan.ts b/src/listeners/member-custom/bushBan.ts
index 1243071..c37d872 100644
--- a/src/listeners/member-custom/bushBan.ts
+++ b/src/listeners/member-custom/bushBan.ts
@@ -23,8 +23,7 @@ export default class BushBanListener extends BushListener {
.addField('**Action**', `${duration ? 'Temp Ban' : 'Perm Ban'}`)
.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('**Reason**', `${reason ? reason : '[No Reason Provided]'}`);
if (duration) logEmbed.addField('**Duration**', util.humanizeDuration(duration));
if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.');
return await logChannel.send({ embeds: [logEmbed] });
diff --git a/src/listeners/member-custom/bushBlock.ts b/src/listeners/member-custom/bushBlock.ts
index 8e8adb6..13523da 100644
--- a/src/listeners/member-custom/bushBlock.ts
+++ b/src/listeners/member-custom/bushBlock.ts
@@ -26,8 +26,7 @@ export default class BushBlockListener extends BushListener {
.addField('**Channel**', `<#${channel.id}>`)
.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('**Reason**', `${reason ? reason : '[No Reason Provided]'}`);
if (duration) logEmbed.addField('**Duration**', `${util.humanizeDuration(duration) || duration}`);
if (dmSuccess === false) logEmbed.addField('**Additional Info**', 'Could not dm user.');
diff --git a/src/listeners/member-custom/bushKick.ts b/src/listeners/member-custom/bushKick.ts
index 26e9617..52b4ff2 100644
--- a/src/listeners/member-custom/bushKick.ts
+++ b/src/listeners/member-custom/bushKick.ts
@@ -23,8 +23,7 @@ export default class BushKickListener extends BushListener {
.addField('**Action**', `${'Kick'}`)
.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('**Reason**', `${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/bushMute.ts b/src/listeners/member-custom/bushMute.ts
index a8637fd..844ae55 100644
--- a/src/listeners/member-custom/bushMute.ts
+++ b/src/listeners/member-custom/bushMute.ts
@@ -23,8 +23,7 @@ export default class BushMuteListener extends BushListener {
.addField('**Action**', `${duration ? 'Temp Mute' : 'Perm Mute'}`)
.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('**Reason**', `${reason ? reason : '[No Reason Provided]'}`);
if (duration) logEmbed.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/listeners/member-custom/bushPunishRole.ts b/src/listeners/member-custom/bushPunishRole.ts
index 731403b..9abac3c 100644
--- a/src/listeners/member-custom/bushPunishRole.ts
+++ b/src/listeners/member-custom/bushPunishRole.ts
@@ -23,8 +23,7 @@ export default class BushPunishRoleListener extends BushListener {
.addField('**Action**', `${duration ? 'Temp Punishment Role' : 'Perm Punishment Role'}`)
.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('**Reason**', `${reason ? reason : '[No Reason Provided]'}`);
if (duration) logEmbed.addField('**Duration**', util.humanizeDuration(duration));
return await logChannel.send({ embeds: [logEmbed] });
}
diff --git a/src/listeners/member-custom/bushPunishRoleRemove.ts b/src/listeners/member-custom/bushPunishRoleRemove.ts
index 7d88ec8..a24546e 100644
--- a/src/listeners/member-custom/bushPunishRoleRemove.ts
+++ b/src/listeners/member-custom/bushPunishRoleRemove.ts
@@ -24,8 +24,7 @@ export default class BushPunishRoleRemoveListener extends BushListener {
.addField('**Role**', `${role}`)
.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('**Reason**', `${reason ? reason : '[No Reason Provided]'}`);
return await logChannel.send({ embeds: [logEmbed] });
}
diff --git a/src/listeners/member-custom/bushRemoveTimeout.ts b/src/listeners/member-custom/bushRemoveTimeout.ts
index 30a3ab4..0f41039 100644
--- a/src/listeners/member-custom/bushRemoveTimeout.ts
+++ b/src/listeners/member-custom/bushRemoveTimeout.ts
@@ -23,8 +23,7 @@ export default class BushRemoveTimeoutListener extends BushListener {
.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]'}`);
+ .addField('**Reason**', `${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
index 63ba41d..a311710 100644
--- a/src/listeners/member-custom/bushTimeout.ts
+++ b/src/listeners/member-custom/bushTimeout.ts
@@ -25,8 +25,7 @@ export default class BushTimeoutListener extends BushListener {
.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('**Reason**', `${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/listeners/member-custom/bushUnban.ts b/src/listeners/member-custom/bushUnban.ts
index e7024ef..b2426e3 100644
--- a/src/listeners/member-custom/bushUnban.ts
+++ b/src/listeners/member-custom/bushUnban.ts
@@ -23,8 +23,7 @@ export default class BushUnbanListener extends BushListener {
.addField('**Action**', `${'Unban'}`)
.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('**Reason**', `${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/bushUnblock.ts b/src/listeners/member-custom/bushUnblock.ts
index e313025..43097a1 100644
--- a/src/listeners/member-custom/bushUnblock.ts
+++ b/src/listeners/member-custom/bushUnblock.ts
@@ -24,8 +24,7 @@ export default class BushUnblockListener extends BushListener {
.addField('**Channel**', `<#${channel.id}>`)
.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('**Reason**', `${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/bushUnmute.ts b/src/listeners/member-custom/bushUnmute.ts
index 4fa808f..89bca46 100644
--- a/src/listeners/member-custom/bushUnmute.ts
+++ b/src/listeners/member-custom/bushUnmute.ts
@@ -23,8 +23,7 @@ export default class BushUnmuteListener extends BushListener {
.addField('**Action**', `${'Unmute'}`)
.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('**Reason**', `${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/bushWarn.ts b/src/listeners/member-custom/bushWarn.ts
index dba9fd8..af8fa62 100644
--- a/src/listeners/member-custom/bushWarn.ts
+++ b/src/listeners/member-custom/bushWarn.ts
@@ -23,8 +23,7 @@ export default class BushWarnListener extends BushListener {
.addField('**Action**', `${'Warn'}`)
.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('**Reason**', `${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/track-manual-punishments/modlogSyncBan.ts b/src/listeners/track-manual-punishments/modlogSyncBan.ts
new file mode 100644
index 0000000..7fe2f7e
--- /dev/null
+++ b/src/listeners/track-manual-punishments/modlogSyncBan.ts
@@ -0,0 +1,65 @@
+import { BushListener, BushUser, Moderation, ModLogType, type BushClientEvents } from '#lib';
+import { MessageEmbed } from 'discord.js';
+
+export default class ModlogSyncBanListener extends BushListener {
+ public constructor() {
+ super('modlogSyncBan', {
+ emitter: 'client',
+ event: 'guildBanAdd',
+ category: 'guild'
+ });
+ }
+
+ public override async exec(...[ban]: BushClientEvents['guildBanAdd']) {
+ if (!(await ban.guild.hasFeature('logManualPunishments'))) return;
+ if (!ban.guild.me!.permissions.has('VIEW_AUDIT_LOG')) {
+ return ban.guild.error(
+ 'modlogSyncBan',
+ `Could not sync the manual ban of ${ban.user.tag} to the modlog because I do not have the "View Audit Log" permission.`
+ );
+ }
+
+ const now = new Date();
+ await util.sleep(0.5); // wait for audit log entry
+
+ const logs = (await ban.guild.fetchAuditLogs({ type: 'MEMBER_BAN_ADD' })).entries.filter(
+ (entry) => entry.target?.id === ban.user.id
+ );
+
+ const first = logs.first();
+ if (!first) return;
+
+ if (!first.executor || first.executor?.bot) return;
+
+ if (Math.abs(first.createdAt.getTime() - now.getTime()) > util.time.minutes) {
+ console.log(util.humanizeDuration(Math.abs(first.createdAt.getTime() - now.getTime())));
+ throw new Error('Time is off by over a minute');
+ }
+
+ const { log } = await Moderation.createModLogEntry({
+ type: ModLogType.PERM_BAN,
+ user: ban.user,
+ moderator: <BushUser>first.executor,
+ reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`,
+ guild: ban.guild
+ });
+ if (!log) throw new Error('Failed to create modlog entry');
+
+ const logChannel = await ban.guild.getLogChannel('moderation');
+ if (!logChannel) return;
+
+ const logEmbed = new MessageEmbed()
+ .setColor(util.colors.discord.RED)
+ .setTimestamp()
+ .setFooter({ text: `CaseID: ${log.id}` })
+ .setAuthor({
+ name: ban.user.tag,
+ iconURL: ban.user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined
+ })
+ .addField('**Action**', `${'Manual Ban'}`)
+ .addField('**User**', `${ban.user} (${ban.user.tag})`)
+ .addField('**Moderator**', `${first.executor} (${first.executor.tag})`)
+ .addField('**Reason**', `${first.reason ? first.reason : '[No Reason Provided]'}`);
+ return await logChannel.send({ embeds: [logEmbed] });
+ }
+}
diff --git a/src/listeners/track-manual-punishments/modlogSyncKick.ts b/src/listeners/track-manual-punishments/modlogSyncKick.ts
new file mode 100644
index 0000000..7c06c4d
--- /dev/null
+++ b/src/listeners/track-manual-punishments/modlogSyncKick.ts
@@ -0,0 +1,65 @@
+import { BushListener, BushUser, Moderation, ModLogType, type BushClientEvents } from '#lib';
+import { MessageEmbed } from 'discord.js';
+
+export default class ModlogSyncKickListener extends BushListener {
+ public constructor() {
+ super('modlogSyncKick', {
+ emitter: 'client',
+ event: 'guildMemberRemove',
+ category: 'guild'
+ });
+ }
+
+ public override async exec(...[member]: BushClientEvents['guildMemberRemove']) {
+ if (!(await member.guild.hasFeature('logManualPunishments'))) return;
+ if (!member.guild.me!.permissions.has('VIEW_AUDIT_LOG')) {
+ return member.guild.error(
+ 'modlogSyncKick',
+ `Could not sync the potential manual kick of ${member.user.tag} to the modlog because I do not have the "View Audit Log" permission.`
+ );
+ }
+
+ const now = new Date();
+ await util.sleep(0.5); // wait for audit log entry
+
+ const logs = (await member.guild.fetchAuditLogs({ type: 'MEMBER_KICK' })).entries.filter(
+ (entry) => entry.target?.id === member.user.id
+ );
+
+ const first = logs.first();
+ if (!first) return;
+
+ if (!first.executor || first.executor?.bot) return;
+
+ if (Math.abs(first.createdAt.getTime() - now.getTime()) > util.time.minutes) {
+ console.log(util.humanizeDuration(Math.abs(first.createdAt.getTime() - now.getTime())));
+ throw new Error('Time is off by over a minute');
+ }
+
+ const { log } = await Moderation.createModLogEntry({
+ type: ModLogType.KICK,
+ user: member.user,
+ moderator: <BushUser>first.executor,
+ reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`,
+ guild: member.guild
+ });
+ if (!log) throw new Error('Failed to create modlog entry');
+
+ const logChannel = await member.guild.getLogChannel('moderation');
+ if (!logChannel) return;
+
+ const logEmbed = new MessageEmbed()
+ .setColor(util.colors.discord.RED)
+ .setTimestamp()
+ .setFooter({ text: `CaseID: ${log.id}` })
+ .setAuthor({
+ name: member.user.tag,
+ iconURL: member.user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined
+ })
+ .addField('**Action**', `${'Manual Kick'}`)
+ .addField('**User**', `${member.user} (${member.user.tag})`)
+ .addField('**Moderator**', `${first.executor} (${first.executor.tag})`)
+ .addField('**Reason**', `${first.reason ? first.reason : '[No Reason Provided]'}`);
+ return await logChannel.send({ embeds: [logEmbed] });
+ }
+}
diff --git a/src/listeners/track-manual-punishments/modlogSyncTimeout.ts b/src/listeners/track-manual-punishments/modlogSyncTimeout.ts
new file mode 100644
index 0000000..e29af7e
--- /dev/null
+++ b/src/listeners/track-manual-punishments/modlogSyncTimeout.ts
@@ -0,0 +1,71 @@
+import { BushListener, BushUser, Moderation, ModLogType, type BushClientEvents } from '#lib';
+import { MessageEmbed } from 'discord.js';
+
+export default class ModlogSyncTimeoutListener extends BushListener {
+ public constructor() {
+ super('modlogSyncTimeout', {
+ emitter: 'client',
+ event: 'guildMemberUpdate',
+ category: 'guild'
+ });
+ }
+
+ public override async exec(...[_oldMember, newMember]: BushClientEvents['guildMemberUpdate']) {
+ if (!(await newMember.guild.hasFeature('logManualPunishments'))) return;
+ if (!newMember.guild.me!.permissions.has('VIEW_AUDIT_LOG')) {
+ return newMember.guild.error(
+ 'modlogSyncTimeout',
+ `Could not sync the potential manual timeout of ${newMember.user.tag} to the modlog because I do not have the "View Audit Log" permission.`
+ );
+ }
+
+ const now = new Date();
+ await util.sleep(0.5); // wait for audit log entry
+
+ const logs = (await newMember.guild.fetchAuditLogs({ type: 'MEMBER_UPDATE' })).entries.filter(
+ (entry) => entry.target?.id === newMember.user.id
+ );
+
+ const first = logs.first();
+ if (!first) return;
+
+ if (!first.executor || first.executor?.bot) return;
+
+ const timeOut = first.changes?.find((changes) => changes.key === 'communication_disabled_until');
+ if (!timeOut) return;
+
+ if (Math.abs(first.createdAt.getTime() - now.getTime()) > util.time.minutes) {
+ console.log(util.humanizeDuration(Math.abs(first.createdAt.getTime() - now.getTime())));
+ throw new Error('Time is off by over a minute');
+ }
+
+ const newTime = <string | null>timeOut.new ? new Date(<string>timeOut.new) : null;
+
+ const { log } = await Moderation.createModLogEntry({
+ type: newTime ? ModLogType.TIMEOUT : ModLogType.REMOVE_TIMEOUT,
+ user: newMember.user,
+ moderator: <BushUser>first.executor,
+ reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`,
+ guild: newMember.guild,
+ duration: newTime ? newTime.getTime() - now.getTime() : undefined
+ });
+ if (!log) throw new Error('Failed to create modlog entry');
+
+ const logChannel = await newMember.guild.getLogChannel('moderation');
+ if (!logChannel) return;
+
+ const logEmbed = new MessageEmbed()
+ .setColor(util.colors.discord[newTime ? 'ORANGE' : 'GREEN'])
+ .setTimestamp()
+ .setFooter({ text: `CaseID: ${log.id}` })
+ .setAuthor({
+ name: newMember.user.tag,
+ iconURL: newMember.user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined
+ })
+ .addField('**Action**', `${newTime ? 'Manual Timeout' : 'Manual Remove Timeout'}`)
+ .addField('**User**', `${newMember.user} (${newMember.user.tag})`)
+ .addField('**Moderator**', `${first.executor} (${first.executor.tag})`)
+ .addField('**Reason**', `${first.reason ? first.reason : '[No Reason Provided]'}`);
+ return await logChannel.send({ embeds: [logEmbed] });
+ }
+}
diff --git a/src/listeners/track-manual-punishments/modlogSyncUnban.ts b/src/listeners/track-manual-punishments/modlogSyncUnban.ts
new file mode 100644
index 0000000..67031b0
--- /dev/null
+++ b/src/listeners/track-manual-punishments/modlogSyncUnban.ts
@@ -0,0 +1,65 @@
+import { BushListener, BushUser, Moderation, ModLogType, type BushClientEvents } from '#lib';
+import { MessageEmbed } from 'discord.js';
+
+export default class ModlogSyncUnbanListener extends BushListener {
+ public constructor() {
+ super('modlogSyncUnban', {
+ emitter: 'client',
+ event: 'guildBanRemove',
+ category: 'guild'
+ });
+ }
+
+ public override async exec(...[ban]: BushClientEvents['guildBanRemove']) {
+ if (!(await ban.guild.hasFeature('logManualPunishments'))) return;
+ if (!ban.guild.me!.permissions.has('VIEW_AUDIT_LOG')) {
+ return ban.guild.error(
+ 'modlogSyncBan',
+ `Could not sync the manual unban of ${ban.user.tag} to the modlog because I do not have the "View Audit Log" permission.`
+ );
+ }
+
+ const now = new Date();
+ await util.sleep(0.5); // wait for audit log entry
+
+ const logs = (await ban.guild.fetchAuditLogs({ type: 'MEMBER_BAN_REMOVE' })).entries.filter(
+ (entry) => entry.target?.id === ban.user.id
+ );
+
+ const first = logs.first();
+ if (!first) return;
+
+ if (!first.executor || first.executor?.bot) return;
+
+ if (Math.abs(first.createdAt.getTime() - now.getTime()) > util.time.minutes) {
+ console.log(util.humanizeDuration(Math.abs(first.createdAt.getTime() - now.getTime())));
+ throw new Error('Time is off by over a minute');
+ }
+
+ const { log } = await Moderation.createModLogEntry({
+ type: ModLogType.UNBAN,
+ user: ban.user,
+ moderator: <BushUser>first.executor,
+ reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`,
+ guild: ban.guild
+ });
+ if (!log) throw new Error('Failed to create modlog entry');
+
+ const logChannel = await ban.guild.getLogChannel('moderation');
+ if (!logChannel) return;
+
+ const logEmbed = new MessageEmbed()
+ .setColor(util.colors.discord.ORANGE)
+ .setTimestamp()
+ .setFooter({ text: `CaseID: ${log.id}` })
+ .setAuthor({
+ name: ban.user.tag,
+ iconURL: ban.user.avatarURL({ dynamic: true, format: 'png', size: 4096 }) ?? undefined
+ })
+ .addField('**Action**', `${'Manual Unban'}`)
+ .addField('**User**', `${ban.user} (${ban.user.tag})`)
+ .addField('**Moderator**', `${first.executor} (${first.executor.tag})`)
+ .addField('**Reason**', `${first.reason ? first.reason : '[No Reason Provided]'}`);
+ return await logChannel.send({ embeds: [logEmbed] });
+ }
+}