initial commit,
This commit is contained in:
parent
d1b273f02a
commit
19d121a572
9 changed files with 614 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.env
|
||||
node_modules/
|
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "unix-chat",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.1",
|
||||
"node-fetch": "^3.2.4",
|
||||
"ws": "^8.6.0"
|
||||
}
|
||||
}
|
255
src/DiscordClient.js
Normal file
255
src/DiscordClient.js
Normal file
|
@ -0,0 +1,255 @@
|
|||
import EventEmitter from "events";
|
||||
import { WebSocket } from "ws";
|
||||
import fetch from "node-fetch";
|
||||
import { logger } from "./common.js";
|
||||
|
||||
const log = logger("log", "DiscordClient");
|
||||
const logError = logger("error", "DiscordClient");
|
||||
const logWarn = logger("warn", "DiscordClient");
|
||||
|
||||
const opcodes = {
|
||||
EVENT: 0,
|
||||
CLIENT_HEARTBEAT: 1,
|
||||
IDENTIFY: 2,
|
||||
RECONNECT: 7,
|
||||
INVALID_SESSION: 9,
|
||||
HELLO: 10,
|
||||
HEARTBEAT_ACK: 11,
|
||||
};
|
||||
|
||||
const skipReconnectFor = [
|
||||
4004, 4010, 4011, 4012, 4013, 4014
|
||||
];
|
||||
|
||||
const CLOSE_CONNECTION_ON_NO_ACK = false;
|
||||
|
||||
class DiscordClient extends EventEmitter {
|
||||
constructor(token, { intents, gatewayUrl="wss://gateway.discord.gg/?v=9&encoding=json", apiBase="https://discord.com/api/v9" } = {}) {
|
||||
super();
|
||||
|
||||
this.token = token;
|
||||
this.gatewayUrl = gatewayUrl;
|
||||
this.apiBase = apiBase;
|
||||
this.ws = null;
|
||||
this.intents = intents;
|
||||
this.user = null;
|
||||
this.guilds = [];
|
||||
this.sessionId = null;
|
||||
this.seq = null;
|
||||
this.gotServerHeartbeatACK = true;
|
||||
}
|
||||
_setHeartbeat(interval) {
|
||||
this._heartbeatIntervalTime = interval;
|
||||
if (interval < 0 && this._heartbeatInterval) {
|
||||
clearInterval(this._heartbeatInterval);
|
||||
return;
|
||||
}
|
||||
this._heartbeatInterval = setInterval(() => {
|
||||
if (CLOSE_CONNECTION_ON_NO_ACK && !this.gotServerHeartbeatACK) {
|
||||
logError("Closing due to no heartbeat ACK...");
|
||||
this.ws.close(1000, "No heartbeat ACK.");
|
||||
return;
|
||||
}
|
||||
this.gotServerHeartbeatACK = false;
|
||||
this.ws.send(JSON.stringify({
|
||||
op: opcodes.CLIENT_HEARTBEAT,
|
||||
d: this.seq
|
||||
}));
|
||||
}, this._heartbeatIntervalTime);
|
||||
}
|
||||
_getIdentifyPayload() {
|
||||
return {
|
||||
token: this.token,
|
||||
intents: this.intents,
|
||||
properties: {
|
||||
"$os": "",
|
||||
"$browser": "",
|
||||
"$device": ""
|
||||
},
|
||||
presence: {
|
||||
since: Date.now(),
|
||||
activities: [
|
||||
{
|
||||
type: 2, // LISTENING
|
||||
name: "the voices"
|
||||
}
|
||||
],
|
||||
status: "online",
|
||||
afk: false
|
||||
}
|
||||
};
|
||||
}
|
||||
_handleGatewayMessage(ws, message) {
|
||||
try {
|
||||
message = JSON.parse(message);
|
||||
} catch(e) {
|
||||
logError("on 'message': failed to parse incoming message as JSON", e);
|
||||
return;
|
||||
}
|
||||
if (message.s) {
|
||||
this.seq = message.s;
|
||||
}
|
||||
const payload = message.d;
|
||||
switch (message.op) {
|
||||
case opcodes.HELLO: {
|
||||
log(`HELLO; heartbeat_interval=${payload.heartbeat_interval}`);
|
||||
this._setHeartbeat(payload.heartbeat_interval);
|
||||
ws.send(JSON.stringify({
|
||||
op: opcodes.IDENTIFY,
|
||||
d: this._getIdentifyPayload()
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case opcodes.EVENT: {
|
||||
switch (message.t) {
|
||||
case "READY": {
|
||||
log("READY");
|
||||
this.user = payload.user;
|
||||
this.sessionId = payload.session_id;
|
||||
this.guilds = payload.guilds;
|
||||
break;
|
||||
}
|
||||
case "GUILD_CREATE": {
|
||||
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||
if (targetGuildIndex < 0) {
|
||||
this.guilds.push(payload);
|
||||
break;
|
||||
}
|
||||
// The guild already exists in our array. This means that
|
||||
// this GUILD_CREATE event is completing our `Unavailable Guild`
|
||||
// objects that we got from the initial READY.
|
||||
this.guilds[targetGuildIndex] = payload;
|
||||
break;
|
||||
}
|
||||
case "GUILD_UPDATE": {
|
||||
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||
if (targetGuildIndex < 0) {
|
||||
// tried to update a guild that doesn't exist???
|
||||
this.emit("warn", "got GUILD_UPDATE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
this.guilds[targetGuildIndex] = payload;
|
||||
break;
|
||||
}
|
||||
case "GUILD_DELETE": {
|
||||
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||
if (targetGuildIndex < 0) {
|
||||
// tried to delete a guild that doesn't exist???
|
||||
this.emit("warn", "got GUILD_DELETE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
this.guilds.splice(targetGuildIndex, 1);
|
||||
break;
|
||||
}
|
||||
case "CHANNEL_CREATE": {
|
||||
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||
if (parentGuildIndex < 0) {
|
||||
this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
this.guilds[parentGuildIndex].channels.push(payload);
|
||||
break;
|
||||
}
|
||||
case "CHANNEL_UPDATE": {
|
||||
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||
if (parentGuildIndex < 0) {
|
||||
this.emit("warn", "got CHANNEL_UPDATE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
const relevantChannelIndex = this.guilds[parentGuildIndex].channels.findIndex(e => e.id === payload.id);
|
||||
this.guilds[parentGuildIndex].channels[relevantChannelIndex] = payload;
|
||||
break;
|
||||
}
|
||||
case "CHANNEL_DELETE": {
|
||||
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||
if (parentGuildIndex < 0) {
|
||||
this.emit("warn", "got CHANNEL_DELETE for a guild that doesn't exist");
|
||||
break;
|
||||
}
|
||||
const relevantChannelIndex = this.guilds[parentGuildIndex].channels.findIndex(e => e.id === payload.id);
|
||||
this.guilds[parentGuildIndex].channels.splice(relevantChannelIndex, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.emit(message.t, payload);
|
||||
break;
|
||||
}
|
||||
case opcodes.HEARTBEAT_ACK: {
|
||||
this.gotServerHeartbeatACK = true;
|
||||
break;
|
||||
}
|
||||
case opcodes.INVALID_SESSION: {
|
||||
logError("INVALID_SESSION - please check your authentication token");
|
||||
logError("INVALID_SESSION: will not reconnect");
|
||||
break;
|
||||
}
|
||||
case opcodes.RECONNECT: {
|
||||
log("gateway is requesting reconnect (payload RECONNECT)");
|
||||
this.connect();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logWarn(`got unhandled opcode "${message.op}"`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
connect() {
|
||||
log("connecting...");
|
||||
if (this.ws) {
|
||||
log("a websocket connection already exists, killing...");
|
||||
this.ws.removeAllListeners();
|
||||
this._setHeartbeat(-1);
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
const ws = new WebSocket(this.gatewayUrl);
|
||||
this.ws = ws;
|
||||
ws.on("message", (data, isBinary) => {
|
||||
if (isBinary)
|
||||
return;
|
||||
|
||||
this._handleGatewayMessage(ws, data.toString());
|
||||
});
|
||||
ws.on("open", () => {
|
||||
log("WebSocket 'open'");
|
||||
});
|
||||
ws.on("close", (code, reason) => {
|
||||
reason = reason.toString();
|
||||
logError(`on 'close': disconnected from gateway: code '${code}', reason '${reason}'`);
|
||||
this.emit("close", code, reason);
|
||||
this._setHeartbeat(-1);
|
||||
if (skipReconnectFor.includes(code)) {
|
||||
logError("on 'close': the exit code above is in skipReconnectFor, and thus the server will not reconnect.");
|
||||
} else {
|
||||
log("on 'close': the client will now attempt to reconnect...");
|
||||
this.connect();
|
||||
}
|
||||
});
|
||||
ws.on("error", (e) => {
|
||||
logError("on 'error': websocket error:", e);
|
||||
log("on 'error': reconnecting due to previous websocket error...");
|
||||
this._setHeartbeat(-1);
|
||||
this.connect();
|
||||
});
|
||||
}
|
||||
async api(method, path, body=undefined, throwOnError=false) {
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
"authorization": `Bot ${this.token}`
|
||||
}
|
||||
};
|
||||
if (method !== "GET" && method !== "HEAD" && typeof body === "object") {
|
||||
options.headers["content-type"] = "application/json";
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
const response = await fetch(`${this.apiBase}${path}`, options);
|
||||
const json = await response.json();
|
||||
if (!response.ok && throwOnError) {
|
||||
throw new Error(`API request returned non-success status ${response.status}, with JSON body ${JSON.stringify(json)}`);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
export default DiscordClient;
|
24
src/bot.js
Normal file
24
src/bot.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { handleCommand } from "./commands/interperter.js";
|
||||
import DiscordClient from "./DiscordClient.js";
|
||||
|
||||
export function makeBot() {
|
||||
const client = new DiscordClient(
|
||||
process.env.DISCORD_TOKEN,
|
||||
{
|
||||
intents: (0 | (1 << 0) | (1 << 9)), // GUILDS & GUILD_MESSAGES
|
||||
}
|
||||
);
|
||||
|
||||
client.on("MESSAGE_CREATE", (message) => {
|
||||
if (message.content.startsWith("$ ")) {
|
||||
const command = message.content.substring(2, message.content.length);
|
||||
handleCommand(command, (_streamName, streamContent) => {
|
||||
client.api("POST", `/channels/${message.channel_id}/messages`, {
|
||||
content: streamContent
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
8
src/commands/commands.js
Normal file
8
src/commands/commands.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
echo: function(ctx) {
|
||||
return ctx.argv.join(" ");
|
||||
},
|
||||
wine: function(ctx) {
|
||||
return `${ctx.streamInput + " " ?? ""}:wine_glass:`;
|
||||
}
|
||||
}
|
206
src/commands/interperter.js
Normal file
206
src/commands/interperter.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
import commands from "./commands.js";
|
||||
|
||||
const MAX_INSTRUCTIONS = 32;
|
||||
const MAX_STREAMS = 6;
|
||||
|
||||
const TokenType = {
|
||||
Command: "Command",
|
||||
Argument: "Argument",
|
||||
Pipe: "Pipe"
|
||||
};
|
||||
|
||||
const InstructionType = {
|
||||
Run: "Run",
|
||||
CreateStream: "CreateStream",
|
||||
DisplayStream: "DisplayStream"
|
||||
};
|
||||
|
||||
const Expected = {
|
||||
Command: 0,
|
||||
AfterCommand: 1
|
||||
};
|
||||
|
||||
function token(type, value) {
|
||||
return { type, value };
|
||||
}
|
||||
|
||||
function tokenize(message) {
|
||||
const tokens = [];
|
||||
let val = "";
|
||||
let expecting = Expected.Command;
|
||||
// length +1 so we get char === undefined at the end of the string
|
||||
for (let i = 0; i < message.length + 1; i++) {
|
||||
const char = message[i];
|
||||
|
||||
if (char === " " || (char === undefined && val !== "")) { // if the character is a string OR if we are at the end of the string and still have a value
|
||||
if (expecting === Expected.AfterCommand) {
|
||||
if (val === "|") {
|
||||
tokens.push(token(TokenType.Pipe, "|"));
|
||||
expecting = Expected.Command;
|
||||
} else {
|
||||
tokens.push(token(TokenType.Argument, val));
|
||||
expecting = Expected.AfterCommand;
|
||||
}
|
||||
} else if (expecting === Expected.Command) {
|
||||
tokens.push(token(TokenType.Command, val));
|
||||
expecting = Expected.AfterCommand;
|
||||
} else {
|
||||
return {
|
||||
error: "Unexpected 'expecting' value, this is probably a bug in the parser",
|
||||
tokens: null,
|
||||
};
|
||||
}
|
||||
val = "";
|
||||
} else {
|
||||
val += char;
|
||||
}
|
||||
}
|
||||
if (expecting !== Expected.AfterCommand) {
|
||||
return {
|
||||
error: "Unexpected end of input",
|
||||
tokens: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: null,
|
||||
tokens
|
||||
};
|
||||
}
|
||||
|
||||
function tokensToInstructions(tokens) {
|
||||
const streamName = "_commandStream"
|
||||
const instructions = [
|
||||
{ type: InstructionType.CreateStream, name: streamName }
|
||||
];
|
||||
|
||||
let commandName = null;
|
||||
let commandArgs = [];
|
||||
const endCommand = () => {
|
||||
instructions.push({
|
||||
type: InstructionType.Run,
|
||||
name: commandName,
|
||||
args: commandArgs,
|
||||
inStream: streamName,
|
||||
outStream: streamName
|
||||
});
|
||||
commandName = null;
|
||||
commandArgs = [];
|
||||
};
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const tok = tokens[i];
|
||||
if (tok.type === TokenType.Command) {
|
||||
commandName = tok.value;
|
||||
if (!commands[commandName]) {
|
||||
return {
|
||||
error: "Unknown command",
|
||||
instructions: null
|
||||
}
|
||||
}
|
||||
} else if (tok.type === TokenType.Argument) {
|
||||
if (!commandName) {
|
||||
return {
|
||||
error: "Got TokenType.Argument, however commandName is null",
|
||||
instructions: null
|
||||
}
|
||||
}
|
||||
commandArgs.push(tok.value);
|
||||
} else if (tok.type === TokenType.Pipe) {
|
||||
if (!commandName) {
|
||||
return {
|
||||
error: "Got TokenType.Pipe, however commandName is null",
|
||||
instructions: null
|
||||
}
|
||||
}
|
||||
endCommand();
|
||||
} else {
|
||||
return {
|
||||
error: "Unknown tok.type",
|
||||
instructions: null
|
||||
}
|
||||
}
|
||||
|
||||
if ((i + 1) === tokens.length) {
|
||||
if (commandName === null) {
|
||||
return {
|
||||
error: "End of input, yet commandName is null",
|
||||
instructions: null
|
||||
}
|
||||
}
|
||||
endCommand();
|
||||
}
|
||||
}
|
||||
|
||||
instructions.push({ type: InstructionType.DisplayStream, name: streamName });
|
||||
|
||||
return {
|
||||
error: null,
|
||||
instructions
|
||||
};
|
||||
}
|
||||
|
||||
function interpretInstructions(instructions, displayFunc) {
|
||||
if (instructions.length > MAX_INSTRUCTIONS) {
|
||||
return { error: `Arbitrary limit of ${MAX_INSTRUCTIONS} instructions reached` };
|
||||
}
|
||||
|
||||
const streams = new Map();
|
||||
for (let i = 0; i < instructions.length; i++) {
|
||||
const inst = instructions[i];
|
||||
switch (inst.type) {
|
||||
case InstructionType.CreateStream: {
|
||||
if (streams.size > MAX_STREAMS) {
|
||||
return { error: `Arbitrary limit of ${MAX_STREAMS} streams reached` };
|
||||
}
|
||||
streams.set(inst.name, "");
|
||||
break;
|
||||
}
|
||||
case InstructionType.Run: {
|
||||
const commandFunc = commands[inst.name];
|
||||
if (!commandFunc) {
|
||||
return { error: "Command not found" };
|
||||
}
|
||||
let streamInput;
|
||||
if (inst.inStream) {
|
||||
streamInput = streams.get(inst.inStream);
|
||||
}
|
||||
let streamOutput = commandFunc({
|
||||
argv: inst.args,
|
||||
argc: inst.args.length,
|
||||
streamInput
|
||||
});
|
||||
if (inst.outStream) {
|
||||
streams.set(inst.outStream, streamOutput);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InstructionType.DisplayStream: {
|
||||
displayFunc(inst.name, streams.get(inst.name));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return { error: "Unknown inst.type" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function handleCommand(message, displayFunc) {
|
||||
const { error: tokError, tokens } = tokenize(message);
|
||||
if (tokError) {
|
||||
return {
|
||||
error: `error: tokenize: ${tokError}`
|
||||
};
|
||||
}
|
||||
const { error: insError, instructions } = tokensToInstructions(tokens);
|
||||
console.log(instructions);
|
||||
if (insError) {
|
||||
return {
|
||||
error: `error: tokensToInstructions: ${insError}`
|
||||
};
|
||||
}
|
||||
|
||||
return interpretInstructions(instructions, displayFunc);
|
||||
}
|
46
src/common.js
Normal file
46
src/common.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
const logContextMap = {
|
||||
DiscordClient: {
|
||||
log: true,
|
||||
warn: true,
|
||||
error: true,
|
||||
},
|
||||
ServerMain: {
|
||||
log: true,
|
||||
warn: true,
|
||||
error: true,
|
||||
},
|
||||
API: {
|
||||
log: true,
|
||||
warn: true,
|
||||
error: true,
|
||||
}
|
||||
};
|
||||
|
||||
export function logger(sink, context) {
|
||||
let sinkFunction;
|
||||
switch (sink) {
|
||||
case "log": {
|
||||
sinkFunction = console.log;
|
||||
break;
|
||||
}
|
||||
case "warn": {
|
||||
sinkFunction = console.warn;
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
sinkFunction = console.error;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
sinkFunction = () => {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (logContextMap[context] && logContextMap[context][sink]) {
|
||||
return (...e) => {
|
||||
sinkFunction(`[${context}]`, ...e);
|
||||
};
|
||||
} else {
|
||||
return (...e) => {};
|
||||
}
|
||||
}
|
9
src/index.js
Normal file
9
src/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import "dotenv/config";
|
||||
import { makeBot } from "./bot.js";
|
||||
|
||||
async function main() {
|
||||
const bot = makeBot();
|
||||
bot.connect();
|
||||
}
|
||||
|
||||
await main();
|
52
yarn.lock
Normal file
52
yarn.lock
Normal file
|
@ -0,0 +1,52 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
data-uri-to-buffer@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
|
||||
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
|
||||
|
||||
dotenv@^16.0.1:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
|
||||
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
|
||||
|
||||
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.5.tgz#0077bf5f3fcdbd9d75a0b5362f77dbb743489863"
|
||||
integrity sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==
|
||||
dependencies:
|
||||
node-domexception "^1.0.0"
|
||||
web-streams-polyfill "^3.0.3"
|
||||
|
||||
formdata-polyfill@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
|
||||
dependencies:
|
||||
fetch-blob "^3.1.2"
|
||||
|
||||
node-domexception@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||
|
||||
node-fetch@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.4.tgz#3fbca2d8838111048232de54cb532bd3cf134947"
|
||||
integrity sha512-WvYJRN7mMyOLurFR2YpysQGuwYrJN+qrrpHjJDuKMcSPdfFccRUla/kng2mz6HWSBxJcqPbvatS6Gb4RhOzCJw==
|
||||
dependencies:
|
||||
data-uri-to-buffer "^4.0.0"
|
||||
fetch-blob "^3.1.4"
|
||||
formdata-polyfill "^4.0.10"
|
||||
|
||||
web-streams-polyfill@^3.0.3:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
||||
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
||||
|
||||
ws@^8.6.0:
|
||||
version "8.6.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23"
|
||||
integrity sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==
|
Loading…
Reference in a new issue