Create timeout command #302
7 changed files with 1036 additions and 9 deletions
151
src/commands/timeout.ts
Normal file
151
src/commands/timeout.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
import { CacheType, CommandInteraction, EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder, TextChannel } from "discord.js";
|
||||
import { AuditType } from "../constants/AuditType";
|
||||
import EmbedColours from "../constants/EmbedColours";
|
||||
import Audit from "../database/entities/Audit";
|
||||
import SettingsHelper from "../helpers/SettingsHelper";
|
||||
import TimeLengthInput from "../helpers/TimeLengthInput";
|
||||
import { Command } from "../type/command";
|
||||
|
||||
export default class Timeout extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
super.CommandBuilder = new SlashCommandBuilder()
|
||||
.setName("timeout")
|
||||
.setDescription("Timeouts a user out, sending them a DM with the reason if possible")
|
||||
.setDefaultMemberPermissions(PermissionsBitField.Flags.ModerateMembers)
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('target')
|
||||
.setDescription('The user')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName("length")
|
||||
.setDescription("How long to timeout for? (Example: 24h, 60m)")
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reason')
|
||||
.setDescription('The reason'));
|
||||
}
|
||||
|
||||
public override async execute(interaction: CommandInteraction<CacheType>) {
|
||||
if (!interaction.guild || !interaction.guildId) return;
|
||||
|
||||
// Interaction Inputs
|
||||
const targetUser = interaction.options.get('target');
|
||||
const lengthInput = interaction.options.get('length');
|
||||
const reasonInput = interaction.options.get('reason');
|
||||
|
||||
// Validation
|
||||
if (!targetUser || !targetUser.user || !targetUser.member) {
|
||||
await interaction.reply('Fields are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lengthInput || !lengthInput.value) {
|
||||
await interaction.reply('Fields are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
// General Variables
|
||||
const targetMember = targetUser.member as GuildMember;
|
||||
const reason = reasonInput && reasonInput.value ? reasonInput.value.toString() : null;
|
||||
|
||||
const timeLength = new TimeLengthInput(lengthInput.value.toString());
|
||||
|
||||
const logEmbed = new EmbedBuilder()
|
||||
.setColor(EmbedColours.Ok)
|
||||
.setTitle("Member Timed Out")
|
||||
.setDescription(`<@${targetUser.user.id}> \`${targetUser.user.tag}\``)
|
||||
.addFields([
|
||||
{
|
||||
name: "Moderator",
|
||||
value: `<@${interaction.user.id}>`,
|
||||
},
|
||||
{
|
||||
name: "Reason",
|
||||
value: reason || "*none*",
|
||||
},
|
||||
{
|
||||
name: "Length",
|
||||
value: timeLength.GetLengthShort(),
|
||||
},
|
||||
{
|
||||
name: "Until",
|
||||
value: timeLength.GetDateFromNow().toString(),
|
||||
},
|
||||
]);
|
||||
|
||||
// Bot Permissions Check
|
||||
if (!targetMember.manageable) {
|
||||
await interaction.reply('Insufficient bot permissions. Please contact a moderator.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute Timeout
|
||||
await targetMember.timeout(timeLength.GetMilliseconds(), reason || "");
|
||||
|
||||
// Log Embed To Channel
|
||||
const channelName = await SettingsHelper.GetSetting('channels.logs.mod', interaction.guildId);
|
||||
|
||||
if (!channelName) return;
|
||||
|
||||
const channel = interaction.guild.channels.cache.find(x => x.name == channelName) as TextChannel;
|
||||
|
||||
if (channel) {
|
||||
await channel.send({ embeds: [ logEmbed ]});
|
||||
}
|
||||
|
||||
// Create Audit
|
||||
const audit = new Audit(targetUser.user.id, AuditType.Timeout, reason || "*none*", interaction.user.id, interaction.guildId);
|
||||
await audit.Save(Audit, audit);
|
||||
|
||||
// DM User, if possible
|
||||
const resultEmbed = new EmbedBuilder()
|
||||
.setColor(EmbedColours.Ok)
|
||||
.setDescription(`<@${targetUser.user.id}> has been timed out`);
|
||||
|
||||
const dmEmbed = new EmbedBuilder()
|
||||
.setColor(EmbedColours.Ok)
|
||||
.setDescription(`You have been timed out in ${interaction.guild.name}`)
|
||||
.addFields([
|
||||
{
|
||||
name: "Reason",
|
||||
value: reason || "*none*"
|
||||
},
|
||||
{
|
||||
name: "Length",
|
||||
value: timeLength.GetLengthShort(),
|
||||
},
|
||||
{
|
||||
name: "Until",
|
||||
value: timeLength.GetDateFromNow().toString(),
|
||||
},
|
||||
]);
|
||||
|
||||
try {
|
||||
const dmChannel = await targetUser.user.createDM();
|
||||
|
||||
await dmChannel.send({ embeds: [ dmEmbed ]});
|
||||
|
||||
resultEmbed.addFields([
|
||||
{
|
||||
name: "DM Sent",
|
||||
value: "true",
|
||||
},
|
||||
]);
|
||||
} catch {
|
||||
resultEmbed.addFields([
|
||||
{
|
||||
name: "DM Sent",
|
||||
value: "false",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Success Reply
|
||||
await interaction.reply({ embeds: [ resultEmbed ]});
|
||||
}
|
||||
}
|
|
@ -4,4 +4,5 @@ export enum AuditType {
|
|||
Mute,
|
||||
Kick,
|
||||
Ban,
|
||||
Timeout,
|
||||
}
|
|
@ -13,6 +13,8 @@ export default class AuditTools {
|
|||
return "Kick";
|
||||
case AuditType.Ban:
|
||||
return "Ban";
|
||||
case AuditType.Timeout:
|
||||
return "Timeout";
|
||||
default:
|
||||
return "Other";
|
||||
}
|
||||
|
@ -30,6 +32,8 @@ export default class AuditTools {
|
|||
return AuditType.Kick;
|
||||
case "ban":
|
||||
return AuditType.Ban;
|
||||
case "timeout":
|
||||
return AuditType.Timeout;
|
||||
default:
|
||||
return AuditType.General;
|
||||
}
|
||||
|
|
119
src/helpers/TimeLengthInput.ts
Normal file
119
src/helpers/TimeLengthInput.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
export default class TimeLengthInput {
|
||||
public readonly value: string;
|
||||
|
||||
constructor(input: string) {
|
||||
this.value = input;
|
||||
}
|
||||
|
||||
public GetDays(): number {
|
||||
return this.GetValue('d');
|
||||
}
|
||||
|
||||
public GetHours(): number {
|
||||
return this.GetValue('h');
|
||||
}
|
||||
|
||||
public GetMinutes(): number {
|
||||
return this.GetValue('m');
|
||||
}
|
||||
|
||||
public GetSeconds(): number {
|
||||
return this.GetValue('s');
|
||||
}
|
||||
|
||||
public GetMilliseconds(): number {
|
||||
const days = this.GetDays();
|
||||
const hours = this.GetHours();
|
||||
const minutes = this.GetMinutes();
|
||||
const seconds = this.GetSeconds();
|
||||
|
||||
let milliseconds = 0;
|
||||
|
||||
milliseconds += seconds * 1000;
|
||||
milliseconds += minutes * 60 * 1000;
|
||||
milliseconds += hours * 60 * 60 * 1000;
|
||||
milliseconds += days * 24 * 60 * 60 * 1000;
|
||||
|
||||
return milliseconds;
|
||||
}
|
||||
|
||||
public GetDateFromNow(): Date {
|
||||
const now = Date.now();
|
||||
|
||||
const dateFromNow = now
|
||||
+ (1000 * this.GetSeconds())
|
||||
+ (1000 * 60 * this.GetMinutes())
|
||||
+ (1000 * 60 * 60 * this.GetHours())
|
||||
+ (1000 * 60 * 60 * 24 * this.GetDays());
|
||||
|
||||
return new Date(dateFromNow);
|
||||
}
|
||||
|
||||
public GetLength(): string {
|
||||
const days = this.GetDays();
|
||||
const hours = this.GetHours();
|
||||
const minutes = this.GetMinutes();
|
||||
const seconds = this.GetSeconds();
|
||||
|
||||
const value = [];
|
||||
|
||||
if (days) {
|
||||
value.push(`${days} days`);
|
||||
}
|
||||
|
||||
if (hours) {
|
||||
value.push(`${hours} hours`);
|
||||
}
|
||||
|
||||
if (minutes) {
|
||||
value.push(`${minutes} minutes`);
|
||||
}
|
||||
|
||||
if (seconds) {
|
||||
value.push(`${seconds} seconds`);
|
||||
}
|
||||
|
||||
return value.join(", ");
|
||||
}
|
||||
|
||||
public GetLengthShort(): string {
|
||||
const days = this.GetDays();
|
||||
const hours = this.GetHours();
|
||||
const minutes = this.GetMinutes();
|
||||
const seconds = this.GetSeconds();
|
||||
|
||||
const value = [];
|
||||
|
||||
if (days) {
|
||||
value.push(`${days}d`);
|
||||
}
|
||||
|
||||
if (hours) {
|
||||
value.push(`${hours}h`);
|
||||
}
|
||||
|
||||
if (minutes) {
|
||||
value.push(`${minutes}m`);
|
||||
}
|
||||
|
||||
if (seconds) {
|
||||
value.push(`${seconds}s`);
|
||||
}
|
||||
|
||||
return value.join(" ");
|
||||
}
|
||||
|
||||
private GetValue(designation: string): number {
|
||||
const valueSplit = this.value.split(' ');
|
||||
|
||||
const desString = valueSplit.find(x => x.charAt(x.length - 1) == designation);
|
||||
|
||||
if (!desString) return 0;
|
||||
|
||||
const desNumber = Number(desString.substring(0, desString.length - 1));
|
||||
|
||||
if (!desNumber) return 0;
|
||||
|
||||
return desNumber;
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import Role from "./commands/Role/role";
|
|||
import ConfigRole from "./commands/Role/config";
|
||||
import Rules from "./commands/rules";
|
||||
import Setup from "./commands/setup";
|
||||
import Timeout from "./commands/timeout";
|
||||
import Unmute from "./commands/unmute";
|
||||
import Warn from "./commands/warn";
|
||||
|
||||
|
@ -38,6 +39,7 @@ import MessageCreate from "./events/MessageEvents/MessageCreate";
|
|||
export default class Registry {
|
||||
public static RegisterCommands() {
|
||||
CoreClient.RegisterCommand("about", new About());
|
||||
CoreClient.RegisterCommand("audits", new Audits());
|
||||
CoreClient.RegisterCommand("ban", new Ban());
|
||||
CoreClient.RegisterCommand("bunny", new Bunny());
|
||||
CoreClient.RegisterCommand("clear", new Clear());
|
||||
|
@ -48,10 +50,10 @@ export default class Registry {
|
|||
CoreClient.RegisterCommand("kick", new Kick());
|
||||
CoreClient.RegisterCommand("mute", new Mute());
|
||||
CoreClient.RegisterCommand("rules", new Rules());
|
||||
CoreClient.RegisterCommand("setup", new Setup());
|
||||
CoreClient.RegisterCommand("timeout", new Timeout());
|
||||
CoreClient.RegisterCommand("unmute", new Unmute());
|
||||
CoreClient.RegisterCommand("warn", new Warn());
|
||||
CoreClient.RegisterCommand("setup", new Setup());
|
||||
CoreClient.RegisterCommand("audits", new Audits());
|
||||
|
||||
CoreClient.RegisterCommand("role", new Role());
|
||||
CoreClient.RegisterCommand("configrole", new ConfigRole());
|
||||
|
|
729
tests/commands/timeout.test.ts
Normal file
729
tests/commands/timeout.test.ts
Normal file
|
@ -0,0 +1,729 @@
|
|||
import { APIEmbed, CacheType, CommandInteraction, CommandInteractionOption, DMChannel, Embed, EmbedBuilder, EmbedField, Guild, GuildChannel, GuildMember, InteractionReplyOptions, JSONEncodable, Message, MessageCreateOptions, MessagePayload, SlashCommandBuilder, TextChannel, User } from "discord.js";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import Timeout from "../../src/commands/timeout";
|
||||
import SettingsHelper from "../../src/helpers/SettingsHelper";
|
||||
import Audit from "../../src/database/entities/Audit";
|
||||
import EmbedColours from "../../src/constants/EmbedColours";
|
||||
import { DeepPartial, EntityTarget } from "typeorm";
|
||||
import BaseEntity from "../../src/contracts/BaseEntity";
|
||||
import { AuditType } from "../../src/constants/AuditType";
|
||||
|
||||
describe('Constructor', () => {
|
||||
test('EXPECT CommandBuilder to be configured', () => {
|
||||
const command = new Timeout();
|
||||
|
||||
expect(command.CommandBuilder).toBeDefined();
|
||||
|
||||
const commandBuilder = command.CommandBuilder as SlashCommandBuilder;
|
||||
|
||||
expect(commandBuilder.name).toBe("timeout");
|
||||
expect(commandBuilder.description).toBe("Timeouts a user out, sending them a DM with the reason if possible");
|
||||
expect(commandBuilder.options.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
// Happy flow
|
||||
test('GIVEN all checks have passed, EXPECT user to be timed out', async () => {
|
||||
let embeds: APIEmbed[] | undefined;
|
||||
|
||||
const command = new Timeout();
|
||||
|
||||
const interactionReply = jest.fn((options: InteractionReplyOptions) => {
|
||||
embeds = options.embeds as APIEmbed[];
|
||||
});
|
||||
|
||||
let savedAudit: DeepPartial<Audit> | undefined;
|
||||
|
||||
const getSetting = jest.spyOn(SettingsHelper, 'GetSetting').mockResolvedValue('mod-logs');
|
||||
const auditSave = jest.spyOn(Audit.prototype, 'Save').mockImplementation((target: EntityTarget<BaseEntity>, entity: DeepPartial<BaseEntity>): Promise<void> => {
|
||||
savedAudit = entity;
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
const timeoutFunc = jest.fn();
|
||||
|
||||
let dmChannelSentEmbeds: (APIEmbed | JSONEncodable<APIEmbed>)[] | undefined;
|
||||
let logsChannelSentEmbeds: (APIEmbed | JSONEncodable<APIEmbed>)[] | undefined;
|
||||
|
||||
const dmChannel = {
|
||||
send: jest.fn().mockImplementation((options: MessageCreateOptions) => {
|
||||
dmChannelSentEmbeds = options.embeds;
|
||||
}),
|
||||
} as unknown as DMChannel;
|
||||
|
||||
const userInput = {
|
||||
user: {
|
||||
id: 'userId',
|
||||
tag: 'userTag',
|
||||
createDM: jest.fn().mockResolvedValue(dmChannel),
|
||||
} as unknown as User,
|
||||
member: {
|
||||
manageable: true,
|
||||
timeout: timeoutFunc,
|
||||
} as unknown as GuildMember,
|
||||
} as CommandInteractionOption;
|
||||
|
||||
const lengthInput = {
|
||||
value: '1s',
|
||||
} as CommandInteractionOption;
|
||||
|
||||
const reasonInput = {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
|
||||
const logsChannel = {
|
||||
name: 'mod-logs',
|
||||
send: jest.fn().mockImplementation((options: MessageCreateOptions) => {
|
||||
logsChannelSentEmbeds = options.embeds;
|
||||
}),
|
||||
} as unknown as TextChannel;
|
||||
|
||||
const interaction = {
|
||||
guild: {
|
||||
channels: {
|
||||
cache: {
|
||||
find: jest.fn()
|
||||
.mockReturnValue(logsChannel),
|
||||
}
|
||||
},
|
||||
name: "Test Guild",
|
||||
} as unknown as Guild,
|
||||
guildId: 'guildId',
|
||||
reply: interactionReply,
|
||||
options: {
|
||||
get: jest.fn()
|
||||
.mockReturnValueOnce(userInput)
|
||||
.mockReturnValueOnce(lengthInput)
|
||||
.mockReturnValue(reasonInput),
|
||||
},
|
||||
user: {
|
||||
id: 'moderatorId'
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
// EXPECT user to be timed out
|
||||
expect(timeoutFunc).toBeCalledWith(1000, 'Test reason');
|
||||
|
||||
// EXPECT embeds to be sent
|
||||
expect(embeds).toBeDefined();
|
||||
expect(embeds!.length).toBe(1);
|
||||
|
||||
// EXPECT resultEmbed to be correctly configured
|
||||
const resultEmbed = embeds![0] as EmbedBuilder;
|
||||
|
||||
expect(resultEmbed.data.description).toBe('<@userId> has been timed out');
|
||||
expect(resultEmbed.data.fields).toBeDefined();
|
||||
expect(resultEmbed.data.fields!.length).toBe(1);
|
||||
|
||||
// EXPECT DM field to be configured
|
||||
const resultEmbedDMField = resultEmbed.data.fields![0];
|
||||
|
||||
expect(resultEmbedDMField.name).toBe("DM Sent");
|
||||
expect(resultEmbedDMField.value).toBe("true");
|
||||
|
||||
// EXPECT user to be DM's with embed
|
||||
expect(dmChannel.send).toBeCalled();
|
||||
expect(dmChannelSentEmbeds).toBeDefined();
|
||||
expect(dmChannelSentEmbeds?.length).toBe(1);
|
||||
|
||||
const dmChannelSentEmbed = (dmChannelSentEmbeds![0] as any).data;
|
||||
|
||||
expect(dmChannelSentEmbed.color).toBe(EmbedColours.Ok);
|
||||
expect(dmChannelSentEmbed.description).toBe("You have been timed out in Test Guild");
|
||||
expect(dmChannelSentEmbed.fields?.length).toBe(3);
|
||||
|
||||
expect(dmChannelSentEmbed.fields![0].name).toBe("Reason");
|
||||
expect(dmChannelSentEmbed.fields![0].value).toBe("Test reason");
|
||||
|
||||
expect(dmChannelSentEmbed.fields![1].name).toBe("Length");
|
||||
expect(dmChannelSentEmbed.fields![1].value).toBe("1s");
|
||||
|
||||
expect(dmChannelSentEmbed.fields![2].name).toBe("Until");
|
||||
expect(dmChannelSentEmbed.fields![2].value).toBeDefined();
|
||||
|
||||
// EXPECT log embed to be sent
|
||||
expect(logsChannel.send).toBeCalled();
|
||||
expect(logsChannelSentEmbeds).toBeDefined();
|
||||
expect(logsChannelSentEmbeds?.length).toBe(1);
|
||||
|
||||
const logsChannelSentEmbed = (logsChannelSentEmbeds![0] as any).data;
|
||||
|
||||
expect(logsChannelSentEmbed.color).toBe(EmbedColours.Ok);
|
||||
expect(logsChannelSentEmbed.title).toBe("Member Timed Out");
|
||||
expect(logsChannelSentEmbed.description).toBe("<@userId> `userTag`");
|
||||
expect(logsChannelSentEmbed.fields?.length).toBe(4);
|
||||
|
||||
expect(logsChannelSentEmbed.fields![0].name).toBe("Moderator");
|
||||
expect(logsChannelSentEmbed.fields![0].value).toBe("<@moderatorId>");
|
||||
|
||||
expect(logsChannelSentEmbed.fields![1].name).toBe("Reason");
|
||||
expect(logsChannelSentEmbed.fields![1].value).toBe("Test reason");
|
||||
|
||||
expect(logsChannelSentEmbed.fields![2].name).toBe("Length");
|
||||
expect(logsChannelSentEmbed.fields![2].value).toBe("1s");
|
||||
|
||||
expect(logsChannelSentEmbed.fields![3].name).toBe("Until");
|
||||
expect(logsChannelSentEmbed.fields![3].value).toBeDefined();
|
||||
|
||||
// EXPECT Audit to be saved
|
||||
expect(auditSave).toBeCalled();
|
||||
|
||||
expect(savedAudit).toBeDefined();
|
||||
expect(savedAudit?.UserId).toBe('userId');
|
||||
expect(savedAudit?.AuditType).toBe(AuditType.Timeout);
|
||||
expect(savedAudit?.Reason).toBe("Test reason");
|
||||
expect(savedAudit?.ModeratorId).toBe('moderatorId');
|
||||
expect(savedAudit?.ServerId).toBe('guildId');
|
||||
});
|
||||
|
||||
// Null checks
|
||||
test('GIVEN interaction.guild IS NULL, EXPECT nothing to happen', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
const interaction = {
|
||||
guild: null,
|
||||
reply: jest.fn(),
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(interaction.reply).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('GIVEN interaction.guildId IS NULL, EXPECT nothing to happen', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
const interaction = {
|
||||
guild: mock<Guild>(),
|
||||
guildId: null,
|
||||
reply: jest.fn(),
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(interaction.reply).not.toBeCalled();
|
||||
});
|
||||
|
||||
// Validation
|
||||
test('GIVEN targetUser IS NULL, EXPECT validation error', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: mock<Guild>(),
|
||||
guildId: 'guildId',
|
||||
options: {
|
||||
get: jest.fn().mockReturnValue(undefined),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toBeCalledWith('Fields are required.');
|
||||
});
|
||||
|
||||
test('GIVEN targetUser.user IS NULL, EXPECT validation error', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: mock<Guild>(),
|
||||
guildId: 'guildId',
|
||||
options: {
|
||||
get: jest.fn((value: string): CommandInteractionOption<CacheType> | null => {
|
||||
switch (value) {
|
||||
case 'target':
|
||||
return {} as CommandInteractionOption;
|
||||
case 'length':
|
||||
return {
|
||||
value: '1m',
|
||||
} as CommandInteractionOption;
|
||||
case 'reason':
|
||||
return {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toBeCalledWith('Fields are required.');
|
||||
});
|
||||
|
||||
test('GIVEN targetUser.member IS NULL, EXPECT validation error', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: mock<Guild>(),
|
||||
guildId: 'guildId',
|
||||
options: {
|
||||
get: jest.fn((value: string): CommandInteractionOption<CacheType> | null => {
|
||||
switch (value) {
|
||||
case 'target':
|
||||
return {
|
||||
user: {} as User,
|
||||
} as CommandInteractionOption;
|
||||
case 'length':
|
||||
return {
|
||||
value: '1m',
|
||||
} as CommandInteractionOption;
|
||||
case 'reason':
|
||||
return {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toBeCalledWith('Fields are required.');
|
||||
});
|
||||
|
||||
test('GIVEN lengthInput IS NULL, EXPECT validation error', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: mock<Guild>(),
|
||||
guildId: 'guildId',
|
||||
options: {
|
||||
get: jest.fn((value: string): CommandInteractionOption<CacheType> | null => {
|
||||
switch (value) {
|
||||
case 'target':
|
||||
return {
|
||||
user: {} as User,
|
||||
member: {} as GuildMember
|
||||
} as CommandInteractionOption;
|
||||
case 'length':
|
||||
return null;
|
||||
case 'reason':
|
||||
return {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toBeCalledWith('Fields are required.');
|
||||
});
|
||||
|
||||
test('GIVEN lengthInput.value IS NULL, EXPECT validation error', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: mock<Guild>(),
|
||||
guildId: 'guildId',
|
||||
options: {
|
||||
get: jest.fn((value: string): CommandInteractionOption<CacheType> | null => {
|
||||
switch (value) {
|
||||
case 'target':
|
||||
return {
|
||||
user: {} as User,
|
||||
member: {} as GuildMember
|
||||
} as CommandInteractionOption;
|
||||
case 'length':
|
||||
return {
|
||||
value: undefined,
|
||||
} as CommandInteractionOption;
|
||||
case 'reason':
|
||||
return {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toBeCalledWith('Fields are required.');
|
||||
});
|
||||
|
||||
test('GIVEN targetMember IS NOT manageable by the bot, EXPECT insufficient permissions error', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: mock<Guild>(),
|
||||
guildId: 'guildId',
|
||||
user: {
|
||||
id: 'moderatorId',
|
||||
},
|
||||
options: {
|
||||
get: jest.fn((value: string): CommandInteractionOption<CacheType> | null => {
|
||||
switch (value) {
|
||||
case 'target':
|
||||
return {
|
||||
user: {
|
||||
id: 'userId',
|
||||
tag: 'userTag',
|
||||
} as User,
|
||||
member: {
|
||||
manageable: false,
|
||||
} as GuildMember
|
||||
} as CommandInteractionOption;
|
||||
case 'length':
|
||||
return {
|
||||
value: '1m',
|
||||
} as CommandInteractionOption;
|
||||
case 'reason':
|
||||
return {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(interaction.reply).toBeCalledWith('Insufficient bot permissions. Please contact a moderator.');
|
||||
});
|
||||
|
||||
// Reason variable
|
||||
test('GIVEN reason IS NULL, EXPECT to be ran with empty string', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
let savedAudit: DeepPartial<Audit> | undefined;
|
||||
|
||||
const auditSave = jest.spyOn(Audit.prototype, 'Save').mockImplementation((target: EntityTarget<BaseEntity>, entity: DeepPartial<BaseEntity>): Promise<void> => {
|
||||
savedAudit = entity;
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
const timeoutFunc = jest.fn();
|
||||
|
||||
const sentEmbeds: EmbedBuilder[] = [];
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: {
|
||||
channels: {
|
||||
cache: {
|
||||
find: jest.fn().mockReturnValue(mock<TextChannel>()),
|
||||
}
|
||||
}
|
||||
},
|
||||
guildId: 'guildId',
|
||||
user: {
|
||||
id: 'moderatorId',
|
||||
},
|
||||
options: {
|
||||
get: jest.fn((value: string): CommandInteractionOption<CacheType> | null => {
|
||||
switch (value) {
|
||||
case 'target':
|
||||
return {
|
||||
user: {
|
||||
id: 'userId',
|
||||
tag: 'userTag',
|
||||
createDM: jest.fn().mockReturnValue({
|
||||
send: jest.fn(async (options: MessageCreateOptions): Promise<Message<false>> => {
|
||||
sentEmbeds.push(options.embeds![0] as EmbedBuilder);
|
||||
|
||||
return mock<Message<false>>();
|
||||
})
|
||||
}) as unknown as DMChannel,
|
||||
} as unknown as User,
|
||||
member: {
|
||||
manageable: true,
|
||||
timeout: timeoutFunc,
|
||||
} as unknown as GuildMember
|
||||
} as CommandInteractionOption;
|
||||
case 'length':
|
||||
return {
|
||||
value: '1m'
|
||||
} as CommandInteractionOption;
|
||||
case 'reason':
|
||||
return {
|
||||
value: undefined,
|
||||
} as CommandInteractionOption;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(timeoutFunc).toBeCalledWith(1000 * 60 * 1, "");
|
||||
expect(savedAudit?.Reason).toBe("*none*");
|
||||
|
||||
const dmEmbed = (sentEmbeds[0] as any).data;
|
||||
const dmEmbedReasonField = dmEmbed.fields![0] as EmbedField;
|
||||
|
||||
expect(dmEmbedReasonField.value).toBe("*none*");
|
||||
});
|
||||
|
||||
// Log embed
|
||||
test('GIVEN channelName IS NULL, EXPECT execution to return', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
let savedAudit: DeepPartial<Audit> | undefined;
|
||||
|
||||
const auditSave = jest.spyOn(Audit.prototype, 'Save').mockImplementation((target: EntityTarget<BaseEntity>, entity: DeepPartial<BaseEntity>): Promise<void> => {
|
||||
savedAudit = entity;
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
const settingsGet = jest.spyOn(SettingsHelper, 'GetSetting').mockResolvedValue(undefined);
|
||||
|
||||
const timeoutFunc = jest.fn();
|
||||
|
||||
const sentEmbeds: EmbedBuilder[] = [];
|
||||
|
||||
const logChannelSendFunc = jest.fn();
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: {
|
||||
channels: {
|
||||
cache: {
|
||||
find: jest.fn().mockReturnValue({
|
||||
send: logChannelSendFunc,
|
||||
} as unknown as TextChannel),
|
||||
}
|
||||
}
|
||||
},
|
||||
guildId: 'guildId',
|
||||
user: {
|
||||
id: 'moderatorId',
|
||||
},
|
||||
options: {
|
||||
get: jest.fn((value: string): CommandInteractionOption<CacheType> | null => {
|
||||
switch (value) {
|
||||
case 'target':
|
||||
return {
|
||||
user: {
|
||||
id: 'userId',
|
||||
tag: 'userTag',
|
||||
createDM: jest.fn().mockReturnValue({
|
||||
send: jest.fn(async (options: MessageCreateOptions): Promise<Message<false>> => {
|
||||
sentEmbeds.push(options.embeds![0] as EmbedBuilder);
|
||||
|
||||
return mock<Message<false>>();
|
||||
})
|
||||
}) as unknown as DMChannel,
|
||||
} as unknown as User,
|
||||
member: {
|
||||
manageable: true,
|
||||
timeout: timeoutFunc,
|
||||
} as unknown as GuildMember
|
||||
} as CommandInteractionOption;
|
||||
case 'length':
|
||||
return {
|
||||
value: '1m'
|
||||
} as CommandInteractionOption;
|
||||
case 'reason':
|
||||
return {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(timeoutFunc).toBeCalled();
|
||||
expect(sentEmbeds.length).toBe(0);
|
||||
expect(logChannelSendFunc).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('GIVEN channel IS NULL, EXPECT embed to not be sent', async () => {
|
||||
const command = new Timeout();
|
||||
|
||||
let savedAudit: DeepPartial<Audit> | undefined;
|
||||
|
||||
const auditSave = jest.spyOn(Audit.prototype, 'Save').mockImplementation((target: EntityTarget<BaseEntity>, entity: DeepPartial<BaseEntity>): Promise<void> => {
|
||||
savedAudit = entity;
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
const settingsGet = jest.spyOn(SettingsHelper, 'GetSetting').mockResolvedValue('mod-logs');
|
||||
|
||||
const timeoutFunc = jest.fn();
|
||||
|
||||
const sentEmbeds: EmbedBuilder[] = [];
|
||||
|
||||
const interaction = {
|
||||
reply: jest.fn(),
|
||||
guild: {
|
||||
channels: {
|
||||
cache: {
|
||||
find: jest.fn().mockReturnValue(undefined),
|
||||
}
|
||||
}
|
||||
},
|
||||
guildId: 'guildId',
|
||||
user: {
|
||||
id: 'moderatorId',
|
||||
},
|
||||
options: {
|
||||
get: jest.fn((value: string): CommandInteractionOption<CacheType> | null => {
|
||||
switch (value) {
|
||||
case 'target':
|
||||
return {
|
||||
user: {
|
||||
id: 'userId',
|
||||
tag: 'userTag',
|
||||
createDM: jest.fn().mockReturnValue({
|
||||
send: jest.fn(async (options: MessageCreateOptions): Promise<Message<false>> => {
|
||||
sentEmbeds.push(options.embeds![0] as EmbedBuilder);
|
||||
|
||||
return mock<Message<false>>();
|
||||
})
|
||||
}) as unknown as DMChannel,
|
||||
} as unknown as User,
|
||||
member: {
|
||||
manageable: true,
|
||||
timeout: timeoutFunc,
|
||||
} as unknown as GuildMember
|
||||
} as CommandInteractionOption;
|
||||
case 'length':
|
||||
return {
|
||||
value: '1m'
|
||||
} as CommandInteractionOption;
|
||||
case 'reason':
|
||||
return {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
expect(timeoutFunc).toBeCalled();
|
||||
expect(sentEmbeds.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// DM user
|
||||
test('GIVEN user can NOT be messaged, EXPECT resultEmbed to contain "DM Sent = false"', async () => {
|
||||
let embeds: APIEmbed[] | undefined;
|
||||
|
||||
const command = new Timeout();
|
||||
|
||||
const interactionReply = jest.fn((options: InteractionReplyOptions) => {
|
||||
embeds = options.embeds as APIEmbed[];
|
||||
});
|
||||
|
||||
let savedAudit: DeepPartial<Audit> | undefined;
|
||||
|
||||
const getSetting = jest.spyOn(SettingsHelper, 'GetSetting').mockResolvedValue('mod-logs');
|
||||
const auditSave = jest.spyOn(Audit.prototype, 'Save').mockImplementation((target: EntityTarget<BaseEntity>, entity: DeepPartial<BaseEntity>): Promise<void> => {
|
||||
savedAudit = entity;
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
const timeoutFunc = jest.fn();
|
||||
|
||||
let dmChannelSentEmbeds: (APIEmbed | JSONEncodable<APIEmbed>)[] | undefined;
|
||||
let logsChannelSentEmbeds: (APIEmbed | JSONEncodable<APIEmbed>)[] | undefined;
|
||||
|
||||
const dmChannel = {
|
||||
send: jest.fn().mockImplementation((options: MessageCreateOptions) => {
|
||||
dmChannelSentEmbeds = options.embeds;
|
||||
}),
|
||||
} as unknown as DMChannel;
|
||||
|
||||
const userInput = {
|
||||
user: {
|
||||
id: 'userId',
|
||||
tag: 'userTag',
|
||||
createDM: jest.fn().mockRejectedValue(undefined),
|
||||
} as unknown as User,
|
||||
member: {
|
||||
manageable: true,
|
||||
timeout: timeoutFunc,
|
||||
} as unknown as GuildMember,
|
||||
} as CommandInteractionOption;
|
||||
|
||||
const lengthInput = {
|
||||
value: '1s',
|
||||
} as CommandInteractionOption;
|
||||
|
||||
const reasonInput = {
|
||||
value: 'Test reason',
|
||||
} as CommandInteractionOption;
|
||||
|
||||
const logsChannel = {
|
||||
name: 'mod-logs',
|
||||
send: jest.fn().mockImplementation((options: MessageCreateOptions) => {
|
||||
logsChannelSentEmbeds = options.embeds;
|
||||
}),
|
||||
} as unknown as TextChannel;
|
||||
|
||||
const interaction = {
|
||||
guild: {
|
||||
channels: {
|
||||
cache: {
|
||||
find: jest.fn()
|
||||
.mockReturnValue(logsChannel),
|
||||
}
|
||||
},
|
||||
name: "Test Guild",
|
||||
} as unknown as Guild,
|
||||
guildId: 'guildId',
|
||||
reply: interactionReply,
|
||||
options: {
|
||||
get: jest.fn()
|
||||
.mockReturnValueOnce(userInput)
|
||||
.mockReturnValueOnce(lengthInput)
|
||||
.mockReturnValue(reasonInput),
|
||||
},
|
||||
user: {
|
||||
id: 'moderatorId'
|
||||
}
|
||||
} as unknown as CommandInteraction;
|
||||
|
||||
await command.execute(interaction);
|
||||
|
||||
// EXPECT embeds to be sent
|
||||
expect(embeds).toBeDefined();
|
||||
expect(embeds!.length).toBe(1);
|
||||
|
||||
const resultEmbed = embeds![0] as EmbedBuilder;
|
||||
|
||||
// EXPECT DM field to be configured
|
||||
const resultEmbedDMField = resultEmbed.data.fields![0];
|
||||
|
||||
expect(resultEmbedDMField.name).toBe("DM Sent");
|
||||
expect(resultEmbedDMField.value).toBe("false");
|
||||
});
|
||||
});
|
35
yarn.lock
35
yarn.lock
|
@ -124,7 +124,7 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.18.6"
|
||||
|
||||
"@babel/helper-string-parser@^7.21.5":
|
||||
"@babel/helper-string-parser@^7.19.4", "@babel/helper-string-parser@^7.21.5":
|
||||
version "7.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd"
|
||||
integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==
|
||||
|
@ -157,11 +157,16 @@
|
|||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8":
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8":
|
||||
version "7.21.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8"
|
||||
integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==
|
||||
|
||||
"@babel/parser@^7.20.7":
|
||||
version "7.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.3.tgz#1d285d67a19162ff9daa358d4cb41d50c06220b3"
|
||||
integrity sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==
|
||||
|
||||
"@babel/plugin-syntax-async-generators@^7.8.4":
|
||||
version "7.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
|
||||
|
@ -292,7 +297,7 @@
|
|||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3":
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3":
|
||||
version "7.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6"
|
||||
integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==
|
||||
|
@ -301,6 +306,15 @@
|
|||
"@babel/helper-validator-identifier" "^7.19.1"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0":
|
||||
version "7.21.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.3.tgz#4865a5357ce40f64e3400b0f3b737dc6d4f64d05"
|
||||
integrity sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.19.4"
|
||||
"@babel/helper-validator-identifier" "^7.19.1"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
|
@ -1023,9 +1037,9 @@ camelcase@^6.2.0:
|
|||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||
|
||||
caniuse-lite@^1.0.30001449:
|
||||
version "1.0.30001486"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz#56a08885228edf62cbe1ac8980f2b5dae159997e"
|
||||
integrity sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==
|
||||
version "1.0.30001469"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz#3dd505430c8522fdc9f94b4a19518e330f5c945a"
|
||||
integrity sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==
|
||||
|
||||
chalk@^2.0.0:
|
||||
version "2.4.2"
|
||||
|
@ -2506,7 +2520,7 @@ safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
|||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
semver@7.x, semver@^7.3.5:
|
||||
semver@7.x:
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0"
|
||||
integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==
|
||||
|
@ -2518,6 +2532,13 @@ semver@^6.0.0, semver@^6.3.0:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@^7.3.5:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
|
||||
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
sha.js@^2.4.11:
|
||||
version "2.4.11"
|
||||
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
|
||||
|
|
Loading…
Reference in a new issue