this is a rewrite of the bot in typescript. detritus is used as a discord library instead of discord.js
This commit is contained in:
spiral 2022-10-21 17:22:01 +00:00
parent 6688d4dcd8
commit 56091a6df7
No known key found for this signature in database
GPG key ID: 244A11E4B0BCF40E
33 changed files with 731 additions and 1116 deletions

View file

@ -1,6 +1,6 @@
FROM alpine:latest FROM alpine:latest
RUN apk add nodejs-current yarn RUN apk add nodejs-current npm yarn git
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock /app/ COPY package.json yarn.lock /app/
@ -8,4 +8,4 @@ RUN yarn
COPY . /app COPY . /app
CMD ["node", "bot.js"] CMD ["yarn", "ts-node", "bot.ts"]

45
bot.js
View file

@ -1,45 +0,0 @@
const Logger = require('@lilywonhalf/pretty-logger');
const { Client } = require('discord.js');
const Config = require('./config.json');
const Command = require('./model/command');
const fs = require('fs');
const dotenv = require('dotenv')
dotenv.config();
global.bot = new Client({ fetchAllMembers: true });
global.isRightGuild = (guildSnowflake) => guildSnowflake === Config.guild;
const crashRecover = (exception) => {
Logger.exception(exception);
Logger.notice('Need reboot');
};
process.on('uncaughtException', crashRecover);
bot.on('error', crashRecover);
Command.init();
const help = require("./command/help");
bot.ws.on('INTERACTION_CREATE', help.interactionHandler);
bot.on('ready', () => {
fs.readdirSync('./event/')
.filter(filename => filename.endsWith('.js'))
.map(filename => filename.substr(0, filename.length - 3))
.forEach(filename => {
const event = filename.replace(/([_-][a-z])/gu, character => `${character.substr(1).toUpperCase()}`);
if (filename !== 'ready') {
bot.on(event, require(`./event/${filename}`));
} else {
require(`./event/${filename}`)();
}
});
});
Logger.info('--------');
Logger.info('Logging in...');
bot.login(process.env.token);

48
bot.ts Normal file
View file

@ -0,0 +1,48 @@
import * as dotenv from 'dotenv';
dotenv.config();
import config from "./config";
import { Socket } from 'detritus-client-socket/lib/gateway';
import { Client } from "detritus-client-rest";
import * as types from 'discord-api-types/v10';
const socket = new Socket(process.env.token!, { intents: 33283 });
export const restClient = new Client(process.env.token);
import handleMessage from './messageHandler';
import help from './command/help';
socket.on('packet', async (evt: types.GatewayDispatchPayload) => handleEvtInner(evt).catch(e => console.error(e, JSON.stringify(e, null, 2))));
async function handleEvtInner(evt: types.GatewayDispatchPayload) {
if (evt.op != 0) return;
if (evt.t == 'READY') {
console.log("successfully logged in:", evt.d.user);
}
// i am quite sure this is the correct type
// @ts-expect-error
if (evt.t == 'INTERACTION_CREATE') await help.interactionHandler(evt.d);
if (evt.t == 'GUILD_MEMBER_ADD') {
if (evt.d.guild_id != config.guild) return;
if (evt.d.user!.bot) return;
const msg = `Welcome, <@${evt.d.user!.id}>! If you joined for any specific support questions `
+ `please check out <#863171642905591830> first to see if your issue is known, `
+ `and make sure that your app is up-to-date before posting.`;
await restClient.createMessage(config.channels.joins, { content: msg });
}
if (evt.t == 'MESSAGE_CREATE') {
if (evt.d.guild_id != config.guild) return;
if (evt.d.author.bot) return;
await handleMessage(evt.d).catch(console.error);
}
};
console.log('Logging in...');
socket.connect("https://gateway.discord.gg");

View file

@ -1,39 +0,0 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Discord = require('discord.js');
const CommandCategory = require('../model/command-category');
const Guild = require('../model/guild');
module.exports = {
aliases: ['av'],
category: CommandCategory.FUN,
isAllowedForContext: () => true,
description: 'Displays the avatar of the specified member.',
process: async (message, args) => {
let user = null;
if (args.length > 0) {
const result = Guild.findDesignatedMemberInMessage(message);
if (result.foundMembers.length > 0) {
if (result.foundMembers[0].user !== undefined) {
user = result.foundMembers[0].user;
} else {
user = result.foundMembers[0];
}
}
} else {
user = message.author;
}
if (user !== null) {
const url = user.displayAvatarURL({ dynamic: true });
message.channel.send(new Discord.MessageAttachment(
url + '?size=2048',
user.id + url.substr(url.lastIndexOf('.'))
)).catch(error => Logger.warning(error.toString()));
} else {
message.reply('I... Have no idea who that could be, sorry.');
}
}
}

25
command/avatar.ts Normal file
View file

@ -0,0 +1,25 @@
import { APIGuildMember, APIUser, GatewayMessageCreateDispatchData } from "discord-api-types/v10";
import { restClient } from "../bot";
import CommandCategory from "../model/command-category";
export default {
aliases: ['avatar', 'av'],
category: CommandCategory.FUN,
isAllowedForContext: (_: GatewayMessageCreateDispatchData) => true,
description: 'Displays the avatar of the specified member.',
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
let user: APIUser;
if (message.mentions.length == 0)
user = message.author;
else if (message.mentions.length > 1)
return restClient.createMessage(message.channel_id, `<@${message.author.id}>, there are too many mentions in this message! `
+`please pick only one user whose avatar you want to show.`);
else user = message.mentions[0];
const url = `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=2048`;
await restClient.createMessage(message.channel_id, { embed: { image: { url } } });
}
}

View file

@ -1,66 +0,0 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../config.json');
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
const { search } = require('../model/jira');
const MAX_CHARACTERS = 1950;
module.exports = {
aliases: ['change-log', 'cl'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberMod,
description: 'Builds the changelog for a given version',
process: async (message, args) => {
const errorHandler = async (error) => {
if (error) {
Logger.exception(error);
}
await message.reactions.removeAll();
await message.react('❌');
}
await message.react('⏳').catch(() => {});
const issues = args.length > 0 && args[0] ? await search(args[0]).catch(errorHandler) : await search().catch(errorHandler);
const taskType = Config.jira.issueTypes.task;
const bugType = Config.jira.issueTypes.bug;
const features = issues.filter(issue => parseInt(issue.fields.issuetype.id) === taskType);
const bugs = issues.filter(issue => parseInt(issue.fields.issuetype.id) === bugType);
if (features.length < 1 && bugs.length < 1) {
await message.channel.send('No issues found.');
await message.reactions.removeAll().catch(() => {});
return;
}
const output = `${features.map(
issue => `* Feature: ${issue.key} - ${issue.fields.summary}`
).join('\n')}\n\n${bugs.map(
issue => `* Fixed: ${issue.key} - ${issue.fields.summary}`
).join('\n')}`.trim();
const messages = [];
let currentMessage = '```';
for (let line of output.split('\n')) {
if (currentMessage.length + line.length >= MAX_CHARACTERS) {
messages.push(`${currentMessage}\`\`\``);
currentMessage = '```';
}
currentMessage = `${currentMessage}\n${line}`;
}
messages.push(`${currentMessage}\`\`\``);
for (let messageToSend of messages) {
await message.channel.send(messageToSend).catch(() => {});
}
await message.reactions.removeAll().catch(() => {});
await message.react('✔').catch(() => {});
}
}

67
command/changelog.ts Normal file
View file

@ -0,0 +1,67 @@
import { GatewayMessageCreateDispatchData } from 'discord-api-types/v10';
import CommandCategory from '../model/command-category';
import * as CommandPermission from '../model/command-permission';
import config from '../config';
import { restClient } from '../bot';
import search from '../model/jira';
const MAX_CHARACTERS = 1950;
export default {
aliases: ['changelog', 'change-log', 'cl'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberMod,
description: 'Builds the changelog for a given version',
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
const errorHandler = async (error: any) => {
await restClient.deleteReactions(message.channel_id, message.id);
await restClient.createReaction(message.channel_id, message.id, '❌');
throw error;
}
await restClient.createReaction(message.channel_id, message.id, '⏳').catch(() => {});
const issues = args.length > 0 && args[0] ? await search(args[0]).catch(errorHandler) : await search().catch(errorHandler);
const taskType = config.jira.issueTypes.task;
const bugType = config.jira.issueTypes.bug;
const features = issues?.filter(issue => parseInt(issue.fields.issuetype.id) === taskType) ?? [];
const bugs = issues?.filter(issue => parseInt(issue.fields.issuetype.id) === bugType) ?? [];
if (features.length < 1 && bugs.length < 1) {
await restClient.createMessage(message.channel_id, 'No issues found.');
await restClient.deleteReactions(message.channel_id, message.id);
return;
}
const output = `${features.map(
issue => `* Feature: ${issue.key} - ${issue.fields.summary}`
).join('\n')}\n\n${bugs.map(
issue => `* Fixed: ${issue.key} - ${issue.fields.summary}`
).join('\n')}`.trim();
const messages: string[] = [];
let currentMessage = '```';
for (let line of output.split('\n')) {
if (currentMessage.length + line.length >= MAX_CHARACTERS) {
messages.push(`${currentMessage}\`\`\``);
currentMessage = '```';
}
currentMessage = `${currentMessage}\n${line}`;
}
messages.push(`${currentMessage}\`\`\``);
for (let messageToSend of messages) {
await restClient.createMessage(message.channel_id, messageToSend);
}
await restClient.deleteReactions(message.channel_id, message.id).catch(() => {});
await restClient.createReaction(message.channel_id, message.id, '✔');
}
}

View file

@ -1,17 +0,0 @@
const Config = require('../config.json');
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
module.exports = {
aliases: ['clear', 'purge'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberMod,
description: 'Deletes messages in bulk.',
process: async (message, args) => {
if (args.length > 0 && parseInt(args[0]) > 0) {
await message.channel.bulkDelete(Math.min(parseInt(args[0]) + 1, 100));
} else {
message.reply(`You have to tell me how many messages I should clean. \`${Config.prefix}clean 10\` for example.`);
}
}
}

25
command/clean.ts Normal file
View file

@ -0,0 +1,25 @@
import { APIMessage, GatewayMessageCreateDispatchData } from 'discord-api-types/v10';
import CommandCategory from '../model/command-category';
import * as CommandPermission from '../model/command-permission';
import config from '../config';
import { restClient } from '../bot';
export default {
aliases: ['clean', 'clear', 'purge'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberMod,
description: 'Deletes messages in bulk.',
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
if (args.length > 0 && parseInt(args[0]) > 0) {
// sometimes abstractions are nice...
// await message.channel.bulkDelete(Math.min(parseInt(args[0]) + 1, 100));
const messages = await restClient.fetchMessages(message.channel_id, { limit: Math.min(parseInt(args[0]) + 1, 100) });
restClient.bulkDeleteMessages(message.channel_id, messages.map((x: APIMessage) => x.id));
} else {
restClient.createMessage(message.channel_id, `You have to tell me how many messages I should clean. \`${config.prefix}clean 10\` for example.`);
}
}
}

View file

@ -1,182 +0,0 @@
const { MessageEmbed, Message } = require('discord.js');
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
const RGB_REGEX = /(\d{1,3})\D*,\D*(\d{1,3})\D*,\D*(\d{1,3})\D*/u;
class EmbedDialog
{
/**
* @param {Message} message
*/
constructor(message) {
this.embed = new MessageEmbed();
this.message = message;
this.channel = message.channel;
this.destinationChannel = null;
this.prompt = (question, hideSkip = false) => {
return this.channel.send(`${question}\n*${!hideSkip ? '`skip` to skip, ' : ''}\`cancel\` to cancel*`);
};
this.isMessageSkip = message => message.content.toLowerCase() === 'skip';
this.messageFilter = testedMessage => {
const byAuthor = testedMessage.author.id === message.author.id;
const hasContent = testedMessage.cleanContent.trim().length > 0;
return byAuthor && hasContent;
}
this.channelMessageFilter = testedMessage => {
return this.messageFilter(testedMessage)
&& (this.isMessageSkip(testedMessage) || testedMessage.mentions.channels.size > 0);
}
this.returnFirstOfCollection = collection => collection && collection.size ? collection.first() : null;
this.awaitOptions = { max: 1, time: 5 * MINUTE, errors: ['time'] };
/**
* @param {function} filter
* @returns {Promise<Message>}
*/
this.awaitMessage = (filter = this.messageFilter) => {
return this.channel.awaitMessages(
filter,
this.awaitOptions
).then(async collection => {
let message = this.returnFirstOfCollection(collection);
if (message && message.content.toLowerCase() === 'cancel') {
message = null;
await this.channel.send('Cancelling embed creation.');
}
return message;
}).catch(async () => {
await this.channel.send('Time out, cancelling embed creation.');
});
};
}
async execute() {
let confirmation = true;
await this.prompt('#️⃣ In which **channel** would you like this embed to be posted?');
const channelMessage = await this.awaitMessage(this.channelMessageFilter);
if (!channelMessage) {
return null;
}
if (this.isMessageSkip(channelMessage)) {
this.destinationChannel = this.channel;
confirmation = false;
} else {
this.destinationChannel = channelMessage.mentions.channels.first();
}
await this.prompt('📰 What do you want the **title** of this embed to be (you can also ping someone so it appears as they are saying what is going to be the description)?');
const titleMessage = await this.awaitMessage(this.messageFilter);
if (!titleMessage) {
return null;
}
if (!this.isMessageSkip(titleMessage)) {
if (titleMessage.mentions.members.size > 0) {
const member = titleMessage.mentions.members.first();
this.embed.setAuthor(member.displayName, member.user.displayAvatarURL({dynamic: true}));
} else {
this.embed.setTitle(titleMessage.content);
}
}
await this.prompt('🎨 What do you want the **color** of this embed to be?');
const colourMessage = await this.awaitMessage(this.messageFilter);
if (!colourMessage) {
return null;
}
if (this.isMessageSkip(colourMessage)) {
this.embed.setColor(APP_MAIN_COLOUR);
} else {
if (colourMessage.content.startsWith('#')) {
this.embed.setColor(parseInt(colourMessage.content.substr(1), 16));
} else if (colourMessage.content.startsWith('0x')) {
this.embed.setColor(parseInt(colourMessage.content.substr(2), 16));
} else if (RGB_REGEX.test(colourMessage.content)) {
const [, red, green, blue] = colourMessage.content.match(RGB_REGEX);
this.embed.setColor([parseInt(red), parseInt(green), parseInt(blue)]);
} else {
this.embed.setColor(colourMessage.content.toUpperCase().replace(/[^A-Z]+/gu, '_'));
}
}
await this.prompt('💬 What do you want the **description** (contents) of this embed to be?', true);
const descriptionMessage = await this.awaitMessage(this.messageFilter);
if (!descriptionMessage) {
return null;
}
this.embed.setDescription(descriptionMessage.content);
await this.destinationChannel.send(this.embed);
if (confirmation) {
return this.channel.send(`✅ The embed has been posted in ${this.destinationChannel}.`);
}
}
}
class Embed
{
static instance = null;
constructor() {
if (Embed.instance !== null) {
return Embed.instance;
}
this.aliases = [];
this.category = CommandCategory.MODERATION;
this.isAllowedForContext = CommandPermission.isMemberModOrHelper;
this.description = 'Allows to post an embed';
}
/**
* @param {Message} message
* @param {Array} args
*/
async process(message, args) {
if (args.length > 0) {
let destinationChannel = message.channel;
let deleteMessage = true;
if (/<#\d+>/u.test(args[0])) {
destinationChannel = message.mentions.channels.first();
args.shift();
deleteMessage = false;
}
const embed = new MessageEmbed();
embed.setColor(APP_MAIN_COLOUR);
embed.setDescription(args.join(' '));
await destinationChannel.send(embed);
if (deleteMessage) {
await message.delete();
} else {
await message.react('✅');
}
} else {
const dialog = new EmbedDialog(message);
return dialog.execute();
}
}
}
module.exports = new Embed();

View file

@ -1,40 +0,0 @@
const Discord = require('discord.js');
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../config.json');
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
// Including every model here so that it's ready to be used by the command
const Guild = require('../model/guild');
const JAVASCRIPT_LOGO_URL = 'https://i.discord.fr/IEV8.png';
module.exports = {
aliases: [],
category: CommandCategory.BOT_MANAGEMENT,
isAllowedForContext: CommandPermission.isMommy,
process: async (message) => {
const code = message.content
.substr(Config.prefix.length + 'eval'.length)
.trim()
.replace(/(`{3})js\n(.+)\n\1/iu, '$2')
.trim();
await message.react('✔');
Logger.notice('Eval: ' + code);
let output = null;
try {
output = eval(`${code}`); // Spoopy! 🎃 🦇 👻 ☠ 🕷
} catch (exception) {
output = `**${exception.name}: ${exception.message}**\n${exception.stack}`;
}
const embed = new Discord.MessageEmbed()
.setAuthor('Eval', JAVASCRIPT_LOGO_URL)
.setColor(0x00FF00)
.setDescription(output);
message.channel.send(embed).catch(error => Logger.warning(error.toString()));
}
};

40
command/eval.ts Normal file
View file

@ -0,0 +1,40 @@
import CommandCategory from "../model/command-category";
import * as CommandPermission from '../model/command-permission';
import config from "../config";
import { APIMessage } from "discord-api-types/v10";
import { restClient } from "../bot";
const JAVASCRIPT_LOGO_URL = 'https://i.discord.fr/IEV8.png';
export default {
aliases: ['eval'],
category: CommandCategory.BOT_MANAGEMENT,
isAllowedForContext: CommandPermission.isMommy,
process: async (message: APIMessage) => {
const code = message.content
.substr(config.prefix.length + 'eval'.length)
.trim()
.replace(/(`{3})js\n(.+)\n\1/iu, '$2')
.trim();
console.log('Eval: ' + code);
let output: string | null = null;
try {
output = eval(`${code}`); // Spoopy! 🎃 🦇 👻 ☠ 🕷
} catch (exception) {
// @ts-expect-error
output = `**${exception.name}: ${exception.message}**\n${exception.stack}`;
}
restClient.createMessage(message.channel_id, { embed: {
author: {
name: 'Eval',
iconUrl: JAVASCRIPT_LOGO_URL,
},
color: 0x00FF00,
description: output!,
}}).catch(error => console.warn(error.toString()));
}
};

View file

@ -1,132 +0,0 @@
const CommandPermission = require('../model/command-permission');
const CommandCategory = require('../model/command-category');
const commands = require('../model/command').commandList;
const messages = require('../model/messages');
const cachelessRequire = (path) => {
if (!typeof path === 'string') {
delete require.cache[require.resolve(path)];
}
return typeof path === 'string' ? require(path) : null;
};
const cleanString = (str) => str === null || str === undefined ? null : str
.replace('_', ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
const mainPage = (id) => {
const buttons = Object.keys(CommandCategory).map(c => ({
type: 2,
style: 1,
label: cleanString(c),
custom_id: `help-${id}-${c}`,
}))
return {
embeds: [{
title: 'Help command',
color: 0xA95B44,
description:
'Spot stands for **S**imply **P**lural b**ot**. This bot is mainly used to generate changelogs and help ' +
'with the definition of features, so recurring questions can be answered more quickly.\n' +
'\n' +
'Commands are classified by categories because displaying every command in this small box would be ' +
'confusing and would break Discord\'s character limit. Click on a buttom below to show the commands ' +
'available in the corresponding category, or type `.help <category name>`.\n'
}],
components: [{
'type': 1,
'components': buttons,
}]
}
}
const categoryPage = (cat, id, direct = false) => {
const c = Object.values(CommandCategory).map(x => cleanString(x).toLowerCase());
if (!c.includes(cat.toLowerCase()))
return;
let data = {
embeds: [{
title: `Help command | ${cleanString(cat)}`,
color: 0xA95B44,
}],
components: [{
'type': 1,
'components': [{
type: 2,
style: 2,
label: 'Back',
custom_id: `help-${id}-home`,
}],
}]
};
if (direct) delete data.components;
if (cat.toLowerCase() === cleanString(CommandCategory.RESOURCE).toLowerCase())
data.embeds[0].fields = messages.getList();
else
data.embeds[0].fields = Array.from(commands.keys()).map(x => {
const cmd = cachelessRequire(commands.get(x));
if (cleanString(cmd.category)?.toLowerCase() !== cat.toLowerCase()) return;
return { name: x, value: cmd.description ?? 'No description.'};
}).filter(x => x);
return data;
}
module.exports = {
aliases: ["commands"],
category: CommandCategory.INFO,
isAllowedForContext: () => true,
description: 'Provides the list of commands.',
process: async (message, args) => {
if (!args[0])
await bot.api.channels(message.channel.id).messages.post({ data: mainPage(message.author.id) });
else
await bot.api.channels(message.channel.id).messages.post({
data:
categoryPage(args.join(' '), message.author.id, true)
?? {
content: 'Category not found.',
message_reference: { message_id: message.id, guild_id: message.guild?.id },
allowed_mentions: { parse: [] },
}
})
}
}
module.exports.interactionHandler = async (event) => {
const user = event.member ? event.member.user.id : event.user.id;
const customId = event.data.custom_id;
let ret;
if (!customId.startsWith(`help-${user}`)) {
ret = {
type: 4,
data: {
flags: 64,
content: 'This help command was sent by someone else. Please run `.help` again.',
},
}
} else if (customId.endsWith('home')) {
ret = { type: 7, data: mainPage(user) };
} else {
let page = categoryPage(cleanString(customId.split(`help-${user}-`).join('')), user);
if (page) {
ret = { type: 7, data: page };
} else {
ret = { type: 4, data: { flags: 64, content: 'Category not found.' }}
}
}
return await bot.api.interactions(event.id, event.token).callback.post({ data: ret })
}

125
command/help.ts Normal file
View file

@ -0,0 +1,125 @@
import { restClient } from "../bot";
import CommandCategory from "../model/command-category";
import * as CommandPermission from '../model/command-permission';
import * as types from 'discord-api-types/v10';
import { Command, commands } from "../messageHandler";
import * as messages from "../model/messages";
import { GatewayMessageCreateDispatchData } from "discord-api-types/v10";
const cleanString = (str: string | null | undefined) => (str === null || str === undefined) ? undefined : str
.replace('_', ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
const mainPage = (id: string) => {
const buttons = Object.values(CommandCategory).filter(x => typeof x === 'string').map(c => ({
type: 2,
style: 1,
label: cleanString(c),
customId: `help-${id}-${c}`,
}))
return {
embeds: [{
title: 'Help command',
color: 0xA95B44,
description:
'Spot stands for **S**imply **P**lural b**ot**. This bot is mainly used to generate changelogs and help ' +
'with the definition of features, so recurring questions can be answered more quickly.\n' +
'\n' +
'Commands are classified by categories because displaying every command in this small box would be ' +
'confusing and would break Discord\'s character limit. Click on a buttom below to show the commands ' +
'available in the corresponding category, or type `.help <category name>`.\n'
}],
components: [{
'type': 1,
'components': buttons,
}]
}
}
const categoryPage = (cat: string, id: string, direct = false) => {
const c = Object.values(CommandCategory).filter(x => typeof x === 'string').map(x => cleanString(x)?.toLowerCase());
if (!c.includes(cat.toLowerCase()))
return;
let data = {
embeds: [{
title: `Help command | ${cleanString(cat)}`,
color: 0xA95B44,
}],
components: [{
'type': 1,
'components': [{
type: 2,
style: 2,
label: 'Back',
customId: `help-${id}-home`,
}],
}],
};
// @ts-expect-error
if (direct) delete data.components;
if (cat == CommandCategory.RESOURCE)
// @ts-expect-error
data.embeds[0].fields = messages.getList();
else
// @ts-expect-error
data.embeds[0].fields = commands.map((cmd: Command) => {
if (cmd.category !== cat) return;
return { name: cmd.aliases[0], value: cmd.description ?? 'No description.'};
}).filter(x => x);
return data;
}
export default {
aliases: ['help', "commands", 'h'],
category: CommandCategory.INFO,
isAllowedForContext: (_: GatewayMessageCreateDispatchData) => true,
description: 'Provides the list of commands.',
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
if (!args[0])
await restClient.createMessage(message.channel_id, mainPage(message.author.id) );
else
await restClient.createMessage(message.channel_id, categoryPage(args.join(' '), message.author.id, true)
?? {
content: 'Category not found.',
messageReference: { messageId: message.id, guildId: message.guild_id },
allowedMentions: { parse: [] },
})
},
interactionHandler: async (event: types.APIMessageComponentInteraction) => {
const user = event.member ? event.member.user.id : event.user!.id;
const customId = event.data.custom_id;
let ret;
if (!customId.startsWith(`help-${user}`)) {
ret = {
type: 4,
data: {
flags: 64,
content: 'This help command was sent by someone else. Please run `.help` again.',
},
}
} else if (customId.endsWith('home')) {
ret = { type: 7, data: mainPage(user) };
} else {
let page = categoryPage(cleanString(customId.split(`help-${user}-`).join(''))!, user);
if (page) {
ret = { type: 7, data: page };
} else {
ret = { type: 4, data: { flags: 64, content: 'Category not found.' }}
}
}
return await restClient.createInteractionResponse(event.id, event.token, ret);
}
};

View file

@ -1,36 +1,38 @@
const CommandCategory = require('../model/command-category'); import CommandCategory from "../model/command-category";
const CommandPermission = require('../model/command-permission'); import * as CommandPermission from '../model/command-permission';
const axios = require('axios'); import axios from "axios";
import { APIMessage, GatewayMessageCreateDispatchData } from "discord-api-types/v10";
import { restClient } from "../bot";
module.exports = { export default {
aliases: [], aliases: ['ticket'],
category: CommandCategory.MODERATION, category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberModOrHelper, isAllowedForContext: CommandPermission.isMemberModOrHelper,
process: async (message) => { process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
if (message.reference == null) if (message.message_reference == null)
return message.reply("missing reply"); return restClient.createMessage(message.channel_id, `<@${message.author.id}>, missing reply`);
let reply = await message.channel.messages.fetch(message.reference.messageID); let reply: APIMessage = await restClient.fetchMessage(message.channel_id, message.message_reference.message_id!);
let user = null; let user = null;
if (reply.webhookID != null) { if (reply.webhook_id != null) {
let pkmsg = await axios(`https://api.pluralkit.me/v2/messages/${reply.id}`); let pkmsg = await axios(`https://api.pluralkit.me/v2/messages/${reply.id}`);
user = pkmsg.data.sender; user = pkmsg.data.sender;
} else { } else {
user = reply.author.id; user = reply.author.id;
} }
message.delete(); restClient.deleteMessage(message.channel_id, message.id).catch(() => {});
message.client.api.channels(message.channel.id).messages.post({ data: { restClient.createMessage(message.channel_id, {
content: `<@${user}>: Please send us a support ticket for your specific issue using this link: <https://apparyllis.atlassian.net/servicedesk/customer/portal/3>\n` content: `<@${user}>: Please send us a support ticket for your specific issue using this link: <https://apparyllis.atlassian.net/servicedesk/customer/portal/3>\n`
+ `You can also click the button below this message.\n\n` + `You can also click the button below this message.\n\n`
+ `Select the support type that best fits your issue. Make sure to fill out any information that is requested.\n\n` + `Select the support type that best fits your issue. Make sure to fill out any information that is requested.\n\n`
+ `Once you fill out the ticket, you will receive an email with a confirmation.\nWhen the developer answers the ticket, you will get another email, ` + `Once you fill out the ticket, you will receive an email with a confirmation.\nWhen the developer answers the ticket, you will get another email, `
+ `so please watch your email (and spam folder) for a reply.`, + `so please watch your email (and spam folder) for a reply.`,
message_reference: { message_id: reply.id }, messageReference: { messageId: reply.id },
components: [{ type: 1, components: [{ type: 2, style: 5, label: "Submit ticket", url: "https://apparyllis.atlassian.net/servicedesk/customer/portals" }]}] components: [{ type: 1, components: [{ type: 2, style: 5, label: "Submit ticket", url: "https://apparyllis.atlassian.net/servicedesk/customer/portals" }]}]
}}); });
} }
}; };

View file

@ -1,4 +1,4 @@
{ export default {
"prefix": ".", "prefix": ".",
"guild": "793906174356619286", "guild": "793906174356619286",
"mom": "840806601957965864", "mom": "840806601957965864",

View file

@ -1,7 +0,0 @@
const Guild = require('../model/guild');
module.exports = async (member) => {
if ((member.guild === null || isRightGuild(member.guild.id)) && !member.user.bot) {
Guild.guildMemberAddHandler(member);
}
};

View file

@ -1,12 +0,0 @@
const Command = require('../model/command');
/**
* @param {Message} message
*/
module.exports = async (message) => {
const user = message.author;
if ((message.guild === null || isRightGuild(message.guild.id)) && !user.bot) {
await Command.parseMessage(message);
}
};

View file

@ -1,19 +0,0 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../config.json');
const Guild = require('../model/guild');
module.exports = async () => {
Logger.info('Logged in as ' + bot.user.username + '#' + bot.user.discriminator);
Logger.info('--------');
Logger.info('Syncing guilds...');
await Guild.init();
Logger.info('Guilds synced. Serving in ' + Guild.discordGuild.name);
Logger.info('--------');
if (process.argv[3] === '--reboot') {
bot.users.cache.get(Config.mom).send(`I'm back :) !`);
}
};

46
messageHandler.ts Normal file
View file

@ -0,0 +1,46 @@
import * as types from 'discord-api-types/v10';
import CommandCategory from './model/command-category';
import { restClient } from './bot';
import config from './config';
import avatar from './command/avatar';
import changelog from './command/changelog';
import clean from './command/clean';
import eval from './command/eval';
import help from './command/help';
import ticket from './command/ticket';
import * as customMessages from './model/messages';
export interface Command {
aliases: string[];
category: CommandCategory;
isAllowedForContext(_: types.GatewayMessageCreateDispatchData): boolean;
description?: string;
process(_: types.GatewayMessageCreateDispatchData, __: string[]): Promise<void>;
}
const privCommands: Record<string, Command> = {};
export const commands = [avatar, changelog, clean, eval, help, ticket];
commands.map((x: Command) => {
x.aliases.map(a => privCommands[a] = x);
});
export default async function handleMessage(message: types.GatewayMessageCreateDispatchData) {
if (!message.content.toLowerCase().startsWith(config.prefix))
return false;
let args = message.content.substr(config.prefix.length).trim().split(' ');
const calledCommand = args.shift()!.toLowerCase();
const embed = customMessages.getEmbed(calledCommand);
if (embed != null)
return await restClient.createMessage(message.channel_id, { embed });
const foundCommand = privCommands[calledCommand];
if (foundCommand == null || !foundCommand.isAllowedForContext(message)) return;
await foundCommand.process(message, args);
}

View file

@ -1,6 +1,5 @@
{ {
"categories": "categories": {
{
"guides": { "guides": {
"name": "Guides", "name": "Guides",
"desc": "Guides on how to do things in the app", "desc": "Guides on how to do things in the app",

View file

@ -1,8 +0,0 @@
const CommandCategory = {
MODERATION: 'moderation',
BOT_MANAGEMENT: 'bot_management',
FUN: 'fun',
RESOURCE: 'resource'
};
module.exports = CommandCategory;

View file

@ -0,0 +1,9 @@
enum CommandCategory {
INFO = 'Info',
MODERATION = 'Moderation',
BOT_MANAGEMENT = 'Bot Managenent',
FUN = 'Fun',
RESOURCE = 'Resource'
}
export default CommandCategory;

View file

@ -1,36 +0,0 @@
const Config = require('../config.json');
const Guild = require('./guild');
const CommandPermission = {
/**
* @param {Message} message
* @returns {Promise.<boolean>}
*/
isMommy: async (message) => {
const member = await Guild.getMemberFromMessage(message);
return member.id === Config.mom;
},
/**
* @param {Message} message
* @returns {Promise.<boolean>}
*/
isMemberMod: async (message) => {
const member = await Guild.getMemberFromMessage(message);
return member.id === Config.mom || await Guild.isMemberMod(member);
},
/**
* @param {Message} message
* @returns {Promise.<boolean>}
*/
isMemberModOrHelper: async (message) => {
const member = await Guild.getMemberFromMessage(message);
return await CommandPermission.isMemberMod(message) || await Guild.isMemberHelper(member);
},
};
module.exports = CommandPermission;

View file

@ -0,0 +1,9 @@
import { GatewayMessageCreateDispatchData } from 'discord-api-types/v10';
import config from '../config';
export const isMommy = (message: GatewayMessageCreateDispatchData) => message.author.id == config.mom;
export const isMemberMod = (message: GatewayMessageCreateDispatchData) => message.member!.roles.includes(config.roles.mod);
export const isMemberModOrHelper = (message: GatewayMessageCreateDispatchData) =>
message.member!.roles.includes(config.roles.mod)
|| message.member!.roles.includes(config.roles.helper);

View file

@ -1,105 +0,0 @@
const fs = require('fs');
const Discord = require('discord.js');
const Config = require('../config.json');
const Guild = require('./guild');
const customMessages = require("./messages");
const cachelessRequire = (path) => {
if (typeof path === 'string') {
delete require.cache[require.resolve(path)];
}
return typeof path === 'string' ? require(path) : null;
};
const Command = {
commandList: new Discord.Collection(),
commandAliases: {},
init: () => {
Command.commandList = new Discord.Collection();
Command.commandAliases = {};
fs.readdirSync('command/').forEach(file => {
if (file.substr(file.lastIndexOf('.')).toLowerCase() === '.js') {
const commandPath = `../command/${file}`;
const commandInstance = cachelessRequire(commandPath);
if (commandInstance !== null) {
const commandName = file.substr(0, file.lastIndexOf('.')).toLowerCase();
Command.commandList.set(commandName, commandPath);
if (commandInstance.aliases !== undefined && commandInstance.aliases !== null) {
commandInstance.aliases.forEach(alias => {
Command.commandAliases[alias.toLowerCase()] = commandName;
});
}
}
}
});
},
/**
* @param {Message} message
* @returns {boolean}
*/
parseMessage: async (message) => {
if (!message.content.toLowerCase().startsWith(Config.prefix))
return false;
let content = message.content.substr(Config.prefix.length).trim().split(' ');
const calledCommand = content.shift().toLowerCase();
if (embed = customMessages.getEmbed(calledCommand))
return await message.channel.send({embed});
if (!await Command.isValid(calledCommand, message)) return;
const member = await Guild.getMemberFromMessage(message);
if (member === null)
return message.reply('sorry, you do not seem to be on the server.');
let commandName = calledCommand;
if (Command.commandAliases.hasOwnProperty(calledCommand))
commandName = Command.commandAliases[calledCommand];
const commandInstance = cachelessRequire(Command.commandList.get(commandName));
if (commandInstance !== null)
commandInstance.process(message, content, Command);
else
Command.commandList.delete(commandName);
},
/**
* @param {string} command
* @param {Message} message
* @return {Promise.<boolean>}
*/
isValid: async (command, message) => {
let canonicalCommand = command.toLowerCase();
let valid = Command.commandList.has(canonicalCommand);
if (!valid && Command.commandAliases.hasOwnProperty(canonicalCommand)) {
canonicalCommand = Command.commandAliases[command];
valid = Command.commandList.has(canonicalCommand);
}
const commandInstance = cachelessRequire(Command.commandList.get(canonicalCommand));
if (commandInstance === null) {
Command.commandList.delete(canonicalCommand);
}
valid = valid
&& commandInstance !== null
&& await commandInstance.isAllowedForContext(message);
return valid;
}
};
module.exports = Command;

View file

@ -1,173 +0,0 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../config.json');
const Discord = require('discord.js');
const cachelessRequire = (path) => {
if (typeof path === 'string') {
delete require.cache[require.resolve(path)];
}
return typeof path === 'string' ? require(path) : null;
};
const Guild = {
/** {Guild} */
discordGuild: null,
/** {TextChannel} */
joinsChannel: null,
/** {Collection} */
messagesCache: new Discord.Collection(),
init: async () => {
Guild.discordGuild = bot.guilds.cache.find(guild => guild.id === Config.guild);
Guild.joinsChannel = Guild.discordGuild.channels.cache.find(channel => channel.id === Config.channels.joins);
},
/**
* @param message
* @returns {Promise.<GuildMember|null>}
*/
getMemberFromMessage: async (message) => {
return await Guild.discordGuild.members.fetch(message.author).catch(exception => {
Logger.error(exception.toString());
return null;
});
},
/**
* @param {GuildMember} member
*/
isMemberMod: (member) => {
return member !== undefined && member !== null && member.roles.cache.has(Config.roles.mod);
},
/**
* @param {GuildMember} member
*/
isMemberHelper: (member) => {
return member !== undefined && member !== null && member.roles.cache.has(Config.roles.helper);
},
/**
* @param {string} roleName
* @returns {Role|null}
*/
getRoleByName: (roleName) => {
return roleName === undefined || roleName === null ? null : Guild.discordGuild.roles.cache.find(
role => role.name.toLowerCase() === roleName.toLowerCase()
);
},
/**
* @param {GuildMember} member
* @param {Snowflake} snowflake - The Role snowflake.
* @returns {boolean}
*/
memberHasRole: (member, snowflake) => {
return member !== undefined && member !== null && member.roles.cache.some(role => role.id === snowflake);
},
/**
* @param {Message} message
* @returns {Discord.MessageEmbed}
*/
messageToEmbed: async (message) => {
const member = await Guild.getMemberFromMessage(message);
const suffix = member !== null && member.nickname !== null && member.nickname !== undefined ? ` aka ${member.nickname}` : '';
const embeds = message.embeds.filter(embed => embed.author.name && embed.author.iconURL);
let authorName = `${message.author.username}#${message.author.discriminator}${suffix}`;
let authorImage = message.author.displayAvatarURL({ dynamic: true });
let description = message.content;
let timestamp = message.createdTimestamp;
let image = null;
if (message.attachments.size > 0) {
image = message.attachments.first().url;
}
if (description.length < 1 && embeds.length > 0) {
const embed = embeds[0];
description = embed.description ? embed.description.trim() : '';
if (message.author.bot) {
if (embed.author) {
authorName = embed.author.name;
authorImage = embed.author.iconURL;
}
if (embed.timestamp) {
timestamp = embed.timestamp;
}
if (embed.image) {
image = embed.image.url;
}
}
}
return new Discord.MessageEmbed()
.setAuthor(authorName, authorImage)
.setColor(0x00FF00)
.setDescription(description)
.setTimestamp(timestamp)
.setImage(image);
},
/**
* @param {Message} message
* @returns {{certain: boolean, foundMembers: Array}}
*/
findDesignatedMemberInMessage: (message) => {
let foundMembers = [];
let certain = true;
const memberList = bot.users.cache.concat(Guild.discordGuild.members.cache);
if (message.mentions.members !== null && message.mentions.members.size > 0) {
foundMembers = message.mentions.members.array();
} else if (message.content.match(/[0-9]{18}/u) !== null) {
const ids = message.content.match(/[0-9]{18}/gu);
ids.map(id => {
if (memberList.has(id)) {
foundMembers.push(memberList.get(id));
}
});
} else {
certain = false;
memberList.forEach(member => {
const user = member.user === undefined ? member : member.user;
const hasNickname = member.nickname !== undefined && member.nickname !== null;
const nickname = hasNickname ? `${member.nickname.toLowerCase()}#${user.discriminator}` : '';
const username = `${user.username.toLowerCase()}#${user.discriminator}`;
const content = message.cleanContent.toLowerCase().split(' ').splice(1).join(' ');
if (content.length > 0) {
const contentInNickname = hasNickname ? nickname.indexOf(content) > -1 : false;
const contentInUsername = username.indexOf(content) > -1;
const nicknameInContent = hasNickname ? content.indexOf(nickname) > -1 : false;
const usernameInContent = content.indexOf(username) > -1;
if (contentInNickname || contentInUsername || nicknameInContent || usernameInContent) {
foundMembers.push(member);
}
}
});
}
return {
certain,
foundMembers
};
},
guildMemberAddHandler: (member) => {
Guild.joinsChannel.send(`Welcome, ${member}! If you joined for any specific support questions please check out <#863171642905591830> first to see if your issue is known, and make sure that your app is up-to-date before posting.`);
}
};
module.exports = Guild;

View file

@ -1,37 +0,0 @@
const axios = require('axios');
const Config = require('../config.json');
const ENDPOINTS = {
search: '/search'
};
class Jira
{
async search(version) {
const params = {
'jql': 'project%20%3D%20SP%20AND%20status%20in%20(%22Claimed%20Fixed%22%2C%20%22In%20Pre-Testing%22)%20AND%20updated%20%3E%3D%20-24h%20ORDER%20BY%20created%20DESC',
'maxResults': 100,
'startAt': 0
};
if (version) {
params.jql = `project%20%3D%20SP%20AND%20status%20in%20(%22Claimed%20Fixed%22%2C%20%22In%20Pre-Testing%22)%20AND%20%22Fixed%20In%20Version%5BNumber%5D%22%20%3D%20%22${version}%22%20ORDER%20BY%20created%20DESC`;
}
const issues = [];
let response = null;
do {
const httpParams = Object.keys(params).map(key => `${key}=${params[key]}`).join('&');
const url = `${Config.jira.baseUrl}${ENDPOINTS.search}?${httpParams}`;
response = await axios(url);
issues.push(...response.data.issues);
params.startAt += 100;
} while (response.data.issues.length > 99 && response.data.total > 100);
return issues;
}
}
module.exports = new Jira();

30
model/jira.ts Normal file
View file

@ -0,0 +1,30 @@
import * as axios from 'axios';
import config from '../config';
export default async function search(version?: string): Promise<any[]> {
const params = {
'jql': 'project%20%3D%20SP%20AND%20status%20in%20(%22Claimed%20Fixed%22%2C%20%22In%20Pre-Testing%22)%20AND%20updated%20%3E%3D%20-24h%20ORDER%20BY%20created%20DESC',
'maxResults': 100,
'startAt': 0
};
if (version) {
params.jql = `project%20%3D%20SP%20AND%20status%20in%20(%22Claimed%20Fixed%22%2C%20%22In%20Pre-Testing%22)%20AND%20%22Fixed%20In%20Version%5BNumber%5D%22%20%3D%20%22${version}%22%20ORDER%20BY%20created%20DESC`;
}
const issues: any[] = [];
let response: any | null = null;
do {
// @ts-expect-error
const httpParams = Object.keys(params).map(key => `${key}=${params[key]}`).join('&');
const url = `${config.jira.baseUrl}/search?${httpParams}`;
// what even is this
response = JSON.parse((await new axios.Axios({}).get(url)).data);
issues.push(...response!.issues);
params.startAt += 100;
} while (response!.issues.length > 99 && response!.total > 100);
return issues;
}

View file

@ -1,65 +0,0 @@
const fs = require("fs")
const path = require('path');
const messages = {};
const load = () =>
{
const messagesFile = fs.readFileSync(path.join(__dirname, '..', 'messages.json'))
const messagesJson = JSON.parse(messagesFile);
const messagesList = messagesJson["faq"];
messagesList.forEach((msg) => {
messages[msg["title"]] = msg
})
}
load();
// interface Message {
// names string[];
// description string;
// title string;
// text string;
// friendlyName string;
// category string;
// }
module.exports = {
get names() {
return names;
},
get messages() {
return messages;
},
get: (name) => messages[name],
getList: () => Object.keys(messages).map(msg => ({ name: messages[msg].names[0], value: messages[msg].description })),
getEmbed: (name) => {
let foundMsg = null
Object.keys(messages).forEach((msgKey) =>
{
if (foundMsg) return;
const msg = messages[msgKey]
if (msg.names.includes(name))
{
foundMsg = msg;
return;
}
});
if (!foundMsg) return;
return {
color: "A95B44",
author: {
name: foundMsg.title,
iconURL: bot.user.displayAvatarURL({ dynamic: true }),
},
description: foundMsg.text,
}
}
}

58
model/messages.ts Normal file
View file

@ -0,0 +1,58 @@
import { restClient } from "../bot";
interface Message {
names: string[];
description: string;
title: string;
text: string;
friendlyName: string;
category: string;
}
export const messages: Record<string, Message> = {};
let avatarUrl = "";
const load = async () =>
{
const messagesList = require('../messages.json')["faq"];
messagesList.forEach((msg: any) => {
messages[msg["title"]] = msg
})
const userInfo = await restClient.fetchMe();
avatarUrl = `https://cdn.discordapp.com/avatars/${userInfo.id}/${userInfo.avatar}.png`;
console.log(avatarUrl);
}
load();
export const get = (name: string) => messages[name];
export const getList = () => Object.keys(messages).map(msg => ({ name: messages[msg].names[0], value: messages[msg].description }));
export const getEmbed = (name: string) => {
let foundMsg: Message | undefined;
Object.keys(messages).forEach((msgKey) =>
{
if (foundMsg) return;
const msg = messages[msgKey]
if (msg.names.includes(name))
{
foundMsg = msg;
return;
}
});
if (foundMsg == null) return;
return {
color: 0xA95B44,
author: {
name: foundMsg.title,
iconUrl: avatarUrl,
},
description: foundMsg.text,
}
}

View file

@ -9,9 +9,12 @@
"author": "Lily Wonhalf <lilywonhalf@gmail.com>", "author": "Lily Wonhalf <lilywonhalf@gmail.com>",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@lilywonhalf/pretty-logger": "^1.1.3",
"axios": "^1.1.3", "axios": "^1.1.3",
"discord.js": "^14.6.0", "detritus-client-rest": "git+https://github.com/Tupperbox/DiscordRest",
"dotenv": "^16.0.3" "detritus-client-socket": "git+https://github.com/Tupperbox/DiscordSocket",
"discord-api-types": "^0.37.14",
"dotenv": "^16.0.3",
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
} }
} }

334
yarn.lock
View file

@ -2,138 +2,250 @@
# yarn lockfile v1 # yarn lockfile v1
"@discordjs/collection@^0.1.6": "@cspotcode/source-map-support@^0.8.0":
"integrity" "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" version "0.8.1"
"resolved" "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
"version" "0.1.6" integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
"@discordjs/form-data@^3.0.1":
"integrity" "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg=="
"resolved" "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz"
"version" "3.0.1"
dependencies: dependencies:
"asynckit" "^0.4.0" "@jridgewell/trace-mapping" "0.3.9"
"combined-stream" "^1.0.8"
"mime-types" "^2.1.12"
"@lilywonhalf/pretty-logger@^1.1.3": "@jridgewell/resolve-uri@^3.0.3":
"integrity" "sha512-DcEGjCrQV2j6Hl29G4zD5M/84hzrt1xw7uPOZtZLWFzrCB5OWS/64RWl/y5/om0walTIYf7Nv00hCzfRS8gw4A==" version "3.1.0"
"resolved" "https://registry.npmjs.org/@lilywonhalf/pretty-logger/-/pretty-logger-1.1.3.tgz" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
"version" "1.1.3" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
"abort-controller@^3.0.0": "@jridgewell/sourcemap-codec@^1.4.10":
"integrity" "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==" version "1.4.14"
"resolved" "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
"version" "3.0.0" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies: dependencies:
"event-target-shim" "^5.0.0" "@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"asynckit@^0.4.0": "@tsconfig/node10@^1.0.7":
"integrity" "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" version "1.0.9"
"resolved" "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
"version" "0.4.0" integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
"axios@^0.21.1": "@tsconfig/node12@^1.0.7":
"integrity" "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==" version "1.0.11"
"resolved" "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
"version" "0.21.4" integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
"@tsconfig/node14@^1.0.0":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
"@types/node@^14.17.1":
version "14.18.32"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.32.tgz#8074f7106731f1a12ba993fe8bad86ee73905014"
integrity sha512-Y6S38pFr04yb13qqHf8uk1nHE3lXgQ30WZbv1mLliV9pt0NjvqdWttLcrOYLnXbOafknVYRHZGoMSpR9UwfYow==
acorn-walk@^8.1.1:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^8.4.1:
version "8.8.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
integrity "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
axios@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35"
integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==
dependencies: dependencies:
"follow-redirects" "^1.14.0" follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
"combined-stream@^1.0.8": combined-stream@^1.0.8:
"integrity" "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==" version "1.0.8"
"resolved" "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
"version" "1.0.8" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies: dependencies:
"delayed-stream" "~1.0.0" delayed-stream "~1.0.0"
"delayed-stream@~1.0.0": create-require@^1.1.0:
"integrity" "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" version "1.1.1"
"resolved" "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
"version" "1.0.0" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
"discord.js@^12.2.0": delayed-stream@~1.0.0:
"integrity" "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==" version "1.0.0"
"resolved" "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
"version" "12.5.3" integrity "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
"detritus-client-rest@git+https://github.com/Tupperbox/DiscordRest":
version "0.10.5"
resolved "git+https://github.com/Tupperbox/DiscordRest#15c7aa2d1782c55c30a60b53f754b6938e658346"
dependencies: dependencies:
"@discordjs/collection" "^0.1.6" detritus-rest "^0.7.0"
"@discordjs/form-data" "^3.0.1" detritus-utils "^0.4.0"
"abort-controller" "^3.0.0"
"node-fetch" "^2.6.1"
"prism-media" "^1.2.9"
"setimmediate" "^1.0.5"
"tweetnacl" "^1.0.3"
"ws" "^7.4.4"
"dotenv@^16.0.3": "detritus-client-socket@git+https://github.com/Tupperbox/DiscordSocket":
"integrity" "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" version "0.8.3"
"resolved" "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz" resolved "git+https://github.com/Tupperbox/DiscordSocket#44c6815e2e0cb0c6fdf42bc77ff91127413b5c58"
"version" "16.0.3"
"event-target-shim@^5.0.0":
"integrity" "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
"resolved" "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz"
"version" "5.0.1"
"follow-redirects@^1.14.0":
"integrity" "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
"resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz"
"version" "1.15.2"
"mime-db@1.50.0":
"integrity" "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A=="
"resolved" "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz"
"version" "1.50.0"
"mime-types@^2.1.12":
"integrity" "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g=="
"resolved" "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz"
"version" "2.1.33"
dependencies: dependencies:
"mime-db" "1.50.0" "@types/node" "^14.17.1"
detritus-utils "^0.4.0"
ws "^7.4.6"
"node-fetch@^2.6.1": detritus-rest@^0.7.0:
"integrity" "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==" version "0.7.0"
"resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" resolved "https://registry.yarnpkg.com/detritus-rest/-/detritus-rest-0.7.0.tgz#15d7a98c7038935de7ba2a88eeed83aba1c88ed7"
"version" "2.6.7" integrity sha512-T5FNXpyv5F69ZXEz6z8mWo5yiz7Mhrg+HQsZumih/uDeqf3+1AvZgNI4XLscG1D56F0o5DSyXkoOxDgd2oeTgA==
dependencies: dependencies:
"whatwg-url" "^5.0.0" form-data "^3.0.0"
node-fetch "^2.6.1"
"prism-media@^1.2.9": detritus-utils@^0.4.0:
"integrity" "sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==" version "0.4.0"
"resolved" "https://registry.npmjs.org/prism-media/-/prism-media-1.3.2.tgz" resolved "https://registry.yarnpkg.com/detritus-utils/-/detritus-utils-0.4.0.tgz#42c01e029bd716c5be30a82ee4c621136f64ef35"
"version" "1.3.2" integrity sha512-XO9crk1eCOecajzOg4WMsvcgXV/3GAltV0nt+kXXmglwcKyh/5DpQ7jmQ/2HycEN0dGBdgLCLPzYMxrWPN9gpw==
"setimmediate@^1.0.5": diff@^4.0.1:
"integrity" "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" version "4.0.2"
"resolved" "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
"version" "1.0.5" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
"tr46@~0.0.3": discord-api-types@^0.37.14:
"integrity" "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" version "0.37.14"
"resolved" "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.14.tgz#0fb10a5bb3ff9415afac12d7eaecc68b430d372a"
"version" "0.0.3" integrity sha512-byBH7SfDCMJwxdqeS8k5sihltH88/YPhuwx+vF2cftSxFLdxyHyU/ZxDL3bq+LB2c4ls/TymE76/ISlLfniUXg==
"tweetnacl@^1.0.3": dotenv@^16.0.3:
"integrity" "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" version "16.0.3"
"resolved" "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz"
"version" "1.0.3" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
"webidl-conversions@^3.0.0": follow-redirects@^1.15.0:
"integrity" "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" version "1.15.2"
"resolved" "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
"version" "3.0.1" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
"whatwg-url@^5.0.0": form-data@^3.0.0:
"integrity" "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==" version "3.0.1"
"resolved" "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
"version" "5.0.0" integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
dependencies: dependencies:
"tr46" "~0.0.3" asynckit "^0.4.0"
"webidl-conversions" "^3.0.0" combined-stream "^1.0.8"
mime-types "^2.1.12"
"ws@^7.4.4": form-data@^4.0.0:
"integrity" "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==" version "4.0.0"
"resolved" "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
"version" "7.5.5" integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
mime-db@1.50.0:
version "1.50.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz"
integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==
mime-types@^2.1.12:
version "2.1.33"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz"
integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==
dependencies:
mime-db "1.50.0"
node-fetch@^2.6.1:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
ts-node@^10.9.1:
version "10.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
typescript@^4.8.4:
version "4.8.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
ws@^7.4.6:
version "7.5.9"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==