From 53f79ff865c9e02d1ddddd620a14a01c32ce6ea9 Mon Sep 17 00:00:00 2001 From: Leandro Di Lorenzo Date: Thu, 11 Jan 2024 17:29:42 -0300 Subject: [PATCH] Transcription (#32) * refactor: some improves * build; add nodemon and remove y18n * chre: remove locales * feat: adding whisper transcription * refactor: moving code into controller * refactor: moving code * refactor: extracting types * feature: add middleware to let pass only authorized channels * refactor: some improves * refactor: min improves * chore: dummy change * a try * revert * trying something * trying++ * trying+2 * ?? * trying bla * bla2 --- .env.example | 2 + .eslintrc.json | 12 ++- .scripts/start-dev.sh | 2 +- Dockerfile | 1 - locales/en.json | 3 - locales/es.json | 3 - package.json | 4 +- src/bot.ts | 47 ++++++----- src/config.ts | 2 + src/controllers/AIController.ts | 51 ------------ src/controllers/BaseController.ts | 7 ++ src/controllers/GPT/GPTController.ts | 53 ++++++++++++ src/controllers/GPT/types.ts | 9 +++ src/controllers/StickersController.ts | 25 +++--- src/controllers/types.ts | 4 + src/core/GPT.ts | 65 +++++++++++++++ src/{features => core}/holidays.ts | 0 src/{features => core}/schedule.ts | 0 src/{features => core}/stickers.ts | 0 src/crontab.ts | 4 +- src/features/AI.ts | 26 ------ src/index.ts | 4 + src/lib/api.ts | 4 +- src/middlewares/GPTMiddleware.ts | 16 ++++ src/router.ts | 2 +- tsconfig.eslint.json | 2 +- yarn.lock | 111 +++++++++++++++++++++++--- 27 files changed, 318 insertions(+), 141 deletions(-) delete mode 100644 locales/en.json delete mode 100644 locales/es.json delete mode 100644 src/controllers/AIController.ts create mode 100644 src/controllers/BaseController.ts create mode 100644 src/controllers/GPT/GPTController.ts create mode 100644 src/controllers/GPT/types.ts create mode 100644 src/controllers/types.ts create mode 100644 src/core/GPT.ts rename src/{features => core}/holidays.ts (100%) rename src/{features => core}/schedule.ts (100%) rename src/{features => core}/stickers.ts (100%) delete mode 100644 src/features/AI.ts create mode 100644 src/middlewares/GPTMiddleware.ts diff --git a/.env.example b/.env.example index 4adba73..b9b1417 100644 --- a/.env.example +++ b/.env.example @@ -9,7 +9,9 @@ LOG_CONSOLE_LEVEL=debug EXPRESS_PORT=3000 # espiDev +OWNER_CHANNEL=123456 MAIN_CHANNEL=454545 +AI_CHANNELS='123456,454545' BOT_TOKEN=12345:ABCDEF BOT_DOMAIN=https://espi.localhost OPENAI_API_KEY=qw-1234567 diff --git a/.eslintrc.json b/.eslintrc.json index 330b690..5ecdf37 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,7 +9,8 @@ ], "env": { "node": true, - "es2021": true + "es2016": true, + "jest": true }, "parserOptions": { "project": "./tsconfig.eslint.json", @@ -23,8 +24,11 @@ }, "import/resolver": { "node": { - "extensions": [".js", ".d.ts", ".ts"], - "moduleDirectory": ["src", "node_modules"] + "extensions": [".d.ts", ".ts"] + }, + "typescript": { + "alwaysTryTypes": true, + "project": "./tsconfig.eslint.json" } } }, @@ -58,6 +62,6 @@ } } ], - "import/no-unresolved": ["error", { "commonjs": true }] + "import/no-unresolved": "error" } } diff --git a/.scripts/start-dev.sh b/.scripts/start-dev.sh index abdf1ab..562d7ad 100644 --- a/.scripts/start-dev.sh +++ b/.scripts/start-dev.sh @@ -13,4 +13,4 @@ echo "API_DOMAIN: $API_DOMAIN" echo "BOT_DOMAIN: $BOT_DOMAIN" # Start the bot -ts-node src/index.ts +nodemon --watch 'src/**/*.ts' --exec 'ts-node' ./src/index.ts diff --git a/Dockerfile b/Dockerfile index 588174c..e4e93dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,6 @@ WORKDIR /app COPY package.json yarn.lock /app/ COPY tsconfig.json /app/ -COPY locales/ /app/locales/ COPY src/ /app/src/ RUN yarn install --frozen-lockfile diff --git a/locales/en.json b/locales/en.json deleted file mode 100644 index 0f79ecc..0000000 --- a/locales/en.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hi": "Hello" -} diff --git a/locales/es.json b/locales/es.json deleted file mode 100644 index 523c072..0000000 --- a/locales/es.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "hi": "Hola" -} diff --git a/package.json b/package.json index 9cb37af..92792d1 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,7 @@ "morgan": "^1.10.0", "openai": "4.24.1", "telegraf": "^4.15.3", - "winston": "^3.11.0", - "y18n": "^5.0.8" + "winston": "^3.11.0" }, "devDependencies": { "@babel/core": "^7.23.7", @@ -61,6 +60,7 @@ "husky": "^8.0.3", "jest": "^29.7.0", "lint-staged": "15.2.0", + "nodemon": "3.0.2", "prettier": "3.1.1", "release-it": "^17.0.1", "ts-jest": "^29.1.1", diff --git a/src/bot.ts b/src/bot.ts index 5c09031..2cd1351 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,15 +1,11 @@ import { Telegraf } from 'telegraf'; +import { message } from 'telegraf/filters'; -import AIController from './controllers/AIController'; +import GPTController from './controllers/GPT/GPTController'; import StickersController from './controllers/StickersController'; -import Holidays from './features/holidays'; +import Holidays from './core/holidays'; import logger from './lib/logger'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const y18n = require('y18n'); - -// eslint-disable-next-line @typescript-eslint/naming-convention -const { __ } = y18n({ locale: 'es' }); +import GPTMiddleware from './middlewares/GPTMiddleware'; if (!process.env.BOT_TOKEN) { logger.error('You have to define BOT_TOKEN env var'); @@ -19,28 +15,37 @@ if (!process.env.BOT_TOKEN) { const bot = new Telegraf(process.env.BOT_TOKEN); -// Handler for /start command. +// Handler for /start command bot.start(ctx => ctx.reply('Welcome!')); -// Handler for /help command. +// Handler for /help command bot.help(ctx => ctx.reply('Send me a sticker')); -// Registers middleware for provided update type. -// bot.on('sticker', ctx => ctx.reply('👍')); - -// Registers middleware for handling text messages. -bot.hears('ping', ctx => ctx.reply('ACK')); -bot.hears('hi', ctx => ctx.reply(__`hi`)); - -// Version +// Basic and Info +bot.hears(/^ping$/i, ctx => ctx.reply('ACK')); +bot.hears(/(hi|hola)/, ctx => ctx.reply('👋')); bot.hears(/^espi +version/i, ctx => ctx.reply(`Soy ${ctx.botInfo?.username}@${process.env.npm_package_version}`)); - -// Espi commands +bot.hears(/^espi +id/i, ctx => { + const name = ctx.chat.type === 'private' ? `${ctx.chat.first_name} (@${ctx.chat.username})` : ctx.chat.title; + ctx.reply(` +- Nombre: ${name} +- ID: ${ctx.chat.id} (${ctx.chat.type}) +`); +}); + +// Espi Featuring bot.hears(/^espi +feriados/i, Holidays.holidaysAR); bot.hears(/^espi +(férié|ferie)/i, Holidays.holidaysCA); bot.hears(/^espi +(finde +largo|fl)/i, Holidays.nextLongWeekendAR); bot.hears(/^espi +(findes +largos|ffll)/i, Holidays.nextThreeLongWeekendsAR); -bot.hears(AIController.shouldRespond, AIController.handleQuestion); +bot.hears(/^espi +(?.+)/i, GPTMiddleware.authorizedChannel, GPTController.handleQuestion); + +// Audio +bot.on( + message('voice'), + (ctx, next) => GPTMiddleware.authorizedChannel(ctx, next), + ctx => GPTController.transcriptAudio(ctx) +); // Reply With Stickers bot.hears(/\bfacuuu\b/i, StickersController.replyWithMaybeFacu); diff --git a/src/config.ts b/src/config.ts index 687e60e..514642a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,8 @@ export const apisCA = { export const mainChannel = toNumber(process.env.MAIN_CHANNEL || '-1'); +export const ownerChannel = toNumber(process.env.OWNER_CHANNEL || '-1'); + export const aiChannels = (process.env.AI_CHANNELS ?? `${mainChannel}`).split(',').map(toNumber) || []; export const espiId = toNumber(process.env.BOT_TOKEN?.split(':')[0] || '-1'); diff --git a/src/controllers/AIController.ts b/src/controllers/AIController.ts deleted file mode 100644 index f337735..0000000 --- a/src/controllers/AIController.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ChatCompletionMessageParam } from 'openai/resources'; -import { Context, NarrowedContext } from 'telegraf'; -import { Message, Update } from 'telegraf/typings/core/types/typegram'; - -import { aiChannels, espiId } from '../config'; -import AI from '../features/AI'; - -type ShouldRespondContext = NarrowedContext< - Context & { - match: RegExpExecArray; - }, - { - message: Update.New & Update.NonChannel & Message.TextMessage; - update_id: number; - } ->; - -const validChannel = (channelId: number) => aiChannels.includes(channelId); - -/** - * Check is should respond to a incoming message - */ -const shouldRespond = (value: string, ctx: Context) => { - const update = ctx.update as Update.MessageUpdate; - if (!validChannel(update.message.chat.id)) return null; - return /^espi +(?.+)/i.exec(value); -}; - -/** - * Handle incoming question asking to ChatGPT API - */ -const handleQuestion = async (ctx: ShouldRespondContext) => { - const question = (ctx.match?.groups?.question || '').trim(); - if (question.length < 7) { - ctx.reply('Muy cortito amigo'); - return; - } - - const reply = ctx.message.reply_to_message as Message.TextMessage; - const contextText = reply?.text || ''; - const contextRole = reply?.from?.id === espiId ? 'system' : 'user'; - const context: ChatCompletionMessageParam | undefined = contextText - ? { role: contextRole, content: contextText } - : undefined; - const answer = await AI.ask(question, context); - ctx.reply(answer); -}; - -const AIController = { shouldRespond, handleQuestion }; - -export default AIController; diff --git a/src/controllers/BaseController.ts b/src/controllers/BaseController.ts new file mode 100644 index 0000000..1e03c04 --- /dev/null +++ b/src/controllers/BaseController.ts @@ -0,0 +1,7 @@ +import { BaseContext } from './types'; + +export default class BaseController { + static showTypingAction(ctx: BaseContext) { + ctx.telegram.sendChatAction(ctx.chat.id, 'typing'); + } +} diff --git a/src/controllers/GPT/GPTController.ts b/src/controllers/GPT/GPTController.ts new file mode 100644 index 0000000..8f9f684 --- /dev/null +++ b/src/controllers/GPT/GPTController.ts @@ -0,0 +1,53 @@ +import { ChatCompletionMessageParam } from 'openai/resources'; +import { Message } from 'telegraf/typings/core/types/typegram'; + +import { TextMatchedContext, TranscriptContext } from './types'; +import { espiId } from '../../config'; +import GPT from '../../core/GPT'; +import api from '../../lib/api'; +import BaseController from '../BaseController'; + +export default class GPTController extends BaseController { + private static maxAudioSize = 10 * 1024 * 1024; // 10MB + + /** + * Handle incoming question asking to ChatGPT API + */ + static async handleQuestion(ctx: TextMatchedContext) { + this.showTypingAction(ctx); + const question = (ctx.match?.groups?.question || '').trim(); + if (question.length < 7) { + ctx.reply('Muy cortito amigo'); + return; + } + + const reply = ctx.message.reply_to_message as Message.TextMessage; + const contextText = reply?.text || ''; + const contextRole = reply?.from?.id === espiId ? 'system' : 'user'; + const context: ChatCompletionMessageParam | undefined = contextText + ? { role: contextRole, content: contextText } + : undefined; + const answer = await GPT.ask(question, context); + ctx.reply(answer); + } + + /** + * Transcript audio using ChatGPT API + */ + static async transcriptAudio(ctx: TranscriptContext) { + this.showTypingAction(ctx); + const voice = ctx.message?.voice; + const link = await ctx.telegram.getFileLink(voice.file_id); + const { data: buffer } = await api.get(link.href, { responseType: 'arraybuffer' }); + + // If audio is too big, don't transcribe it and return + if (buffer.length > this.maxAudioSize) { + ctx.reply('El audio es muy grande, no puedo transcribirlo'); + return; + } + + // Transcript audio and reply with it + const transcription = await GPT.transcript(buffer, ctx.message?.message_id); + ctx.reply(transcription); + } +} diff --git a/src/controllers/GPT/types.ts b/src/controllers/GPT/types.ts new file mode 100644 index 0000000..a796079 --- /dev/null +++ b/src/controllers/GPT/types.ts @@ -0,0 +1,9 @@ +import { Context, NarrowedContext } from 'telegraf'; +import { Message, Update } from 'telegraf/typings/core/types/typegram'; + +export type TextMatchedContext = NarrowedContext< + Context & { match: RegExpExecArray }, + Update.MessageUpdate +>; + +export type TranscriptContext = NarrowedContext>; diff --git a/src/controllers/StickersController.ts b/src/controllers/StickersController.ts index 5dc37fb..c451c41 100644 --- a/src/controllers/StickersController.ts +++ b/src/controllers/StickersController.ts @@ -1,29 +1,28 @@ import { Context } from 'telegraf'; import { Update } from 'telegraf/typings/core/types/typegram'; -import stickers from '../features/stickers'; +import BaseController from './BaseController'; +import stickers from '../core/stickers'; -const StickersController = { - replyWithSticker: async (ctx: Context, stickerPromise: Promise) => { +export default class StickersController extends BaseController { + static async replyWithSticker(ctx: Context, stickerPromise: Promise) { const sticker = await stickerPromise; if (!sticker) { await ctx.reply('Acá debería venir un sticker pero no lo encontré :('); } await ctx.replyWithSticker(sticker); - }, + } - replyWithMaybeFacu: async (ctx: Context) => { + static async replyWithMaybeFacu(ctx: Context) { await StickersController.replyWithSticker(ctx, stickers.maybeFacu()); - }, + } - replyWithPaintedDog: async (ctx: Context) => { + static async replyWithPaintedDog(ctx: Context) { await StickersController.replyWithSticker(ctx, stickers.paintedDog()); - }, + } - replyWithPatternMatchingDan: async (ctx: Context) => { + static async replyWithPatternMatchingDan(ctx: Context) { await StickersController.replyWithSticker(ctx, stickers.patternMatchingDan()); - }, -}; - -export default StickersController; + } +} diff --git a/src/controllers/types.ts b/src/controllers/types.ts new file mode 100644 index 0000000..aa826d4 --- /dev/null +++ b/src/controllers/types.ts @@ -0,0 +1,4 @@ +import { Context, NarrowedContext } from 'telegraf'; +import { Update } from 'telegraf/typings/core/types/typegram'; + +export type BaseContext = NarrowedContext; diff --git a/src/core/GPT.ts b/src/core/GPT.ts new file mode 100644 index 0000000..341c263 --- /dev/null +++ b/src/core/GPT.ts @@ -0,0 +1,65 @@ +import fs from 'fs'; + +import OpenAI from 'openai'; +import { ChatCompletionMessageParam } from 'openai/resources'; + +import { openAI } from '../config'; +import logger from '../lib/logger'; + +/** + * Wrapper for OpenAI API + * @see https://platform.openai.com/docs/api-reference + */ +const openai = new OpenAI({ apiKey: openAI.apiKey }); + +/** + * Wrapper class for ChatGPT API + */ +export default class GPT { + /** + * Ask a question to ChatGPT API + * @param question Question to ask + * @param context Context to provide to the API + * @returns Answer from the API + */ + static async ask(question: string, context?: ChatCompletionMessageParam) { + const messages: ChatCompletionMessageParam[] = [ + { role: 'system', content: 'Sus un bot de Telegram llamado Espi. Estás en el grupo "12 Cactus".' }, + ]; + if (context) messages.push(context); + messages.push({ role: 'user', content: question }); + + const response = await openai.chat.completions.create({ + ...openAI.defaultOptions, + messages, + }); + + const answer = response.choices[0].message?.content || ''; + return answer || 'Ups... no se que decirte'; + } + + /** + * Transcribe an audio file + * @param buffer Audio file buffer + * @param messageId Message ID to use as filename + * @returns Transcription + */ + static async transcript(buffer: Uint8Array, messageId = 42) { + const filename = `./voice-${messageId}.mp3`; + try { + await fs.promises.writeFile(filename, buffer); + const file = fs.createReadStream(filename); + const transcript = await openai.audio.transcriptions.create({ + file, + model: 'whisper-1', + language: 'es', + }); + return transcript.text; + } catch (error: any) { + logger.error(error, { filename }); + return 'Hubo un problema desconocido al transcribir el audio...'; + } finally { + await fs.promises.rm(filename, { force: true }); + } + } +} diff --git a/src/features/holidays.ts b/src/core/holidays.ts similarity index 100% rename from src/features/holidays.ts rename to src/core/holidays.ts diff --git a/src/features/schedule.ts b/src/core/schedule.ts similarity index 100% rename from src/features/schedule.ts rename to src/core/schedule.ts diff --git a/src/features/stickers.ts b/src/core/stickers.ts similarity index 100% rename from src/features/stickers.ts rename to src/core/stickers.ts diff --git a/src/crontab.ts b/src/crontab.ts index f201039..69fdccf 100644 --- a/src/crontab.ts +++ b/src/crontab.ts @@ -2,8 +2,8 @@ import { CronJob } from 'cron'; import dayjs, { Dayjs } from 'dayjs'; import bot from './bot'; -import Holidays from './features/holidays'; -import Schedule from './features/schedule'; +import Holidays from './core/holidays'; +import Schedule from './core/schedule'; const Job = (cron: string, triggerFn: () => void) => new CronJob( diff --git a/src/features/AI.ts b/src/features/AI.ts deleted file mode 100644 index 699fcf6..0000000 --- a/src/features/AI.ts +++ /dev/null @@ -1,26 +0,0 @@ -import OpenAI from 'openai'; -import { ChatCompletionMessageParam } from 'openai/resources'; - -import { openAI } from '../config'; - -const openai = new OpenAI({ apiKey: openAI.apiKey }); - -const ask = async (question: string, context?: ChatCompletionMessageParam) => { - const messages: ChatCompletionMessageParam[] = [ - { role: 'system', content: 'Sus un bot de Telegram llamado Espi. Estás en el grupo "12 Cactus".' }, - ]; - if (context) messages.push(context); - messages.push({ role: 'user', content: question }); - - const response = await openai.chat.completions.create({ - ...openAI.defaultOptions, - messages, - }); - - const answer = response.choices[0].message?.content || ''; - return answer || 'Ups... no se que decirte'; -}; - -const AI = { ask }; - -export default AI; diff --git a/src/index.ts b/src/index.ts index c8f3af8..1e4d2b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ import dotenv from 'dotenv'; dotenv.config(); import app from './app'; +import bot from './bot'; +import { ownerChannel } from './config'; import crontab from './crontab'; import logger from './lib/logger'; @@ -14,6 +16,8 @@ const { EXPRESS_PORT = 3000 } = process.env; app.listen(EXPRESS_PORT, () => { // start cron jobs crontab.schedule.start(); + bot.telegram.sendMessage(ownerChannel, '🌵 Espi started'); logger.info('✔ Crontab started'); + logger.info('✔ Bot launched'); logger.info(`✔ Running on ${EXPRESS_PORT}`); }); diff --git a/src/lib/api.ts b/src/lib/api.ts index c633160..f100c1a 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,10 +1,10 @@ import axios from 'axios'; -const instance = axios.create({ +const api = axios.create({ baseURL: process.env.API_DOMAIN || process.env.BOT_DOMAIN, headers: { 'Content-type': 'application/json', }, }); -export default instance; +export default api; diff --git a/src/middlewares/GPTMiddleware.ts b/src/middlewares/GPTMiddleware.ts new file mode 100644 index 0000000..f25d5c2 --- /dev/null +++ b/src/middlewares/GPTMiddleware.ts @@ -0,0 +1,16 @@ +import { Context, NarrowedContext } from 'telegraf'; +import { Update } from 'telegraf/typings/core/types/typegram'; + +import { aiChannels } from '../config'; + +type GPTMiddlewareContext = NarrowedContext, Update.MessageUpdate>; + +export default class GPTMiddleware { + static authorizedChannel(ctx: GPTMiddlewareContext, next: () => Promise) { + const isValidChannel = aiChannels.includes(ctx.chat.id); + if (!isValidChannel) return; + + // All good, continue + return next(); + } +} diff --git a/src/router.ts b/src/router.ts index e0cd328..bf507ae 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,8 +1,8 @@ import express, { NextFunction, Request, Response } from 'express'; import bot from './bot'; +import Holidays from './core/holidays'; import BadRequestResponse from './exceptions/BadRequestResponse'; -import Holidays from './features/holidays'; import { version } from '../package.json'; const router = express.Router(); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 243be40..60d07af 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,5 +1,5 @@ { "extends": "./tsconfig.json", - "include": ["src/**/*.ts", "./__tests__/**/*.ts"], + "include": ["src/**/*.ts", "__tests__/**/*.ts", "node_modules/@types"], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index 7620915..d43c9f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1171,6 +1171,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -1280,7 +1285,7 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@^3.0.3: +anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -1536,6 +1541,11 @@ big-integer@^1.6.44: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -1608,7 +1618,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1779,6 +1789,21 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== +chokidar@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + ci-info@^3.2.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" @@ -2055,7 +2080,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2952,7 +2977,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3057,7 +3082,7 @@ git-url-parse@13.1.1: dependencies: git-up "^7.0.0" -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -3319,6 +3344,11 @@ ieee754@^1.1.13, ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + ignore@^5.2.0, ignore@^5.2.4: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" @@ -3457,6 +3487,13 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -3533,7 +3570,7 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -4611,7 +4648,30 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -normalize-path@^3.0.0: +nodemon@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.0.2.tgz#222dd0de79fc7b7b3eedba422d2b9e5fc678621e" + integrity sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA== + dependencies: + chokidar "^3.5.2" + debug "^4" + ignore-by-default "^1.0.1" + minimatch "^3.1.2" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== + dependencies: + abbrev "1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -4976,7 +5036,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -5074,6 +5134,11 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -5147,6 +5212,13 @@ readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -5497,6 +5569,13 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -5726,7 +5805,7 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -5824,6 +5903,13 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -5993,6 +6079,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" @@ -6272,7 +6363,7 @@ xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9" integrity sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ== -y18n@^5.0.5, y18n@^5.0.8: +y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==