From c8edd1b4c5effd89878c3eeb0608e9fb77079155 Mon Sep 17 00:00:00 2001 From: Vylpes Date: Sat, 5 Feb 2022 21:09:35 +0000 Subject: [PATCH] Add moderator names to audit reason (#108) --- src/commands/ban.ts | 2 +- src/commands/kick.ts | 2 +- src/commands/mute.ts | 2 +- src/commands/role.ts | 4 +- src/commands/unmute.ts | 2 +- tests/commands/ban.test.ts | 160 +++++++++++++++++++++++++++++++++- tests/commands/kick.test.ts | 103 +++++++++++++++++++++- tests/commands/mute.test.ts | 142 +++++++++++++++++++++++++++++- tests/commands/role.test.ts | 4 +- tests/commands/unmute.test.ts | 142 +++++++++++++++++++++++++++++- 10 files changed, 551 insertions(+), 12 deletions(-) diff --git a/src/commands/ban.ts b/src/commands/ban.ts index fddac6c..f565834 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -67,7 +67,7 @@ export default class Ban extends Command { const publicEmbed = new PublicEmbed(context, "", `${targetUser} has been banned`); - await targetMember.ban({ reason: reason }); + await targetMember.ban({ reason: `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}` }); logEmbed.SendToModLogsChannel(); publicEmbed.SendToCurrentChannel(); diff --git a/src/commands/kick.ts b/src/commands/kick.ts index a98c729..852b57a 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -70,7 +70,7 @@ export default class Kick extends Command { const publicEmbed = new PublicEmbed(context, "", `${targetUser} has been kicked`); - await targetMember.kick(reason); + await targetMember.kick(`Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`); logEmbed.SendToModLogsChannel(); publicEmbed.SendToCurrentChannel(); diff --git a/src/commands/mute.ts b/src/commands/mute.ts index dd0f81a..4b63527 100644 --- a/src/commands/mute.ts +++ b/src/commands/mute.ts @@ -83,7 +83,7 @@ export default class Mute extends Command { }; } - await targetMember.roles.add(mutedRole, reason); + await targetMember.roles.add(mutedRole, `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`); logEmbed.SendToModLogsChannel(); publicEmbed.SendToCurrentChannel(); diff --git a/src/commands/role.ts b/src/commands/role.ts index e9e7475..88bd5b8 100644 --- a/src/commands/role.ts +++ b/src/commands/role.ts @@ -74,7 +74,7 @@ export default class Role extends Command { } public async AddRole(context: ICommandContext, role: DiscordRole): Promise { - await context.message.member?.roles.add(role); + await context.message.member?.roles.add(role, "Toggled with role command"); const embed = new PublicEmbed(context, "", `Gave role: ${role.name}`); embed.SendToCurrentChannel(); @@ -86,7 +86,7 @@ export default class Role extends Command { } public async RemoveRole(context: ICommandContext, role: DiscordRole): Promise { - await context.message.member?.roles.remove(role); + await context.message.member?.roles.remove(role, "Toggled with role command"); const embed = new PublicEmbed(context, "", `Removed role: ${role.name}`); embed.SendToCurrentChannel(); diff --git a/src/commands/unmute.ts b/src/commands/unmute.ts index 51face6..6b5901c 100644 --- a/src/commands/unmute.ts +++ b/src/commands/unmute.ts @@ -83,7 +83,7 @@ export default class Unmute extends Command { }; } - await targetMember.roles.remove(mutedRole, reason); + await targetMember.roles.remove(mutedRole, `Moderator: ${context.message.author.tag}, Reason: ${reason || "*none*"}`); logEmbed.SendToModLogsChannel(); publicEmbed.SendToCurrentChannel(); diff --git a/tests/commands/ban.test.ts b/tests/commands/ban.test.ts index 6ed09e6..8391abb 100644 --- a/tests/commands/ban.test.ts +++ b/tests/commands/ban.test.ts @@ -89,7 +89,80 @@ describe('Execute', () => { expect(messageChannelSend).toBeCalledTimes(1); expect(logChannel.send).toBeCalledTimes(1); - expect(mentionedMember.ban).toBeCalledWith({ reason: 'Test Reason' }); + expect(mentionedMember.ban).toBeCalledWith({ reason: 'Moderator: AUTHORTAG, Reason: Test Reason' }); + }); + + test('Given moderator did not supply a reason, expect default message', async () => { + process.env = { + ROLES_MODERATOR: 'Moderator', + CHANNELS_LOGS_MOD: 'mod-logs' + }; + + const mentionedUser = { + displayAvatarURL: jest.fn(), + tag: 'USERTAG' + } as unknown as User; + const mentionedMember = { + bannable: true, + ban: jest.fn() + } as unknown as GuildMember; + const logChannel = { + name: 'mod-logs', + send: jest.fn() + } as unknown as TextChannel; + + const messageChannelSend = jest.fn(); + const messageMentionsUsersFirst = jest.fn() + .mockReturnValue(mentionedUser); + const messageGuildMember = jest.fn() + .mockReturnValue(mentionedMember); + const messageGuildChannelsCacheFind = jest.fn() + .mockImplementation((callback): TextChannel | undefined => { + const result = callback(logChannel); + + if (!result) { + return undefined; + } + + return logChannel; + }); + + const message = { + channel: { + send: messageChannelSend + }, + mentions: { + users: { + first: messageMentionsUsersFirst + } + }, + guild: { + member: messageGuildMember , + channels: { + cache: { + find: messageGuildChannelsCacheFind + } + }, + available: true + }, + author: { + tag: 'AUTHORTAG' + } + } as unknown as Message; + + const context: ICommandContext = { + name: 'ban', + args: ['ban'], + message: message + }; + + const ban = new Ban(); + + const result = await ban.execute(context); + + expect(messageChannelSend).toBeCalledTimes(1); + expect(logChannel.send).toBeCalledTimes(1); + expect(mentionedMember.ban).toBeCalledWith({ reason: 'Moderator: AUTHORTAG, Reason: *none*' }); }); test('Given user has permissions, expect embeds to be correct', async () => { @@ -257,6 +330,91 @@ describe('Execute', () => { expect(fieldReason.value).toBe('Test Reason'); }); + test('Given moderator did not supply a reason, expect reason field to be default message', async () => { + process.env = { + ROLES_MODERATOR: 'Moderator', + CHANNELS_LOGS_MOD: 'mod-logs' + }; + + const mentionedUser = { + displayAvatarURL: jest.fn().mockReturnValue('URL'), + tag: 'USERTAG' + } as unknown as User; + const mentionedMember = { + bannable: true, + ban: jest.fn() + } as unknown as GuildMember; + const logChannel = { + name: 'mod-logs', + send: jest.fn() + } as unknown as TextChannel; + + const messageChannelSend = jest.fn(); + const messageMentionsUsersFirst = jest.fn() + .mockReturnValue(mentionedUser); + const messageGuildMember = jest.fn() + .mockReturnValue(mentionedMember); + const messageGuildChannelsCacheFind = jest.fn() + .mockImplementation((callback): TextChannel | undefined => { + const result = callback(logChannel); + + if (!result) { + return undefined; + } + + return logChannel; + }); + + const message = { + channel: { + send: messageChannelSend + }, + mentions: { + users: { + first: messageMentionsUsersFirst + } + }, + guild: { + member: messageGuildMember , + channels: { + cache: { + find: messageGuildChannelsCacheFind + } + }, + available: true + }, + author: { + tag: 'AUTHORTAG' + } + } as unknown as Message; + + const context: ICommandContext = { + name: 'ban', + args: ['ban'], + message: message + }; + + const ban = new Ban(); + + const result = await ban.execute(context); + + const logEmbed = result.embeds[0]; + + const fieldUser = logEmbed.fields[0]; + const fieldModerator = logEmbed.fields[1]; + const fieldReason = logEmbed.fields[2]; + + expect(fieldUser.name).toBe("User"); + expect(fieldUser.value).toBe("[object Object] `USERTAG`"); + expect(logEmbed.thumbnail?.url).toBe("URL"); + + expect(fieldModerator.name).toBe('Moderator'); + expect(fieldModerator.value).toBe('[object Object] `AUTHORTAG`'); + + expect(fieldReason.name).toBe('Reason'); + expect(fieldReason.value).toBe('*none*'); + }); + test('Given user is not mentioned, expect error embed to be sent', async () => { process.env = { ROLES_MODERATOR: 'Moderator', diff --git a/tests/commands/kick.test.ts b/tests/commands/kick.test.ts index c5ccd68..f0fc6a4 100644 --- a/tests/commands/kick.test.ts +++ b/tests/commands/kick.test.ts @@ -94,7 +94,7 @@ describe('Execute', () => { expect(messageChannelSend).toBeCalledTimes(1); expect(logChannel.send).toBeCalledTimes(1); - expect(member.kick).toBeCalledWith('Test Reason'); + expect(member.kick).toBeCalledWith('Moderator: AUTHORTAG, Reason: Test Reason'); expect(result.embeds.length).toBe(2); @@ -124,6 +124,107 @@ describe('Execute', () => { expect(logEmbedFieldReason.value).toBe('Test Reason'); }); + test('Given moderator did not supply a reason, expect default reason to be added', async () => { + process.env = { + CHANNELS_LOGS_MOD: 'mod-logs' + }; + + const user = { + displayAvatarURL: jest.fn(), + tag: 'USERTAG' + } as unknown as User; + + const member = { + kickable: true, + kick: jest.fn() + } as unknown as GuildMember; + + const logChannel = { + name: 'mod-logs', + send: jest.fn() + } as unknown as TextChannel; + + const messageMentionsUsersFirst = jest.fn() + .mockReturnValue(user); + const messageGuildMember = jest.fn() + .mockReturnValue(member); + const messageGuildChannelsCacheFind = jest.fn() + .mockImplementation((callback): TextChannel | undefined => { + const result = callback(logChannel); + + if (!result) { + return undefined; + } + + return logChannel; + }); + const messageChannelSend = jest.fn(); + + const message = { + channel: { + send: messageChannelSend + }, + mentions: { + users: { + first: messageMentionsUsersFirst + } + }, + guild: { + member: messageGuildMember, + channels: { + cache: { + find: messageGuildChannelsCacheFind + } + }, + available: true + }, + author: { + tag: 'AUTHORTAG' + } + } as unknown as Message; + + const context: ICommandContext = { + name: "kick", + args: ["USER"], + message: message + } + + const kick = new Kick(); + + const result = await kick.execute(context); + + expect(messageChannelSend).toBeCalledTimes(1); + expect(logChannel.send).toBeCalledTimes(1); + expect(member.kick).toBeCalledWith('Moderator: AUTHORTAG, Reason: *none*'); + + expect(result.embeds.length).toBe(2); + + // Log Embed + const logEmbed = result.embeds[0]; + + expect(logEmbed.title).toBe('Member Kicked'); + expect(logEmbed.fields.length).toBe(3); + + // Log Embed -> User Field + const logEmbedFieldUser = logEmbed.fields[0]; + + expect(logEmbedFieldUser.name).toBe('User'); + expect(logEmbedFieldUser.value).toBe('[object Object] `USERTAG`'); + expect(logEmbedFieldUser.inline).toBeTruthy(); + + // Log Embed -> Moderator Field + const logEmbedFieldModerator = logEmbed.fields[1]; + + expect(logEmbedFieldModerator.name).toBe('Moderator'); + expect(logEmbedFieldModerator.value).toBe('[object Object] `AUTHORTAG`'); + + // Log Embed -> Reason Field + const logEmbedFieldReason = logEmbed.fields[2]; + + expect(logEmbedFieldReason.name).toBe('Reason'); + expect(logEmbedFieldReason.value).toBe('*none*'); + }); + test('Given target user is not found, expect user does not exist error', async () => { process.env = { CHANNELS_LOGS_MOD: 'mod-logs' diff --git a/tests/commands/mute.test.ts b/tests/commands/mute.test.ts index a0137f5..974f676 100644 --- a/tests/commands/mute.test.ts +++ b/tests/commands/mute.test.ts @@ -121,7 +121,7 @@ describe('Execute', () => { expect(messageGuildRolesCacheFind).toBeCalledTimes(1); expect(messageGuildChannelsCacheFind).toBeCalledTimes(1); expect(messageChannelSend).toBeCalledTimes(1); - expect(member.roles.add).toBeCalledWith(role, 'Test Reason'); + expect(member.roles.add).toBeCalledWith(role, 'Moderator: AUTHORTAG, Reason: Test Reason'); expect(result.embeds.length).toBe(2); @@ -144,6 +144,146 @@ describe('Execute', () => { expect(logEmbedModeratorField.name).toBe('Moderator'); expect(logEmbedModeratorField.value).toBe('[object Object] `AUTHORTAG`'); + // Log Embed -> Reason Field + const logEmbedFieldReason = logEmbed.fields[2]; + + expect(logEmbedFieldReason.name).toBe('Reason'); + expect(logEmbedFieldReason.value).toBe('Test Reason'); + + // Public Embed + const publicEmbed = result.embeds[1]; + + expect(publicEmbed.title).toBe(''); + expect(publicEmbed.description).toBe('[object Object] has been muted'); + }); + + test('Given moderator did not supply a reason, expect default reason used', async () => { + process.env = { + CHANNELS_LOGS_MOD: 'mod-logs', + ROLES_MUTED: 'Muted' + }; + + const user = { + displayAvatarURL: jest.fn(), + tag: 'USERTAG' + } as unknown as User; + + const messageAuthor = { + tag: 'AUTHORTAG' + } as unknown as User; + + const member = { + manageable: true, + roles: { + add: jest.fn() + } + } as unknown as GuildMember; + + const role = { + name: 'Muted' + } as unknown as Role; + + const logChannel = { + name: 'mod-logs', + send: jest.fn() + } as unknown as TextChannel; + + const messageMentionsUsersFirst = jest.fn() + .mockReturnValue(user); + const messageGuildMember = jest.fn() + .mockReturnValue(member); + const messageGuildRolesCacheFind = jest.fn() + .mockImplementation((callback): Role | undefined => { + const result = callback(role); + + if (!result) { + return undefined; + } + + return role; + }); + const messageChannelSend = jest.fn(); + const messageGuildChannelsCacheFind = jest.fn() + .mockImplementation((callback): TextChannel | undefined => { + const result = callback(logChannel); + + if (!result) { + return undefined; + } + + return logChannel; + }); + + const message = { + mentions: { + users: { + first: messageMentionsUsersFirst + } + }, + guild: { + member: messageGuildMember, + available: true, + roles: { + cache: { + find: messageGuildRolesCacheFind + } + }, + channels: { + cache: { + find: messageGuildChannelsCacheFind + } + } + }, + channel: { + send: messageChannelSend + }, + author: messageAuthor + } as unknown as Message; + + const context: ICommandContext = { + name: 'mute', + args: ['USER'], + message: message + }; + + const mute = new Mute(); + + const result = await mute.execute(context); + + expect(messageMentionsUsersFirst).toBeCalledTimes(1); + expect(messageGuildMember).toBeCalledWith(user); + expect(messageGuildRolesCacheFind).toBeCalledTimes(1); + expect(messageGuildChannelsCacheFind).toBeCalledTimes(1); + expect(messageChannelSend).toBeCalledTimes(1); + expect(member.roles.add).toBeCalledWith(role, 'Moderator: AUTHORTAG, Reason: *none*'); + + expect(result.embeds.length).toBe(2); + + // Log Embed + const logEmbed = result.embeds[0]; + + expect(logEmbed.title).toBe('Member Muted'); + expect(logEmbed.fields.length).toBe(3); + + // Log Embed -> User Field + const logEmbedUserField = logEmbed.fields[0]; + + expect(logEmbedUserField.name).toBe('User'); + expect(logEmbedUserField.value).toBe('[object Object] `USERTAG`'); + expect(logEmbedUserField.inline).toBeTruthy(); + + // Log Embed -> Moderator Field + const logEmbedModeratorField = logEmbed.fields[1]; + + expect(logEmbedModeratorField.name).toBe('Moderator'); + expect(logEmbedModeratorField.value).toBe('[object Object] `AUTHORTAG`'); + + // Log Embed -> Reason Field + const logEmbedFieldReason = logEmbed.fields[2]; + + expect(logEmbedFieldReason.name).toBe('Reason'); + expect(logEmbedFieldReason.value).toBe('*none*'); + // Public Embed const publicEmbed = result.embeds[1]; diff --git a/tests/commands/role.test.ts b/tests/commands/role.test.ts index f72d5da..d14d56a 100644 --- a/tests/commands/role.test.ts +++ b/tests/commands/role.test.ts @@ -355,7 +355,7 @@ describe('AddRole', () => { const result = await role.AddRole(context, discordRole); - expect(guildMemberRoleManager.add).toBeCalledWith(discordRole); + expect(guildMemberRoleManager.add).toBeCalledWith(discordRole, "Toggled with role command"); expect(messageChannelSend).toBeCalled(); expect(result.embeds.length).toBe(1); @@ -397,7 +397,7 @@ describe('RemoveRole', () => { const result = await role.RemoveRole(context, discordRole); - expect(guildMemberRoleManager.remove).toBeCalledWith(discordRole); + expect(guildMemberRoleManager.remove).toBeCalledWith(discordRole, "Toggled with role command"); expect(messageChannelSend).toBeCalled(); expect(result.embeds.length).toBe(1); diff --git a/tests/commands/unmute.test.ts b/tests/commands/unmute.test.ts index 1449b39..b61a8f8 100644 --- a/tests/commands/unmute.test.ts +++ b/tests/commands/unmute.test.ts @@ -119,7 +119,7 @@ describe('Execute', () => { expect(messageGuildRolesCacheFind).toBeCalledTimes(1); expect(messageGuildChannelsCacheFind).toBeCalledTimes(1); expect(messageChannelSend).toBeCalledTimes(1); - expect(member.roles.remove).toBeCalledWith(role, 'Test Reason'); + expect(member.roles.remove).toBeCalledWith(role, 'Moderator: AUTHORTAG, Reason: Test Reason'); expect(result.embeds.length).toBe(2); @@ -142,6 +142,146 @@ describe('Execute', () => { expect(logEmbedModeratorField.name).toBe('Moderator'); expect(logEmbedModeratorField.value).toBe('[object Object] `AUTHORTAG`'); + // Log Embed -> Reason Field + const logEmbedFieldReason = logEmbed.fields[2]; + + expect(logEmbedFieldReason.name).toBe('Reason'); + expect(logEmbedFieldReason.value).toBe('Test Reason'); + + // Public Embed + const publicEmbed = result.embeds[1]; + + expect(publicEmbed.title).toBe(''); + expect(publicEmbed.description).toBe('[object Object] has been unmuted'); + }); + + test('Given moderator did not supply a reason, expect default reason is used', async () => { + process.env = { + CHANNELS_LOGS_MOD: 'mod-logs', + ROLES_MUTED: 'Muted' + }; + + const user = { + displayAvatarURL: jest.fn(), + tag: 'USERTAG' + } as unknown as User; + + const messageAuthor = { + tag: 'AUTHORTAG' + } as unknown as User; + + const member = { + manageable: true, + roles: { + remove: jest.fn() + } + } as unknown as GuildMember; + + const role = { + name: 'Muted' + } as unknown as Role; + + const logChannel = { + name: 'mod-logs', + send: jest.fn() + } as unknown as TextChannel; + + const messageMentionsUsersFirst = jest.fn() + .mockReturnValue(user); + const messageGuildMember = jest.fn() + .mockReturnValue(member); + const messageGuildRolesCacheFind = jest.fn() + .mockImplementation((callback): Role | undefined => { + const result = callback(role); + + if (!result) { + return undefined; + } + + return role; + }); + const messageChannelSend = jest.fn(); + const messageGuildChannelsCacheFind = jest.fn() + .mockImplementation((callback): TextChannel | undefined => { + const result = callback(logChannel); + + if (!result) { + return undefined; + } + + return logChannel; + }); + + const message = { + mentions: { + users: { + first: messageMentionsUsersFirst + } + }, + guild: { + member: messageGuildMember, + available: true, + roles: { + cache: { + find: messageGuildRolesCacheFind + } + }, + channels: { + cache: { + find: messageGuildChannelsCacheFind + } + } + }, + channel: { + send: messageChannelSend + }, + author: messageAuthor + } as unknown as Message; + + const context: ICommandContext = { + name: 'mute', + args: ['USER'], + message: message + }; + + const mute = new Unmute(); + + const result = await mute.execute(context); + + expect(messageMentionsUsersFirst).toBeCalledTimes(1); + expect(messageGuildMember).toBeCalledWith(user); + expect(messageGuildRolesCacheFind).toBeCalledTimes(1); + expect(messageGuildChannelsCacheFind).toBeCalledTimes(1); + expect(messageChannelSend).toBeCalledTimes(1); + expect(member.roles.remove).toBeCalledWith(role, 'Moderator: AUTHORTAG, Reason: *none*'); + + expect(result.embeds.length).toBe(2); + + // Log Embed + const logEmbed = result.embeds[0]; + + expect(logEmbed.title).toBe('Member Unmuted'); + expect(logEmbed.fields.length).toBe(3); + + // Log Embed -> User Field + const logEmbedUserField = logEmbed.fields[0]; + + expect(logEmbedUserField.name).toBe('User'); + expect(logEmbedUserField.value).toBe('[object Object] `USERTAG`'); + expect(logEmbedUserField.inline).toBeTruthy(); + + // Log Embed -> Moderator Field + const logEmbedModeratorField = logEmbed.fields[1]; + + expect(logEmbedModeratorField.name).toBe('Moderator'); + expect(logEmbedModeratorField.value).toBe('[object Object] `AUTHORTAG`'); + + // Log Embed -> Reason Field + const logEmbedFieldReason = logEmbed.fields[2]; + + expect(logEmbedFieldReason.name).toBe('Reason'); + expect(logEmbedFieldReason.value).toBe('*none*'); + // Public Embed const publicEmbed = result.embeds[1];