use dotenv instead of config, sync with changes from glitch
This commit is contained in:
parent
febd3feb61
commit
5876183d93
7
.env.sample
Normal file
7
.env.sample
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
MATRIX_SERVER_URL=
|
||||||
|
BOT_DISPLAY_NAME=
|
||||||
|
BOT_USERNAME=
|
||||||
|
BOT_PASSWORD=
|
||||||
|
BOT_USERID=
|
||||||
|
FACILITATOR_ROOM_ID=
|
||||||
|
CHAT_OFFLINE_MESSAGE=
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
config
|
.env
|
||||||
node_modules
|
node_modules
|
||||||
*.log
|
*.log
|
@ -4,12 +4,12 @@
|
|||||||
"description": "Chatbot to manage interactions on OCRCC client chatbots",
|
"description": "Chatbot to manage interactions on OCRCC client chatbots",
|
||||||
"main": "dist/ocrcc-chatbot.js",
|
"main": "dist/ocrcc-chatbot.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "nodemon --exec babel-node src/index.js"
|
"start": "nodemon -r dotenv/config --exec babel-node src/index.js"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"config": "^3.2.5",
|
"dotenv": "^8.2.0",
|
||||||
"matrix-bot-sdk": "^0.5.0",
|
"matrix-bot-sdk": "^0.5.0",
|
||||||
"matrix-js-sdk": "^5.0.0",
|
"matrix-js-sdk": "^5.0.0",
|
||||||
"node-localstorage": "^2.1.5",
|
"node-localstorage": "^2.1.5",
|
||||||
|
301
src/index.js
301
src/index.js
@ -1,205 +1,194 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from "fs";
|
||||||
import * as os from 'os'
|
import * as os from "os";
|
||||||
import * as path from 'path'
|
import * as path from "path";
|
||||||
import * as util from 'util'
|
import * as util from "util";
|
||||||
import { LocalStorage } from "node-localstorage";
|
import { LocalStorage } from "node-localstorage";
|
||||||
import { uuid } from "uuidv4"
|
|
||||||
import config from 'config';
|
|
||||||
|
|
||||||
global.Olm = require('olm');
|
global.Olm = require("olm");
|
||||||
|
|
||||||
import * as matrix from "matrix-js-sdk";
|
import * as matrix from "matrix-js-sdk";
|
||||||
|
|
||||||
import logger from './logger'
|
import logger from "./logger";
|
||||||
|
|
||||||
const ENCRYPTION_CONFIG = { "algorithm": "m.megolm.v1.aes-sha2" };
|
|
||||||
|
const ENCRYPTION_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" };
|
||||||
|
|
||||||
class OcrccBot {
|
class OcrccBot {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.awaitingAgreement = {}
|
this.awaitingAgreement = {};
|
||||||
this.awaitingFacilitator = {}
|
this.awaitingFacilitator = {};
|
||||||
this.client = matrix.createClient(config.get('homeserverUrl'))
|
this.client = matrix.createClient(process.env.MATRIX_SERVER_URL);
|
||||||
|
this.joinedRooms = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
createLocalStorage() {
|
createLocalStorage() {
|
||||||
const storageLoc = `matrix-chatbot-${config.get('username')}`
|
const storageLoc = `matrix-chatbot-${process.env.BOT_USERNAME}`;
|
||||||
const dir = path.resolve(path.join(os.homedir(), ".local-storage"))
|
const dir = path.resolve(path.join(os.homedir(), ".local-storage"));
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir);
|
fs.mkdirSync(dir);
|
||||||
}
|
}
|
||||||
const localStoragePath = path.resolve(path.join(dir, storageLoc))
|
const localStoragePath = path.resolve(path.join(dir, storageLoc));
|
||||||
return new LocalStorage(localStoragePath);
|
return new LocalStorage(localStoragePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(roomId, msgText) {
|
sendMessage(roomId, msgText) {
|
||||||
return this.client.sendTextMessage(roomId, msgText)
|
return this.client
|
||||||
.then((res) => {
|
.sendTextMessage(roomId, msgText)
|
||||||
logger.log('info', "Message sent")
|
.then(res => {
|
||||||
logger.log('info', res)
|
logger.log("info", "Message sent");
|
||||||
|
logger.log("info", res);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
switch (err["name"]) {
|
switch (err["name"]) {
|
||||||
case "UnknownDeviceError":
|
case "UnknownDeviceError":
|
||||||
Object.keys(err.devices).forEach((userId) => {
|
Object.keys(err.devices).forEach(userId => {
|
||||||
Object.keys(err.devices[userId]).map((deviceId) => {
|
Object.keys(err.devices[userId]).map(deviceId => {
|
||||||
this.client.setDeviceVerified(userId, deviceId, true);
|
this.client.setDeviceVerified(userId, deviceId, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return this.sendMessage(roomId, msgText)
|
return this.sendMessage(roomId, msgText);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
logger.log('error', "Error sending message");
|
logger.log("error", "Error sending message");
|
||||||
logger.log('error', err);
|
logger.log("error", err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
sendHtmlMessage(roomId, msgText, msgHtml) {
|
|
||||||
return this.client.sendHtmlMessage(roomId, msgText, msgHtml)
|
|
||||||
.then((res) => {
|
|
||||||
logger.log('info', "Message sent")
|
|
||||||
logger.log('info', res)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
switch (err["name"]) {
|
|
||||||
case "UnknownDeviceError":
|
|
||||||
Object.keys(err.devices).forEach((userId) => {
|
|
||||||
Object.keys(err.devices[userId]).map((deviceId) => {
|
|
||||||
this.client.setDeviceVerified(userId, deviceId, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return this.sendHtmlMessage(roomId, msgText, msgHtml)
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logger.log('error', "Error sending message");
|
|
||||||
logger.log('error', err);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteFacilitators(roomId) {
|
inviteFacilitators(roomId) {
|
||||||
this.awaitingFacilitator[roomId] = true
|
this.awaitingFacilitator[roomId] = true;
|
||||||
this.client.getJoinedRoomMembers(config.get('waitingRoomId'))
|
let chatOffline = true;
|
||||||
.then((members) => {
|
this.client
|
||||||
Object.keys(members["joined"]).forEach((member) => {
|
.getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID)
|
||||||
if (member !== config.get('userId'))
|
.then(members => {
|
||||||
this.client.invite(roomId, member)
|
let onlineMembersCount = 0;
|
||||||
|
Object.keys(members["joined"]).forEach(member => {
|
||||||
|
const user = this.client.getUser(member);
|
||||||
|
if (user.presence === "online" && member !== process.env.BOT_USERID) {
|
||||||
|
logger.log("info", "INVITING MEMBER: " + member);
|
||||||
|
chatOffline = false;
|
||||||
|
this.client.invite(roomId, member);
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
})
|
.then(() => {
|
||||||
// const notif = `There is a support seeker waiting. Go to https://riot.im/app/#/room/${roomId} to respond.`
|
if (chatOffline) {
|
||||||
// sendMessage(waitingRoomId, notif)
|
this.sendMessage(roomId, process.env.CHAT_OFFLINE_MESSAGE);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.log("error", "ERROR GETTING ROOM MEMBERS");
|
||||||
|
logger.log("error", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
uninviteFacilitators(roomId) {
|
uninviteFacilitators(roomId) {
|
||||||
this.awaitingFacilitator[roomId] = false
|
this.awaitingFacilitator[roomId] = false;
|
||||||
this.client.getJoinedRoomMembers(config.get('waitingRoomId'))
|
this.client
|
||||||
.then((allFacilitators) => {
|
.getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID)
|
||||||
this.client.getJoinedRoomMembers(roomId)
|
.then(allFacilitators => {
|
||||||
.then((roomMembers) => {
|
this.client.getJoinedRoomMembers(roomId).then(roomMembers => {
|
||||||
const membersIds = Object.keys(roomMembers["joined"])
|
const membersIds = Object.keys(roomMembers["joined"]);
|
||||||
const facilitatorsIds = Object.keys(allFacilitators["joined"])
|
const facilitatorsIds = Object.keys(allFacilitators["joined"]);
|
||||||
facilitatorsIds.forEach((f) => {
|
facilitatorsIds.forEach(f => {
|
||||||
if (!membersIds.includes(f)) {
|
if (!membersIds.includes(f)) {
|
||||||
logger.log("info", "kicking out " + f + " from " + roomId)
|
logger.log("info", "kicking out " + f + " from " + roomId);
|
||||||
this.client.kick(roomId, f, "A facilitator has already joined this chat.")
|
this.client
|
||||||
.then(() => {
|
.kick(roomId, f, "A facilitator has already joined this chat.")
|
||||||
logger.log("info", "Kick success")
|
.then(() => {
|
||||||
})
|
logger.log("info", "Kick success");
|
||||||
.catch((err) => {
|
})
|
||||||
logger.log("error", err)
|
.catch(err => {
|
||||||
})
|
logger.log("error", err);
|
||||||
}
|
});
|
||||||
})
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
})
|
})
|
||||||
})
|
.catch(err => logger.log("error", err));
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const localStorage = this.createLocalStorage()
|
const localStorage = this.createLocalStorage();
|
||||||
let deviceId = localStorage.getItem('deviceId')
|
|
||||||
|
|
||||||
this.client.login('m.login.password', {
|
this.client
|
||||||
user: config.get('username'),
|
.login("m.login.password", {
|
||||||
password: config.get('password'),
|
user: process.env.BOT_USERNAME,
|
||||||
initial_device_display_name: config.get('botName'),
|
password: process.env.BOT_PASSWORD,
|
||||||
deviceId: deviceId,
|
initial_device_display_name: process.env.BOT_DISPLAY_NAME
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then(data => {
|
||||||
const accessToken = data.access_token
|
const accessToken = data.access_token;
|
||||||
const deviceId = data.device_id
|
const deviceId = data.device_id;
|
||||||
|
|
||||||
localStorage.setItem('deviceId', data.device_id)
|
// create new client with full options
|
||||||
|
|
||||||
// create new client with full options
|
let opts = {
|
||||||
|
baseUrl: process.env.MATRIX_SERVER_URL,
|
||||||
|
accessToken: accessToken,
|
||||||
|
userId: process.env.BOT_USERID,
|
||||||
|
deviceId: deviceId,
|
||||||
|
sessionStore: new matrix.WebStorageSessionStore(localStorage)
|
||||||
|
};
|
||||||
|
|
||||||
let opts = {
|
this.client = matrix.createClient(opts);
|
||||||
baseUrl: config.get('homeserverUrl'),
|
})
|
||||||
accessToken: accessToken,
|
.catch(err => {
|
||||||
userId: config.get('userId'),
|
logger.log("error", `Login error: ${err}`);
|
||||||
deviceId: deviceId,
|
})
|
||||||
sessionStore: new matrix.WebStorageSessionStore(localStorage),
|
.then(() =>
|
||||||
}
|
this.client.initCrypto().catch(err => {
|
||||||
|
logger.log("error", `ERROR STARTING CRYPTO: ${err}`);
|
||||||
this.client = matrix.createClient(opts)
|
})
|
||||||
})
|
)
|
||||||
.catch(err => {
|
.then(() =>
|
||||||
logger.log('error', `Login error: ${err}`)
|
this.client.getJoinedRooms().then(data => {
|
||||||
})
|
this.joinedRooms = data["joined_rooms"];
|
||||||
.then(() => this.client.initCrypto())
|
})
|
||||||
.then(() => {
|
)
|
||||||
|
.then(() => {
|
||||||
// Automatically accept all room invitations
|
// Automatically accept all room invitations
|
||||||
// On joining a room, send the intro messages and wait for agreement to continue
|
this.client.on("RoomMember.membership", (event, member) => {
|
||||||
this.client.on("RoomMember.membership", (event, member) => {
|
if (
|
||||||
if (member.membership === "invite" && member.userId === config.get('userId')) {
|
member.membership === "invite" &&
|
||||||
logger.log("info", "Auto-joining room " + member.roomId)
|
member.userId === process.env.BOT_USERID &&
|
||||||
this.client.joinRoom(member.roomId)
|
!this.joinedRooms.includes(member.roomId)
|
||||||
.then(() => this.client.setRoomEncryption(member.roomId, ENCRYPTION_CONFIG))
|
) {
|
||||||
.then(() => {
|
logger.log("info", "Auto-joining room " + member.roomId);
|
||||||
if (member.roomId !== config.get('waitingRoomId')) {
|
this.client
|
||||||
this.sendMessage(member.roomId, config.get('introMessage'))
|
.joinRoom(member.roomId)
|
||||||
.then(() => this.sendHtmlMessage(member.roomId, `Please read the terms and conditions at ${config.get('termsUrl')}`, `Please read the full <a href="${config.get('termsUrl')}">terms and conditions</a>.`))
|
.then(room => {
|
||||||
.then(() => this.sendMessage(member.roomId, config.get('agreementMessage')))
|
this.sendMessage(
|
||||||
.then(() => this.awaitingAgreement[member.roomId] = true)
|
process.env.FACILITATOR_ROOM_ID,
|
||||||
}
|
`A support seeker requested a chat (Room ID: ${member.roomId})`
|
||||||
})
|
);
|
||||||
}
|
this.client.setRoomEncryption(member.roomId, ENCRYPTION_CONFIG);
|
||||||
|
})
|
||||||
// When the first facilitator joins a support session, uninvite the other facilitators
|
.then(() => this.inviteFacilitators(member.roomId));
|
||||||
if (member.membership === 'join' && this.awaitingFacilitator[member.roomId]) {
|
|
||||||
this.uninviteFacilitators(member.roomId)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for incoming messages
|
|
||||||
this.client.on('Event.decrypted', (event) => {
|
|
||||||
if (event.getType() === 'm.room.message') {
|
|
||||||
const roomId = event.getRoomId()
|
|
||||||
const sender = event.getSender()
|
|
||||||
const content = event.getContent()
|
|
||||||
const body = content.body
|
|
||||||
|
|
||||||
// Listen for the user to agree to continue, then invite facilitators to join
|
|
||||||
if (sender !== config.get('userId') && this.awaitingAgreement[roomId]) {
|
|
||||||
if (body.toLowerCase().startsWith('yes')) {
|
|
||||||
this.sendMessage(roomId, config.get('confirmationMessage'))
|
|
||||||
this.inviteFacilitators(roomId)
|
|
||||||
this.awaitingAgreement[roomId] = false
|
|
||||||
} else {
|
|
||||||
this.sendMessage(roomId, config.get('exitMessage'))
|
|
||||||
this.awaitingAgreement[roomId] = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => this.client.startClient())
|
|
||||||
|
|
||||||
|
// When the first facilitator joins a support session, uninvite the other facilitators
|
||||||
|
if (
|
||||||
|
member.membership === "join" &&
|
||||||
|
member.userId !== process.env.BOT_USERID &&
|
||||||
|
this.awaitingFacilitator[member.roomId]
|
||||||
|
) {
|
||||||
|
this.sendMessage(
|
||||||
|
member.roomId,
|
||||||
|
`${member.name} has joined the chat.`
|
||||||
|
);
|
||||||
|
this.sendMessage(
|
||||||
|
process.env.FACILITATOR_ROOM_ID,
|
||||||
|
`${member.name} joined the chat (Room ID: ${member.roomId})`
|
||||||
|
);
|
||||||
|
this.uninviteFacilitators(member.roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => this.client.startClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bot = new OcrccBot();
|
const bot = new OcrccBot();
|
||||||
bot.start()
|
bot.start();
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import winston from 'winston'
|
import winston from "winston";
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: 'info',
|
level: "info",
|
||||||
format: winston.format.json(),
|
format: winston.format.json(),
|
||||||
defaultMeta: { service: 'user-service' },
|
defaultMeta: { service: "user-service" },
|
||||||
transports: [
|
transports: [
|
||||||
//
|
//
|
||||||
// - Write all logs with level `error` and below to `error.log`
|
// - Write all logs with level `error` and below to `error.log`
|
||||||
// - Write all logs with level `info` and below to `combined.log`
|
// - Write all logs with level `info` and below to `combined.log`
|
||||||
//
|
//
|
||||||
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
new winston.transports.File({ filename: "error.log", level: "error" }),
|
||||||
new winston.transports.File({ filename: 'combined.log' })
|
new winston.transports.File({ filename: "combined.log" })
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -18,10 +18,12 @@ const logger = winston.createLogger({
|
|||||||
// If we're not in production then log to the `console` with the format:
|
// If we're not in production then log to the `console` with the format:
|
||||||
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
||||||
//
|
//
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
logger.add(new winston.transports.Console({
|
logger.add(
|
||||||
format: winston.format.simple()
|
new winston.transports.Console({
|
||||||
}));
|
format: winston.format.simple()
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default logger;
|
export default logger;
|
||||||
|
19
yarn.lock
19
yarn.lock
@ -1167,13 +1167,6 @@ concat-map@0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||||
|
|
||||||
config@^3.2.5:
|
|
||||||
version "3.2.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/config/-/config-3.2.5.tgz#ab10ab88b61a873fbf9a5f0c6b4a22750422f243"
|
|
||||||
integrity sha512-8itpjyR01lAJanhAlPncBngYRZez/LoRLW8wnGi+6SEcsUyA1wvHvbpIrAJYDJT+W9BScnj4mYoUgbtp9I+0+Q==
|
|
||||||
dependencies:
|
|
||||||
json5 "^1.0.1"
|
|
||||||
|
|
||||||
configstore@^3.0.0:
|
configstore@^3.0.0:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
|
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
|
||||||
@ -1358,6 +1351,11 @@ dot-prop@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-obj "^1.0.0"
|
is-obj "^1.0.0"
|
||||||
|
|
||||||
|
dotenv@^8.2.0:
|
||||||
|
version "8.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||||
|
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||||
|
|
||||||
duplexer3@^0.1.4:
|
duplexer3@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
|
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
|
||||||
@ -1983,13 +1981,6 @@ json-stringify-safe@~5.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||||
|
|
||||||
json5@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
|
|
||||||
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
|
|
||||||
dependencies:
|
|
||||||
minimist "^1.2.0"
|
|
||||||
|
|
||||||
json5@^2.1.0:
|
json5@^2.1.0:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6"
|
||||||
|
Loading…
Reference in New Issue
Block a user