diff --git a/Component/Web.ts b/Component/Web.ts new file mode 100644 index 0000000..14605f4 --- /dev/null +++ b/Component/Web.ts @@ -0,0 +1,349 @@ +import crypto from "crypto"; +import { Application, NextFunction, Request, Response } from "express"; +import express from "express"; +import { promises as fsp } from "fs"; +import { ObjectID } from "mongodb"; +import multer from "multer"; +import { Core } from ".."; +import { AudioManager, ERR_MISSING_TITLE, ERR_NOT_AUDIO, IAudioData } from "../Core/AudioManager"; +import { IAudioList, ListManager } from "../Core/ListManager"; +import { IUserData, UserManager } from "../Core/UserManager"; +// import { retry, sleep } from "../Core/Utils/PromiseUtils"; + +export const BIND_TYPE = "telegram"; // "web"; +const ERR_MISSING_TOKEN = Error("Telegram bot api token not found!"); +// const ERR_NOT_VALID_TITLE = Error("Not valid title"); +// const ERR_LIST_NOT_FOUND = Error("Playlist not found"); +const ERR_BAD_REQUEST = "Maybe wrong paramters!"; +const ERR_FORBIDDEN = "You're not allowed to do this!"; +const ERR_NOT_REGISTER = "Please use /register to register or bind account via Telegram!"; +const ERR_NOT_FOUND = "File disappeared!"; + +export class Web { + private audio: AudioManager; + private user: UserManager; + private list: ListManager; + private digest: Buffer; + private upload: string; + private webConfig: any; + private server: Application; + + constructor(core: Core) { + if (!core.config.telegram.token) throw ERR_MISSING_TOKEN; + + this.user = core.userManager; + this.audio = core.audioManager; + this.list = core.listManager; + + this.digest = crypto.createHash("sha256").update(core.config.telegram.token).digest(); + this.upload = core.config.web.upload; + this.webConfig = { + tgBotName: core.config.telegram.botname, + title: core.config.web.title, + }; + + // Create Server + this.server = express(); + + this.middlewares(); + this.registerRoutes(); + this.errorHandler(); + + this.server.listen(8081); + } + + private async middlewares() { + this.server.use(express.json()); // for parsing application/json + } + + private async errorHandler() { + this.server.use((err: Error, req: Request, res: Response, next: NextFunction) => { + if (err.message.startsWith("HTTP400")) { + res.status(400).json({ + error: ERR_BAD_REQUEST + }); + } else if (err.message.startsWith("HTTP401")) { + res.status(401).json({ + error: ERR_NOT_REGISTER + }); + } else if (err.message.startsWith("HTTP403")) { + res.status(403).json({ + error: ERR_FORBIDDEN + }); + } else if (err.message.startsWith("HTTP404")) { + res.status(404).json({ + error: ERR_NOT_FOUND + }); + } else { + next(err); + } + }); + } + + private route(fn: (req: Request, res: Response, next: NextFunction) => Promise | void) { + return (req: Request, res: Response, next: NextFunction) => { + const promise = fn.bind(this)(req, res, next); + if (promise instanceof Promise) { + promise.catch(next); + } + }; + } + + private async registerRoutes() { + const upload = multer({ dest: this.upload }); + + this.server.get("/", (req: Request, res: Response) => res.send("MusicBot Web Server")); + this.server.get("/config", this.route(this.getConfig)); + this.server.get("/login", this.route(this.getLogin)); + this.server.get("/lists", this.route(this.getLists)); + this.server.post("/lists", this.route(this.postLists)); + this.server.get("/list/:lid", this.route(this.getList)); + this.server.patch("/list/:lid", this.route(this.patchList)); + this.server.delete("/list/:lid", this.route(this.deleteList)); + this.server.get("/list/:lid/audios", this.route(this.getListAudios)); + this.server.post("/list/:lid/audios", upload.array("audio"), this.route(this.postListAudios)); + this.server.delete("/list/:lid/audio/:aid", this.route(this.deleteListAudio)); + this.server.get("/audio/:aid", this.route(this.getAudio)); + this.server.get("/audio/:aid/file", this.route(this.getAudioFile)); + } + + private async getConfig(req: Request, res: Response) { + res.json(this.webConfig); + } + + private async getLogin(req: Request, res: Response) { + const user = await this.checkUser(req); + res.json({ + msg: "OK", + user + }); + } + + private async getLists(req: Request, res: Response) { + const user = await this.getUser(req); + const lists = await this.list.getAll().map((list: IAudioList) => { + return { + id: list._id, + name: list.name, + own: !!user && user._id!.equals(list.owner) + }; + }).toArray(); + const own: any[] = []; + const other: any[] = []; + lists.forEach(list => { + if (list.own) { + own.push(list); + } else { + other.push(list); + } + }); + res.json({ + lists: [...own, ...other], + msg: "OK" + }); + } + + private async postLists(req: Request, res: Response) { + const user = await this.checkUser(req); + const name = (req as any).body.name; + if (!name) { + throw Error("HTTP400"); + } + const list = await this.list.create(name, user._id!); + res.json({ + list, + msg: "OK" + }); + } + + private async getList(req: Request, res: Response) { + const list = await this.list.get(new ObjectID(req.params.lid)); + res.json({ + list, + msg: "OK" + }); + } + + private async patchList(req: Request, res: Response) { + const user = await this.checkUser(req); + const list = await this.list.get(new ObjectID(req.params.lid)); + const name = (req as any).body.name; + if (!name) { + throw Error("HTTP400"); + } + if (!list) { + throw Error("HTTP404"); + } + if (!user._id!.equals(list.owner)) { + throw Error("HTTP403"); + } + const result = await this.list.rename(list._id, name); + res.json({ + msg: "OK", + result + }); + } + + private async deleteList(req: Request, res: Response) { + const user = await this.checkUser(req); + const list = await this.list.get(new ObjectID(req.params.lid)); + if (!list) { + throw Error("HTTP404"); + } + if (!user._id!.equals(list.owner)) { + throw Error("HTTP403"); + } + const result = await this.list.delete(list._id); + res.json({ + msg: "OK", + result + }); + } + + private async getListAudios(req: Request, res: Response) { + const list = await this.list.get(new ObjectID(req.params.lid)); + if (!list) { + throw Error("HTTP404"); + } + const audios = await this.audio.search({ + _id: { + $in: list.audio.map(id => new ObjectID(id)) + } + }).toArray(); + res.json({ + audios, + msg: "OK" + }); + } + + private async postListAudios(req: Request, res: Response) { + try { + const user = await this.checkUser(req); + const list = await this.list.get(new ObjectID(req.params.lid)); + if (!list) { + throw Error("HTTP404"); + } + if (!user._id!.equals(list.owner)) { + throw Error("HTTP403"); + } + const audios: IAudioData[] = []; + const paths: string[] = []; + if ((req as any).files) { + (req as any).files.forEach((file: any) => { + paths.push(file.path); + }); + } + if ((req as any).body) { + const body = (req as any).body; + if (body.uris) { + if (typeof body.uris === "string") { + paths.push(...JSON.parse(body.uris)); + } else { + paths.push(...body.uris); + } + } + } + await Promise.all(paths.map(async path => { + const audio = await this.processFile(path, user); + if (audio) { + await this.list.addAudio(list._id!, audio._id!); + audios.push(audio); + } else { + // failed + } + })); + res.json({ + audios, + msg: "OK" + }); + } finally { + if ((req as any).files) { + await Promise.all((req as any).files.map(async (file: any) => await fsp.unlink(file.path))); + } + } + } + + private async deleteListAudio(req: Request, res: Response) { + const user = await this.checkUser(req); + const list = await this.list.get(new ObjectID(req.params.lid)); + if (!list) { + throw Error("HTTP404"); + } + if (!user._id!.equals(list.owner)) { + throw Error("HTTP403"); + } + const result = await this.list.delAudio(list._id, new ObjectID(req.params.aid)); + res.json({ + msg: "OK", + result + }); + } + + private async getAudio(req: Request, res: Response) { + const audio = await this.audio.get(new ObjectID(req.params.aid)); + res.json({ + audio, + msg: "OK" + }); + } + + private async getAudioFile(req: Request, res: Response) { + const audio = await this.audio.get(new ObjectID(req.params.aid)); + if (!audio) { + throw Error("HTTP404"); + } + const file = await this.audio.getFile(audio); + if (!file) { + throw Error("HTTP404"); + } + res.download(file); + } + + private async getUser(req: Request) { + const tgStr = req.get("X-Auth"); + if (!tgStr) return null; + const tg = JSON.parse(tgStr); + const hash = tg.hash; + delete tg.hash; + const payload = Object.keys(tg).sort().map(key => { + return `${key}=${tg[key]}`; + }).join("\n"); + const hmac = crypto.createHmac("sha256", this.digest); + hmac.update(payload); + if (hmac.digest("hex") !== hash) { + return null; + } + return this.user.getFromBind(BIND_TYPE, tg.id); + } + + private async checkUser(req: Request) { + const user = await this.getUser(req); + if (!user) { + throw Error("HTTP401"); + } + return user; + } + + private async processFile(file: string, sender: IUserData) { + if (!file) return null; + + let audio; + try { + audio = await this.audio.add(sender._id!, file); + } catch (error) { + if (error === ERR_MISSING_TITLE) { + // TODO show error + return null; + } else if (error === ERR_NOT_AUDIO) { + // not audio file + return null; + } else { + // unkown dead + return null; + } + } + + return audio; + } + +} diff --git a/Core/Utils/MediaInfo.ts b/Core/Utils/MediaInfo.ts index ed7b70b..15cf5dd 100644 --- a/Core/Utils/MediaInfo.ts +++ b/Core/Utils/MediaInfo.ts @@ -7,6 +7,7 @@ try { execFileSync("ffprobe", ["-version"], { stdio: "ignore" }); ffprobe = "ffprobe"; } catch (err) { + // tslint:disable-next-line:no-var-requires ffprobe = require("@ffprobe-installer/ffprobe").path; } diff --git a/config.json.example b/config.json.example index 968ffe3..e458625 100644 --- a/config.json.example +++ b/config.json.example @@ -9,10 +9,15 @@ "name": "musicbot" }, "telegram": { - "token": "" + "token": "", + "botname": "" }, "discord": { "token": "", "owner": "self" + }, + "web": { + "upload": "Upload", + "title": "MusicBot" } -} \ No newline at end of file +} diff --git a/dist/Component/Web.js b/dist/Component/Web.js new file mode 100644 index 0000000..f2445c5 --- /dev/null +++ b/dist/Component/Web.js @@ -0,0 +1,320 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Web = exports.BIND_TYPE = void 0; +const crypto_1 = __importDefault(require("crypto")); +const express_1 = __importDefault(require("express")); +const fs_1 = require("fs"); +const mongodb_1 = require("mongodb"); +const multer_1 = __importDefault(require("multer")); +const AudioManager_1 = require("../Core/AudioManager"); +exports.BIND_TYPE = "telegram"; +const ERR_MISSING_TOKEN = Error("Telegram bot api token not found!"); +const ERR_BAD_REQUEST = "Maybe wrong paramters!"; +const ERR_FORBIDDEN = "You're not allowed to do this!"; +const ERR_NOT_REGISTER = "Please use /register to register or bind account via Telegram!"; +const ERR_NOT_FOUND = "File disappeared!"; +class Web { + constructor(core) { + if (!core.config.telegram.token) + throw ERR_MISSING_TOKEN; + this.user = core.userManager; + this.audio = core.audioManager; + this.list = core.listManager; + this.digest = crypto_1.default.createHash("sha256").update(core.config.telegram.token).digest(); + this.upload = core.config.web.upload; + this.webConfig = { + tgBotName: core.config.telegram.botname, + title: core.config.web.title, + }; + this.server = express_1.default(); + this.middlewares(); + this.registerRoutes(); + this.errorHandler(); + this.server.listen(8081); + } + async middlewares() { + this.server.use(express_1.default.json()); + } + async errorHandler() { + this.server.use((err, req, res, next) => { + if (err.message.startsWith("HTTP400")) { + res.status(400).json({ + error: ERR_BAD_REQUEST + }); + } + else if (err.message.startsWith("HTTP401")) { + res.status(401).json({ + error: ERR_NOT_REGISTER + }); + } + else if (err.message.startsWith("HTTP403")) { + res.status(403).json({ + error: ERR_FORBIDDEN + }); + } + else if (err.message.startsWith("HTTP404")) { + res.status(404).json({ + error: ERR_NOT_FOUND + }); + } + else { + next(err); + } + }); + } + route(fn) { + return (req, res, next) => { + const promise = fn.bind(this)(req, res, next); + if (promise instanceof Promise) { + promise.catch(next); + } + }; + } + async registerRoutes() { + const upload = multer_1.default({ dest: this.upload }); + this.server.get("/", (req, res) => res.send("MusicBot Web Server")); + this.server.get("/config", this.route(this.getConfig)); + this.server.get("/login", this.route(this.getLogin)); + this.server.get("/lists", this.route(this.getLists)); + this.server.post("/lists", this.route(this.postLists)); + this.server.get("/list/:lid", this.route(this.getList)); + this.server.patch("/list/:lid", this.route(this.patchList)); + this.server.delete("/list/:lid", this.route(this.deleteList)); + this.server.get("/list/:lid/audios", this.route(this.getListAudios)); + this.server.post("/list/:lid/audios", upload.array("audio"), this.route(this.postListAudios)); + this.server.delete("/list/:lid/audio/:aid", this.route(this.deleteListAudio)); + this.server.get("/audio/:aid", this.route(this.getAudio)); + this.server.get("/audio/:aid/file", this.route(this.getAudioFile)); + } + async getConfig(req, res) { + res.json(this.webConfig); + } + async getLogin(req, res) { + const user = await this.checkUser(req); + res.json({ + msg: "OK", + user + }); + } + async getLists(req, res) { + const user = await this.getUser(req); + const lists = await this.list.getAll().map((list) => { + return { + id: list._id, + name: list.name, + own: !!user && user._id.equals(list.owner) + }; + }).toArray(); + const own = []; + const other = []; + lists.forEach(list => { + if (list.own) { + own.push(list); + } + else { + other.push(list); + } + }); + res.json({ + lists: [...own, ...other], + msg: "OK" + }); + } + async postLists(req, res) { + const user = await this.checkUser(req); + const name = req.body.name; + if (!name) { + throw Error("HTTP400"); + } + const list = await this.list.create(name, user._id); + res.json({ + list, + msg: "OK" + }); + } + async getList(req, res) { + const list = await this.list.get(new mongodb_1.ObjectID(req.params.lid)); + res.json({ + list, + msg: "OK" + }); + } + async patchList(req, res) { + const user = await this.checkUser(req); + const list = await this.list.get(new mongodb_1.ObjectID(req.params.lid)); + const name = req.body.name; + if (!name) { + throw Error("HTTP400"); + } + if (!list) { + throw Error("HTTP404"); + } + if (!user._id.equals(list.owner)) { + throw Error("HTTP403"); + } + const result = await this.list.rename(list._id, name); + res.json({ + msg: "OK", + result + }); + } + async deleteList(req, res) { + const user = await this.checkUser(req); + const list = await this.list.get(new mongodb_1.ObjectID(req.params.lid)); + if (!list) { + throw Error("HTTP404"); + } + if (!user._id.equals(list.owner)) { + throw Error("HTTP403"); + } + const result = await this.list.delete(list._id); + res.json({ + msg: "OK", + result + }); + } + async getListAudios(req, res) { + const list = await this.list.get(new mongodb_1.ObjectID(req.params.lid)); + if (!list) { + throw Error("HTTP404"); + } + const audios = await this.audio.search({ + _id: { + $in: list.audio.map(id => new mongodb_1.ObjectID(id)) + } + }).toArray(); + res.json({ + audios, + msg: "OK" + }); + } + async postListAudios(req, res) { + try { + const user = await this.checkUser(req); + const list = await this.list.get(new mongodb_1.ObjectID(req.params.lid)); + if (!list) { + throw Error("HTTP404"); + } + if (!user._id.equals(list.owner)) { + throw Error("HTTP403"); + } + const audios = []; + const paths = []; + if (req.files) { + req.files.forEach((file) => { + paths.push(file.path); + }); + } + if (req.body) { + const body = req.body; + if (body.uris) { + if (typeof body.uris === "string") { + paths.push(...JSON.parse(body.uris)); + } + else { + paths.push(...body.uris); + } + } + } + await Promise.all(paths.map(async (path) => { + const audio = await this.processFile(path, user); + if (audio) { + await this.list.addAudio(list._id, audio._id); + audios.push(audio); + } + else { + } + })); + res.json({ + audios, + msg: "OK" + }); + } + finally { + if (req.files) { + await Promise.all(req.files.map(async (file) => await fs_1.promises.unlink(file.path))); + } + } + } + async deleteListAudio(req, res) { + const user = await this.checkUser(req); + const list = await this.list.get(new mongodb_1.ObjectID(req.params.lid)); + if (!list) { + throw Error("HTTP404"); + } + if (!user._id.equals(list.owner)) { + throw Error("HTTP403"); + } + const result = await this.list.delAudio(list._id, new mongodb_1.ObjectID(req.params.aid)); + res.json({ + msg: "OK", + result + }); + } + async getAudio(req, res) { + const audio = await this.audio.get(new mongodb_1.ObjectID(req.params.aid)); + res.json({ + audio, + msg: "OK" + }); + } + async getAudioFile(req, res) { + const audio = await this.audio.get(new mongodb_1.ObjectID(req.params.aid)); + if (!audio) { + throw Error("HTTP404"); + } + const file = await this.audio.getFile(audio); + if (!file) { + throw Error("HTTP404"); + } + res.download(file); + } + async getUser(req) { + const tgStr = req.get("X-Auth"); + if (!tgStr) + return null; + const tg = JSON.parse(tgStr); + const hash = tg.hash; + delete tg.hash; + const payload = Object.keys(tg).sort().map(key => { + return `${key}=${tg[key]}`; + }).join("\n"); + const hmac = crypto_1.default.createHmac("sha256", this.digest); + hmac.update(payload); + if (hmac.digest("hex") !== hash) { + return null; + } + return this.user.getFromBind(exports.BIND_TYPE, tg.id); + } + async checkUser(req) { + const user = await this.getUser(req); + if (!user) { + throw Error("HTTP401"); + } + return user; + } + async processFile(file, sender) { + if (!file) + return null; + let audio; + try { + audio = await this.audio.add(sender._id, file); + } + catch (error) { + if (error === AudioManager_1.ERR_MISSING_TITLE) { + return null; + } + else if (error === AudioManager_1.ERR_NOT_AUDIO) { + return null; + } + else { + return null; + } + } + return audio; + } +} +exports.Web = Web; diff --git a/dist/index.js b/dist/index.js index 6108a04..15f0063 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6,6 +6,7 @@ const fs_1 = require("fs"); const path_1 = require("path"); const Discord_1 = require("./Component/Discord"); const Telegram_1 = require("./Component/Telegram"); +const Web_1 = require("./Component/Web"); const AudioManager_1 = require("./Core/AudioManager"); const ListManager_1 = require("./Core/ListManager"); const MongoDB_1 = require("./Core/MongoDB"); @@ -52,6 +53,12 @@ class Core extends events_1.EventEmitter { catch (error) { console.error(error); } + try { + new Web_1.Web(this); + } + catch (error) { + console.error(error); + } }); } } diff --git a/dist/web/main.js b/dist/web/main.js new file mode 100644 index 0000000..717d600 --- /dev/null +++ b/dist/web/main.js @@ -0,0 +1,376 @@ +"use strict"; +const config = { + base: `${location.pathname}api/`, + urlRegex: /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i, +}; +Vue.use(VuetifyUploadButton); +const app = new Vue({ + el: "#app", + components: { + vueTelegramLogin, + }, + data() { + return { + tg: {}, + isMobile: false, + configLoaded: false, + title: "", + tgBotName: "", + drop: { + in: false, + overlay: false, + timer: 0, + }, + upload: { + uploading: false, + total: 1, + done: 0, + current: "", + encoding: false, + file: true, + single: false, + }, + showDrawer: false, + itemActing: {}, + itemDelete: { + act: "", + cb: null, + dialog: false, + }, + listModify: { + act: "", + dialog: false, + name: "", + nameRules: [ + v => !!v || "請輸入名稱", + v => { + if (this.listModify.act === "edit") { + return this.itemActing.name !== v || "請修改名稱"; + } + else { + return true; + } + }, + ], + valid: false, + }, + lists: [], + listSelected: 0, + listsHeaders: [ + { + align: "left", + sortable: false, + text: "清單名稱", + value: "name", + }, + { + align: "center", + sortable: false, + text: "操作", + value: "act", + width: "72px", + }, + ], + audios: [], + audioSelected: 0, + audiosHeaders: [ + { + text: "歌名", + value: "title", + width: "50%", + }, + { + text: "歌手", + value: "artist", + width: "40%", + }, + { + text: "長度", + value: "duration", + width: "5%", + }, + { + text: "操作", + value: "act", + width: "5%", + }, + ], + }; + }, + created() { + this.isMobile = window.innerWidth < 769 ? true : false; + if (!this.isMobile) { + this.showDrawer = true; + } + this.loadConfig(); + this.refresh(); + }, + computed: { + loggedIn() { + return Object.keys(this.tg).length > 0; + }, + uploadTotalProgress() { + const base = this.upload.done * 100 / this.upload.total; + const seg = 100.0 / this.upload.total; + let add = 0; + if (this.upload.file && this.upload.encoding) { + add = seg; + } + else if (this.upload.single) { + add = seg * this.upload.single / 100; + } + return base + add; + }, + list() { + const list = this.lists[this.listSelected]; + return list || {}; + }, + }, + watch: { + list() { + this.getAudios(); + }, + lists(newVal, oldVal) { + const oldList = oldVal[this.listSelected]; + if (!oldList) { + return; + } + let newIndex = 0; + this.lists.forEach((list, idx) => { + if (list.id === oldList.id) { + newIndex = idx; + } + }); + this.listSelected = newIndex; + }, + }, + methods: { + onAuth(tg) { + this.tg = tg; + this.api("get", "login").then(json => { + this.refresh(); + console.log(json); + }, json => { + this.tg = {}; + }); + }, + onDragOver(evt) { + if (this.list.own && !this.upload.uploading) { + clearTimeout(this.drop.timer); + this.drop.timer = setTimeout(() => { + this.drop.in = false; + clearTimeout(this.drop.timer); + this.drop.overlay = false; + }, 100); + if (!this.drop.in) { + this.drop.in = true; + this.drop.overlay = true; + } + } + }, + onDrop(evt) { + if (this.list.own && !this.upload.uploading) { + this.audioAdd(evt.dataTransfer.items || evt.dataTransfer.files); + } + }, + onPaste(evt) { + if (this.list.own && !this.upload.uploading) { + this.audioAdd(evt.clipboardData.items); + } + }, + loadConfig() { + this.api("get", "config").then(json => { + this.title = json.title; + document.title = this.title; + this.tgBotName = json.tgBotName; + this.configLoaded = true; + }); + }, + refresh() { + this.getLists(); + }, + api(act, path, body, uploadProgress) { + const ep = `${config.base}${path}`; + const conf = { + headers: { + "X-Auth": JSON.stringify(this.tg), + }, + uploadProgress, + }; + const params = body ? [ep, body, conf] : [ep, conf]; + return new Promise((resolve, reject) => { + const handle = res => { + res.json().then(resolve, reject); + }; + this.$http[act](...params).then(handle, handle); + }); + }, + itemDeleteCall() { + if (this.itemDelete.cb) { + this.itemDelete.cb(); + } + }, + getLists() { + this.api("get", "lists").then(json => { + this.lists = json.lists.map((list, idx) => { + list.index = idx; + return list; + }); + }); + }, + listDeleteConfirm(list) { + this.itemActing = list; + this.itemDelete.dialog = true; + this.itemDelete.cb = this.listDeleteCall.bind(this); + }, + listDeleteCall() { + this.api("delete", `list/${this.itemActing.id}`).then(json => { + this.itemDelete.dialog = false; + this.getLists(); + console.log(json); + }); + }, + listNewInput() { + this.listModify.act = "new"; + this.listModify.name = ""; + this.listModify.dialog = true; + }, + listEditInput(list) { + this.itemActing = list; + this.listModify.act = "edit"; + this.listModify.name = list.name; + this.listModify.dialog = true; + }, + listModifyCall() { + if (this.$refs.listModify.validate()) { + switch (this.listModify.act) { + case "new": + this.api("post", "lists", { + name: this.listModify.name, + }).then(json => { + this.listModify.dialog = false; + this.getLists(); + console.log(json); + }); + break; + case "edit": + this.api("patch", `list/${this.itemActing.id}`, { + name: this.listModify.name, + }).then(json => { + this.listModify.dialog = false; + this.getLists(); + console.log(json); + }); + break; + } + } + }, + getAudios() { + this.api("get", `list/${this.list.id}/audios`).then(json => { + this.audios = json.audios.map((audio, idx) => { + audio.index = idx; + return audio; + }); + }); + }, + audioDeleteConfirm(audio) { + this.itemActing = audio; + this.itemDelete.dialog = true; + this.itemDelete.cb = this.audioDeleteCall.bind(this); + }, + audioDeleteCall() { + this.api("delete", `list/${this.list.id}/audio/${this.itemActing._id}`).then(json => { + this.itemDelete.dialog = false; + this.getAudios(); + console.log(json); + }); + }, + async audioAdd(files) { + this.upload.uploading = true; + const arr = []; + await Promise.all(Array.from(files).map(ele => { + if (ele instanceof DataTransferItem) { + if (ele.kind === "file") { + arr.push(ele.getAsFile()); + } + else if (ele.kind === "string") { + if (/^text\/uri-list/.exec(ele.type) || /^text\/plain/.exec(ele.type)) { + return new Promise((resolve, reject) => { + ele.getAsString((str) => { + str.split("\n").forEach(l => { + if (config.urlRegex.exec(l)) { + try { + const url = new URL(l); + arr.push(url.href); + return; + } + catch (e) { + } + } + }); + resolve(); + }); + }); + } + } + } + else if (ele instanceof File) { + arr.push(ele); + } + })); + this.upload.total = arr.length; + const consume = result => new Promise((resolve, reject) => { + const ele = arr.pop(); + if (ele) { + let data; + if (ele instanceof File) { + data = new FormData(); + data.append("audio", ele); + this.upload.current = ele.name; + this.upload.file = true; + } + else { + data = { + uris: [ele], + }; + this.upload.current = ele; + this.upload.file = false; + } + this.upload.done = this.upload.total - arr.length - 1; + const handle = json => { + this.upload.encoding = false; + result.push({ + ele, + json, + }); + resolve(consume(result)); + }; + const upload = evt => { + if (evt.lengthComputable) { + const progress = evt.loaded * 100 / evt.total; + if (progress > 99) { + this.upload.single = false; + this.upload.encoding = true; + } + else { + this.upload.single = progress; + } + } + else { + this.upload.single = false; + } + }; + this.api("post", `list/${this.list.id}/audios`, data, upload).then(handle, handle); + } + else { + resolve(result); + } + }); + consume([]).then(result => { + this.upload.uploading = false; + console.log(result); + this.getAudios(); + }); + }, + }, +}); diff --git a/index.ts b/index.ts index 624b407..0731b64 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,7 @@ import { existsSync, mkdirSync } from "fs"; import { resolve } from "path"; import { Discord } from "./Component/Discord"; import { Telegram } from "./Component/Telegram"; +import { Web } from "./Component/Web"; import { AudioManager } from "./Core/AudioManager"; import { ListManager } from "./Core/ListManager"; import { MongoDB } from "./Core/MongoDB"; @@ -60,6 +61,13 @@ export class Core extends EventEmitter { } catch (error) { console.error(error); } + + try { + // tslint:disable-next-line:no-unused-expression + new Web(this); + } catch (error) { + console.error(error); + } }); } } diff --git a/multer.d.ts b/multer.d.ts new file mode 100644 index 0000000..6bdeb62 --- /dev/null +++ b/multer.d.ts @@ -0,0 +1 @@ +declare module "multer"; diff --git a/package.json b/package.json index fc4d849..2801e0c 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,17 @@ }, "dependencies": { "eris": "0.13.3", + "express": "^4.17.1", "fluent-ffmpeg": "^2.1.2", "mongodb": "^3.6.2", + "multer": "^1.4.1", "node-telegram-bot-api": "^0.50.0", "promise-queue": "^2.2.5", "shuffle-array": "^1.0.1", "ytdl-core": "^3.2.2" }, "devDependencies": { + "@types/express": "^4.17.0", "@types/fluent-ffmpeg": "^2.1.15", "@types/mongodb": "^3.5.27", "@types/node": "^14.10.3", diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..722ec93 --- /dev/null +++ b/web/index.html @@ -0,0 +1,132 @@ + + + + + + + MusicBot + + + + + + + + + + +
+ + + 將檔案拖曳至此處以上傳至歌單 + + + + 上傳中 + + 已完成 {{ upload.done }} / 總共 {{ upload.total }} 個檔案 + + 正在{{ upload.encoding ? "處理" : "上傳"}} {{ upload.current }} + + + + + + + {{ title }} + + + + + + 播放清單 + + + add + + + + + + + + + 新增播放清單 + + + + + + + + 取消 + 確定 + + + + + + 確認刪除 + 確定要刪除{{ itemActing.name || itemActing.title }}嗎? + + + 取消 + 刪除 + + + + + + {{ list.name }} + + + + + + + + + + +
+ + + diff --git a/web/main.js b/web/main.js new file mode 100644 index 0000000..9a4adf3 --- /dev/null +++ b/web/main.js @@ -0,0 +1,371 @@ +const config = { + base: `${location.pathname}api/`, + // https://gist.github.com/dperini/729294 + urlRegex: /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i, +}; + +Vue.use(VuetifyUploadButton); + +const app = new Vue({ + el: "#app", + components: { + vueTelegramLogin, + }, + data() { + return { + tg: {}, + isMobile: false, + configLoaded: false, + title: "", + tgBotName: "", + drop: { + in: false, + overlay: false, + timer: 0, + }, + upload: { + uploading: false, + total: 1, + done: 0, + current: "", + encoding: false, + file: true, + single: false, + }, + showDrawer: false, + itemActing: {}, + itemDelete: { + act: "", + cb: null, + dialog: false, + }, + listModify: { + act: "", + dialog: false, + name: "", + nameRules: [ + v => !!v || "請輸入名稱", + v => { + if (this.listModify.act === "edit") { + return this.itemActing.name !== v || "請修改名稱"; + } else { + return true; + } + }, + ], + valid: false, + }, + lists: [], + listSelected: 0, + listsHeaders: [ + { + align: "left", + sortable: false, + text: "清單名稱", + value: "name", + }, + { + align: "center", + sortable: false, + text: "操作", + value: "act", + width: "72px", + }, + ], + audios: [], + audioSelected: 0, + audiosHeaders: [ + { + text: "歌名", + value: "title", + width: "50%", + }, + { + text: "歌手", + value: "artist", + width: "40%", + }, + { + text: "長度", + value: "duration", + width: "5%", + }, + { + text: "操作", + value: "act", + width: "5%", + }, + ], + }; + }, + created() { + this.isMobile = window.innerWidth < 769 ? true : false; + if (!this.isMobile) { this.showDrawer = true; } + this.loadConfig(); + this.refresh(); + }, + computed: { + loggedIn() { + return Object.keys(this.tg).length > 0; + }, + uploadTotalProgress() { + const base = this.upload.done * 100 / this.upload.total; + const seg = 100.0 / this.upload.total; + let add = 0; + if (this.upload.file && this.upload.encoding) { + add = seg; + } else if (this.upload.single) { + add = seg * this.upload.single / 100; + } + return base + add; + }, + list() { + const list = this.lists[this.listSelected]; + return list || {}; + }, + }, + watch: { + list() { + this.getAudios(); + }, + lists(newVal, oldVal) { + const oldList = oldVal[this.listSelected]; + if (!oldList) { return; } + let newIndex = 0; + this.lists.forEach((list, idx) => { + if (list.id === oldList.id) { + newIndex = idx; + } + }); + this.listSelected = newIndex; + }, + }, + methods: { + onAuth(tg) { + this.tg = tg; // X-Auth header for login api request + this.api("get", "login").then(json => { + this.refresh(); + console.log(json); + }, json => { + // chear tg object if login failed + this.tg = {}; + }); + }, + onDragOver(evt) { + if (this.list.own && !this.upload.uploading) { + clearTimeout(this.drop.timer); + this.drop.timer = setTimeout(() => { + this.drop.in = false; + clearTimeout(this.drop.timer); + + this.drop.overlay = false; + }, 100); + + if (!this.drop.in) { + this.drop.in = true; + + this.drop.overlay = true; + } + } + }, + onDrop(evt) { + if (this.list.own && !this.upload.uploading) { + this.audioAdd(evt.dataTransfer.items || evt.dataTransfer.files); + } + }, + onPaste(evt) { + if (this.list.own && !this.upload.uploading) { + this.audioAdd(evt.clipboardData.items); + } + }, + loadConfig() { + this.api("get", "config").then(json => { + this.title = json.title; + document.title = this.title; + this.tgBotName = json.tgBotName; + this.configLoaded = true; + }); + }, + refresh() { + this.getLists(); + }, + api(act, path, body, uploadProgress) { + const ep = `${config.base}${path}`; + const conf = { + headers: { + "X-Auth": JSON.stringify(this.tg), + }, + uploadProgress, + }; + const params = body ? [ep, body, conf] : [ep, conf]; + return new Promise((resolve, reject) => { + const handle = res => { + res.json().then(resolve, reject); + }; + this.$http[act](...params).then(handle, handle); + }); + }, + itemDeleteCall() { + if (this.itemDelete.cb) { + this.itemDelete.cb(); + } + }, + getLists() { + this.api("get", "lists").then(json => { + this.lists = json.lists.map((list, idx) => { + list.index = idx; + return list; + }); + }); + }, + listDeleteConfirm(list) { + this.itemActing = list; + this.itemDelete.dialog = true; + this.itemDelete.cb = this.listDeleteCall.bind(this); + }, + listDeleteCall() { + this.api("delete", `list/${this.itemActing.id}`).then(json => { + this.itemDelete.dialog = false; + this.getLists(); + console.log(json); + }); + }, + listNewInput() { + this.listModify.act = "new"; + this.listModify.name = ""; + this.listModify.dialog = true; + }, + listEditInput(list) { + this.itemActing = list; + this.listModify.act = "edit"; + this.listModify.name = list.name; + this.listModify.dialog = true; + }, + listModifyCall() { + if (this.$refs.listModify.validate()) { + switch (this.listModify.act) { + case "new": + this.api("post", "lists", { + name: this.listModify.name, + }).then(json => { + this.listModify.dialog = false; + this.getLists(); + console.log(json); + }); + break; + case "edit": + this.api("patch", `list/${this.itemActing.id}`, { + name: this.listModify.name, + }).then(json => { + this.listModify.dialog = false; + this.getLists(); + console.log(json); + }); + break; + } + } + }, + getAudios() { + this.api("get", `list/${this.list.id}/audios`).then(json => { + this.audios = json.audios.map((audio, idx) => { + audio.index = idx; + return audio; + }); + }); + }, + audioDeleteConfirm(audio) { + this.itemActing = audio; + this.itemDelete.dialog = true; + this.itemDelete.cb = this.audioDeleteCall.bind(this); + }, + audioDeleteCall() { + this.api("delete", `list/${this.list.id}/audio/${this.itemActing._id}`).then(json => { + this.itemDelete.dialog = false; + this.getAudios(); + console.log(json); + }); + }, + async audioAdd(files) { + this.upload.uploading = true; + const arr = []; + await Promise.all(Array.from(files).map(ele => { + if (ele instanceof DataTransferItem) { + if (ele.kind === "file") { + arr.push(ele.getAsFile()); + } else if (ele.kind === "string") { + if (/^text\/uri-list/.exec(ele.type) || /^text\/plain/.exec(ele.type)) { + return new Promise((resolve, reject) => { + ele.getAsString((str) => { + // try parse uri + str.split("\n").forEach(l => { + if (config.urlRegex.exec(l)) { + try { + const url = new URL(l); + arr.push(url.href); + return; + } catch (e) { + } + } + }); + resolve(); + }); + }); + } + } + } else if (ele instanceof File) { + arr.push(ele); + } + })); + this.upload.total = arr.length; + const consume = result => new Promise((resolve, reject) => { + const ele = arr.pop(); + if (ele) { + let data; + if (ele instanceof File) { + data = new FormData(); + data.append("audio", ele); + this.upload.current = ele.name; + this.upload.file = true; + } else { + data = { + uris: [ele], + }; + this.upload.current = ele; + this.upload.file = false; + } + // this must assign after file judgement + this.upload.done = this.upload.total - arr.length - 1; + const handle = json => { + this.upload.encoding = false; + result.push({ + ele, + json, + }); + resolve(consume(result)); + }; + const upload = evt => { + if (evt.lengthComputable) { + const progress = evt.loaded * 100 / evt.total; + if (progress > 99) { + this.upload.single = false; + this.upload.encoding = true; + } else { + this.upload.single = progress; + } + } else { + this.upload.single = false; + } + }; + this.api("post", `list/${this.list.id}/audios`, data, upload).then(handle, handle); + } else { + resolve(result); + } + }); + consume([]).then(result => { + this.upload.uploading = false; + console.log(result); + this.getAudios(); + }); + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 9459daf..c3e3358 100644 --- a/yarn.lock +++ b/yarn.lock @@ -119,6 +119,14 @@ resolved "https://registry.yarnpkg.com/@ffprobe-installer/win32-x64/-/win32-x64-4.1.0.tgz#ed3e8a329eeb6c0625ac439e31ad9ec43001b33c" integrity sha512-gPW2FZxexzCAOhGch0JFkeSSln+wcL5d1JDlJwfSJVEAShHf9MmxiWq0NpHoCSzFvK5qwl0C58KG180eKvd3mA== +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/bson@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.2.tgz#7accb85942fc39bbdb7515d4de437c04f698115f" @@ -136,6 +144,32 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.0": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/fluent-ffmpeg@^2.1.15": version "2.1.15" resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.15.tgz#af49499bc7ff7a22c56734dec16065e0e1bcc69c" @@ -143,6 +177,11 @@ dependencies: "@types/node" "*" +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + "@types/mongodb@^3.5.27": version "3.5.27" resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.5.27.tgz#158a7a43ce25ef3592ac8a62e62ab38bebf661f2" @@ -174,6 +213,16 @@ resolved "https://registry.yarnpkg.com/@types/promise-queue/-/promise-queue-2.2.0.tgz#cdba35f1b2c0bd8aa2bf925c2b1ed02958067a0a" integrity sha512-9QLtid6GxEWqpF+QImxBRG6bSVOHtpAm2kXuIyEvZBbSOupLvqhhJv8uaHbS8kUL8FDjzH3RWcSyC/52WOVtGw== +"@types/qs@*": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + "@types/request@*": version "2.48.5" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" @@ -184,6 +233,14 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/serve-static@*": + version "1.13.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" + integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + "@types/shuffle-array@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/shuffle-array/-/shuffle-array-1.0.0.tgz#ac107d83648817e6c811093e5eca7f9eae9f118d" @@ -194,6 +251,14 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + ajv@^6.12.3: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" @@ -229,6 +294,11 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -249,6 +319,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + array.prototype.findindex@^2.0.2: version "2.1.0" resolved "https://registry.yarnpkg.com/array.prototype.findindex/-/array.prototype.findindex-2.1.0.tgz#f43f8ed823274f0733647ee403b2c0c9771a97fe" @@ -329,6 +404,22 @@ bluebird@^3.5.0, bluebird@^3.5.1: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -375,6 +466,11 @@ buffer-fill@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + bufferutil@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7" @@ -387,6 +483,19 @@ builtin-modules@^1.1.1: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -475,11 +584,43 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -503,6 +644,13 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@^3.1.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -544,16 +692,29 @@ denque@^1.4.1: resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== -depd@^1.1.1: +depd@^1.1.1, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -567,6 +728,16 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -642,6 +813,11 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -652,6 +828,11 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + eventemitter3@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" @@ -667,6 +848,42 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -702,6 +919,19 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + find-up@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -748,6 +978,16 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -846,6 +1086,28 @@ html-entities@^1.3.1: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -871,6 +1133,13 @@ husky@^4.3.0: slash "^3.0.0" which-pm-runs "^1.0.0" +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" @@ -887,16 +1156,26 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -948,6 +1227,11 @@ is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -1036,24 +1320,39 @@ m3u8stream@^0.7.1: miniget "^1.6.1" sax "^1.2.4" +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: mime-db "1.44.0" -mime@^1.6.0: +mime@1.6.0, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -1105,11 +1404,35 @@ mongodb@^3.6.2: optionalDependencies: saslprep "^1.0.0" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + ms@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +multer@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.1" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + nan@^2.14.0: version "2.14.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" @@ -1120,6 +1443,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + node-abi@^2.7.0: version "2.19.1" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.1.tgz#6aa32561d0a5e2fdb6810d8c25641b657a8cea85" @@ -1179,7 +1507,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -1204,6 +1532,13 @@ object.assign@^4.1.0: has-symbols "^1.0.1" object-keys "^1.1.1" +on-finished@^2.3.0, on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1262,6 +1597,11 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -1277,6 +1617,11 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -1333,6 +1678,14 @@ promise-queue@^2.2.5: resolved "https://registry.yarnpkg.com/promise-queue/-/promise-queue-2.2.5.tgz#2f6f5f7c0f6d08109e967659c79b88a9ed5e93b4" integrity sha1-L29ffA9tCBCelnZZx5uIqe1ek7Q= +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -1359,11 +1712,31 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -1374,7 +1747,17 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5: +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -1455,17 +1838,17 @@ resolve@^1.3.2: dependencies: path-parse "^1.0.6" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -1497,11 +1880,45 @@ semver@^5.1.0, semver@^5.3.0, semver@^5.4.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + shuffle-array@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/shuffle-array/-/shuffle-array-1.0.1.tgz#c4ff3cfe74d16f93730592301b25e6577b12898b" @@ -1566,11 +1983,21 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -1604,6 +2031,11 @@ string.prototype.trimstart@^1.0.1: define-properties "^1.1.3" es-abstract "^1.17.5" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -1672,6 +2104,11 @@ to-buffer@^1.1.1: resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -1728,11 +2165,29 @@ tweetnacl@^1.0.1: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== +type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + typescript@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + uri-js@^4.2.2: version "4.4.0" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" @@ -1745,11 +2200,21 @@ util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"