From 99eefc7f948b926bc2eed039d0949546124a55ed Mon Sep 17 00:00:00 2001 From: marylorian Date: Thu, 6 Jul 2023 01:36:32 +0200 Subject: [PATCH 01/11] issue(2): initial chatgpt prompt + parsing logic --- .env.example | 2 + .env.testing | 2 + README.md | 4 + docker-compose.yml | 2 + package-lock.json | 1523 +++++++++-------- package.json | 4 +- src/launchApplication.ts | 10 +- src/schemas/vacancy.ts | 1 + src/services/ai/index.ts | 12 + .../message-preview/sendMessagePreview.ts | 29 +- src/services/parse-message/index.ts | 0 .../parse-message/parseVacancyWithAI.ts | 159 ++ .../parse-message/processIncomingMessage.ts | 53 + .../subscribeToTextMessage.ts | 40 +- src/types/environment.d.ts | 2 + src/types/vacancy.ts | 1 + src/utils/buildMessageFromVacancy.ts | 99 +- src/utils/config.ts | 4 + src/utils/parseMessageEntities.ts | 40 + 19 files changed, 1195 insertions(+), 792 deletions(-) create mode 100644 src/services/ai/index.ts create mode 100644 src/services/parse-message/index.ts create mode 100644 src/services/parse-message/parseVacancyWithAI.ts create mode 100644 src/services/parse-message/processIncomingMessage.ts create mode 100644 src/utils/parseMessageEntities.ts diff --git a/.env.example b/.env.example index 0bfceb5..5baa28a 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,8 @@ DB_URL= NODE_ENV= BOT_TOKEN= BOT_CONSULTANT_USERNAME= +OPENAI_ORGANIZATION_ID= +OPENAI_API_KEY= # contacts list of Tg chat IDs to publish vacancies to BOT_CONTACTS=, diff --git a/.env.testing b/.env.testing index efcb3b1..b6b1bb2 100644 --- a/.env.testing +++ b/.env.testing @@ -3,6 +3,8 @@ APP_PORT=3000 BOT_TOKEN=test-token:123 BOT_CONTACTS=chat1,chat2,chat3 BOT_CONSULTANT_USERNAME=test_username +OPENAI_ORGANIZATION_ID="test-org" +OPENAI_API_KEY="test-api-key" MIN_PUBLISH_INTERVAL=2 PUBLISH_INTERVAL=1 diff --git a/README.md b/README.md index 335c3cd..a3d47f3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ BOT_CONSULTANT_USERNAME= DB_URL= DB_SSL_ENABLED= # true, by default + OPENAI_ORGANIZATION_ID= + OPENAI_API_KEY= MIN_PUBLISH_INTERVAL= # the smallest interval between vacancy publishing, by default 2 PUBLISH_INTERVAL= # interval between vacancy publishing, by default 5 @@ -24,6 +26,8 @@ PUBLISH_CONFIG='{ "mon": [10,18], "tue": [10,18], "wed": [10,18], "thu": [10,18], "fri": [10,18] }' # daily publish config in format JSON<{ "week day": [from hours, to hours] }> ``` +4. Start working with `npm run dev` for local development and `npm run start` for global work + ### Getting started with docker-compose You need docker-compose version 1.29 diff --git a/docker-compose.yml b/docker-compose.yml index 8de55a3..fa22e0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: - NODE_ENV=${NODE_ENV} - BOT_TOKEN=${BOT_TOKEN} - BOT_CONSULTANT_USERNAME=${BOT_CONSULTANT_USERNAME} + - OPENAI_ORGANIZATION_ID=${OPENAI_ORGANIZATION_ID} + - OPENAI_API_KEY=${OPENAI_API_KEY} - MIN_PUBLISH_INTERVAL=${MIN_PUBLISH_INTERVAL} - PUBLISH_INTERVAL=${PUBLISH_INTERVAL} - USER_MONTH_VACANCY_LIMIT=${USER_MONTH_VACANCY_LIMIT} diff --git a/package-lock.json b/package-lock.json index 84645e1..eb91f52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "GPL-3.0", "dependencies": { "dotenv": "^16.0.3", + "openai": "^3.3.0", "pg": "^8.11.0", "pg-hstore": "^2.3.4", "sequelize": "^6.32.0", @@ -35,6 +36,15 @@ "typescript": "^5.0.4" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -61,35 +71,35 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", + "@babel/traverse": "^7.22.6", "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.2" }, "engines": { "node": ">=6.9.0" @@ -115,9 +125,9 @@ } }, "node_modules/@babel/core/node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.5", @@ -125,8 +135,8 @@ "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", "@babel/types": "^7.22.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -189,15 +199,6 @@ "node": ">=4" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { "version": "7.17.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", @@ -213,16 +214,16 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.5", + "@babel/compat-data": "^7.22.6", "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" }, "engines": { "node": ">=6.9.0" @@ -240,15 +241,6 @@ "yallist": "^3.0.2" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -482,9 +474,9 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { "@babel/types": "^7.22.5" @@ -535,13 +527,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "dev": true, "dependencies": { "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", + "@babel/traverse": "^7.22.6", "@babel/types": "^7.22.5" }, "engines": { @@ -564,9 +556,9 @@ } }, "node_modules/@babel/helpers/node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.5", @@ -574,8 +566,8 @@ "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", "@babel/types": "^7.22.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -718,9 +710,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", + "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1211,16 +1203,16 @@ } }, "node_modules/@jest/console": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", - "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.0.tgz", + "integrity": "sha512-anb6L1yg7uPQpytNVA5skRaXy3BmrsU8icRhTVNbWdjYWDDfy8M1Kq5HIVRpYoABdbpqsc5Dr+jtu4+qWRQBiQ==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-message-util": "^29.6.0", + "jest-util": "^29.6.0", "slash": "^3.0.0" }, "engines": { @@ -1259,16 +1251,16 @@ } }, "node_modules/@jest/core": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", - "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.0.tgz", + "integrity": "sha512-5dbMHfY/5R9m8NbgmB3JlxQqooZ/ooPSOiwEQZZ+HODwJTbIu37seVcZNBK29aMdXtjvTRB3f6LCvkKq+r8uQA==", "dev": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/reporters": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.6.0", + "@jest/reporters": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", @@ -1276,20 +1268,20 @@ "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.5.0", - "jest-config": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", + "jest-config": "^29.6.0", + "jest-haste-map": "^29.6.0", + "jest-message-util": "^29.6.0", "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-resolve-dependencies": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "jest-watcher": "^29.5.0", + "jest-resolve": "^29.6.0", + "jest-resolve-dependencies": "^29.6.0", + "jest-runner": "^29.6.0", + "jest-runtime": "^29.6.0", + "jest-snapshot": "^29.6.0", + "jest-util": "^29.6.0", + "jest-validate": "^29.6.0", + "jest-watcher": "^29.6.0", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.6.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -1358,37 +1350,37 @@ } }, "node_modules/@jest/environment": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", - "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.0.tgz", + "integrity": "sha512-bUZLYUxYlUIsslBbxII0fq0kr1+friI3Gty+cRLmocGB1jdcAHs7FS8QdCDqedE8q4DZE1g/AJHH6OJZBLGGsg==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/fake-timers": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", - "jest-mock": "^29.5.0" + "jest-mock": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.0.tgz", + "integrity": "sha512-a7pISPW28Q3c0/pLwz4mQ6tbAI+hc8/0CJp9ix6e9U4dQ6TiHQX82CT5DV5BMWaw8bFH4E6zsfZxXdn6Ka23Bw==", "dev": true, "dependencies": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" + "expect": "^29.6.0", + "jest-snapshot": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.0.tgz", + "integrity": "sha512-LLSQQN7oypMSETKoPWpsWYVKJd9LQWmSDDAc4hUQ4JocVC7LAMy9R3ZMhlnLwbcFvQORZnZR7HM893Px6cJhvA==", "dev": true, "dependencies": { "jest-get-type": "^29.4.3" @@ -1398,49 +1390,49 @@ } }, "node_modules/@jest/fake-timers": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", - "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.0.tgz", + "integrity": "sha512-nuCU46AsZoskthWSDS2Aj6LARgyNcp5Fjx2qxsO/fPl1Wp1CJ+dBDqs0OkEcJK8FBeV/MbjH5efe79M2sHcV+A==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-message-util": "^29.6.0", + "jest-mock": "^29.6.0", + "jest-util": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", - "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.0.tgz", + "integrity": "sha512-IQQ3hZ2D/hwEwXSMv5GbfhzdH0nTQR3KPYxnuW6gYWbd6+7/zgMz7Okn6EgBbNtJNONq03k5EKA6HqGyzRbpeg==", "dev": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" + "@jest/environment": "^29.6.0", + "@jest/expect": "^29.6.0", + "@jest/types": "^29.6.0", + "jest-mock": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", - "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.0.tgz", + "integrity": "sha512-dWEq4HI0VvHcAD6XTtyBKKARLytyyWPIy1SvGOcU91106MfvHPdxZgupFwVHd8TFpZPpA3SebYjtwS5BUS76Rw==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/console": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", @@ -1452,9 +1444,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-message-util": "^29.6.0", + "jest-util": "^29.6.0", + "jest-worker": "^29.6.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -1550,24 +1542,24 @@ } }, "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", + "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/source-map": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", - "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.0.tgz", + "integrity": "sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.15", + "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" }, @@ -1601,13 +1593,13 @@ } }, "node_modules/@jest/test-result": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", - "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.0.tgz", + "integrity": "sha512-9qLb7xITeyWhM4yatn2muqfomuoCTOhv0QV9i7XiIyYi3QLfnvPv5NeJp5u0PZeutAOROMLKakOkmoAisOr3YQ==", "dev": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.6.0", + "@jest/types": "^29.6.0", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -1616,14 +1608,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", - "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.0.tgz", + "integrity": "sha512-HYCS3LKRQotKWj2mnA3AN13PPevYZu8MJKm12lzYojpJNnn6kI/3PWmr1At/e3tUu+FHQDiOyaDVuR4EV3ezBw==", "dev": true, "dependencies": { - "@jest/test-result": "^29.5.0", + "@jest/test-result": "^29.6.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.6.0", "slash": "^3.0.0" }, "engines": { @@ -1631,22 +1623,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", - "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.0.tgz", + "integrity": "sha512-bhP/KxPo3e322FJ0nKAcb6WVK76ZYyQd1lWygJzoSqP8SYMSLdxHqP4wnPTI4WvbB8PKPDV30y5y7Tya4RHOBA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/types": "^29.6.0", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.6.0", "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", + "jest-util": "^29.6.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -1713,12 +1705,12 @@ } }, "node_modules/@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.0.tgz", + "integrity": "sha512-8XCgL9JhqbJTFnMRjEAO+TuW251+MoMd5BSzLiE3vvzpQ8RlBxy8NoyNkDhs3K3OL3HeVinlOl9or5p7GTeOLg==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.0", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -1808,6 +1800,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1844,9 +1845,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "node_modules/@sinonjs/commons": { @@ -2499,13 +2500,18 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/babel-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", - "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.0.tgz", + "integrity": "sha512-Jj8Bq2yKsk11XLk06Nm8SdvYkAcecH+GuhxB8DnK5SncjHnJ88TQjSnGgE7jpajpnSvz9DZ6X8hXrDkD/6/TPQ==", "dev": true, "dependencies": { - "@jest/transform": "^29.5.0", + "@jest/transform": "^29.6.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.5.0", @@ -2975,9 +2981,9 @@ } }, "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, "node_modules/color": { @@ -3043,6 +3049,17 @@ "text-hex": "1.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -3121,6 +3138,14 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3516,16 +3541,17 @@ } }, "node_modules/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.0.tgz", + "integrity": "sha512-AV+HaBtnDJ2YEUhPPo25HyUHBLaetM+y/Dq6pEC8VPQyt1dK+k8MfGkMy46djy2bddcqESc1kl4/K1uLWSfk9g==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.5.0", + "@jest/expect-utils": "^29.6.0", + "@types/node": "*", "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" + "jest-matcher-utils": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-util": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3664,6 +3690,38 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4173,15 +4231,15 @@ "dev": true }, "node_modules/jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", - "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.0.tgz", + "integrity": "sha512-do1J9gGrQ68E4UfMz/4OM71p9qCqQxu32N/9ZfeYFSSlx0uUOuxeyZxtJZNaUTW12ZA11ERhmBjBhy1Ho96R4g==", "dev": true, "dependencies": { - "@jest/core": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.6.0", + "@jest/types": "^29.6.0", "import-local": "^3.0.2", - "jest-cli": "^29.5.0" + "jest-cli": "^29.6.0" }, "bin": { "jest": "bin/jest.js" @@ -4301,28 +4359,28 @@ } }, "node_modules/jest-circus": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", - "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.0.tgz", + "integrity": "sha512-LtG45qEKhse2Ws5zNR4DnZATReLGQXzBZGZnJ0DU37p6d4wDhu41vvczCQ3Ou+llR6CRYDBshsubV7H4jZvIkw==", "dev": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.6.0", + "@jest/expect": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.5.0", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-each": "^29.6.0", + "jest-matcher-utils": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-runtime": "^29.6.0", + "jest-snapshot": "^29.6.0", + "jest-util": "^29.6.0", "p-limit": "^3.1.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.6.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -4363,21 +4421,21 @@ } }, "node_modules/jest-cli": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", - "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.0.tgz", + "integrity": "sha512-WvZIaanK/abkw6s01924DQ2QLwM5Q4Y4iPbSDb9Zg6smyXGqqcPQ7ft9X8D7B0jICz312eSzM6UlQNxuZJBrMw==", "dev": true, "dependencies": { - "@jest/core": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/types": "^29.6.0", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-config": "^29.6.0", + "jest-util": "^29.6.0", + "jest-validate": "^29.6.0", "prompts": "^2.0.1", "yargs": "^17.3.1" }, @@ -4428,31 +4486,31 @@ } }, "node_modules/jest-config": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", - "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.0.tgz", + "integrity": "sha512-fKA4jM91PDqWVkMpb1FVKxIuhg3hC6hgaen57cr1rRZkR96dCatvJZsk3ik7/GNu9ERj9wgAspOmyvkFoGsZhA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.5.0", - "@jest/types": "^29.5.0", - "babel-jest": "^29.5.0", + "@jest/test-sequencer": "^29.6.0", + "@jest/types": "^29.6.0", + "babel-jest": "^29.6.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.5.0", - "jest-environment-node": "^29.5.0", + "jest-circus": "^29.6.0", + "jest-environment-node": "^29.6.0", "jest-get-type": "^29.4.3", "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-resolve": "^29.6.0", + "jest-runner": "^29.6.0", + "jest-util": "^29.6.0", + "jest-validate": "^29.6.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.6.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -4504,15 +4562,15 @@ } }, "node_modules/jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.0.tgz", + "integrity": "sha512-ZRm7cd2m9YyZ0N3iMyuo1iUiprxQ/MFpYWXzEEj7hjzL3WnDffKW8192XBDcrAI8j7hnrM1wed3bL/oEnYF/8w==", "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.4.3", "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "pretty-format": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4562,16 +4620,16 @@ } }, "node_modules/jest-each": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", - "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.0.tgz", + "integrity": "sha512-d0Jem4RBAlFUyV6JSXPSHVUpNo5RleSj+iJEy1G3+ZCrzHDjWs/1jUfrbnJKHdJdAx5BCEce/Ju379WqHhQk4w==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "chalk": "^4.0.0", "jest-get-type": "^29.4.3", - "jest-util": "^29.5.0", - "pretty-format": "^29.5.0" + "jest-util": "^29.6.0", + "pretty-format": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4609,17 +4667,17 @@ } }, "node_modules/jest-environment-node": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", - "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.0.tgz", + "integrity": "sha512-BOf5Q2/nFCdBOnyBM5c5/6DbdQYgc+0gyUQ8l8qhUAB8O7pM+4QJXIXJsRZJaxd5SHV6y5VArTVhOfogoqcP8Q==", "dev": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.6.0", + "@jest/fake-timers": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-mock": "^29.6.0", + "jest-util": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4635,20 +4693,20 @@ } }, "node_modules/jest-haste-map": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", - "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.0.tgz", + "integrity": "sha512-dY1DKufptj7hcJSuhpqlYPGcnN3XjlOy/g0jinpRTMsbb40ivZHiuIPzeminOZkrek8C+oDxC54ILGO3vMLojg==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-util": "^29.6.0", + "jest-worker": "^29.6.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -4660,28 +4718,28 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", - "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.0.tgz", + "integrity": "sha512-JdV6EZOPxHR1gd6ccxjNowuROkT2jtGU5G/g58RcJX1xe5mrtLj0g6/ZkyMoXF4cs+tTkHMFX6pcIrB1QPQwCw==", "dev": true, "dependencies": { "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "pretty-format": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.0.tgz", + "integrity": "sha512-oSlqfGN+sbkB2Q5um/zL7z80w84FEAcLKzXBZIPyRk2F2Srg1ubhrHVKW68JCvb2+xKzAeGw35b+6gciS24PHw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.5.0", + "jest-diff": "^29.6.0", "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "pretty-format": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4719,18 +4777,18 @@ } }, "node_modules/jest-message-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.0.tgz", + "integrity": "sha512-mkCp56cETbpoNtsaeWVy6SKzk228mMi9FPHSObaRIhbR2Ujw9PqjW/yqVHD2tN1bHbC8ol6h3UEo7dOPmIYwIA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.6.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -4770,14 +4828,14 @@ } }, "node_modules/jest-mock": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", - "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.0.tgz", + "integrity": "sha512-2Pb7R2w24Q0aUVn+2/vdRDL6CqGqpheDZy7zrXav8FotOpSGw/4bS2hyVoKHMEx4xzOn6EyCAGwc5czWxXeN7w==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/node": "*", - "jest-util": "^29.5.0" + "jest-util": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4810,17 +4868,17 @@ } }, "node_modules/jest-resolve": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", - "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.0.tgz", + "integrity": "sha512-+hrpY4LzAONoZA/rvB6rnZLkOSA6UgJLpdCWrOZNSgGxWMumzRLu7dLUSCabAHzoHIDQ9qXfr3th1zYNJ0E8sQ==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.6.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-util": "^29.6.0", + "jest-validate": "^29.6.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -4830,13 +4888,13 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", - "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.0.tgz", + "integrity": "sha512-eOfPog9K3hJdJk/3i6O6bQhXS+3uXhMDkLJGX+xmMPp7T1d/zdcFofbDnHgNoEkhD/mSimC5IagLEP7lpLLu/A==", "dev": true, "dependencies": { "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.5.0" + "jest-snapshot": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4874,30 +4932,30 @@ } }, "node_modules/jest-runner": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", - "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.0.tgz", + "integrity": "sha512-4fZuGV2lOxS2BiqEG9/AI8E6O+jo+QZjMVcgi1x5E6aDql0Gd/EFIbUQ0pSS09y8cya1vJB/qC2xsE468jqtSg==", "dev": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/environment": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.6.0", + "@jest/environment": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-leak-detector": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-resolve": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-util": "^29.5.0", - "jest-watcher": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-environment-node": "^29.6.0", + "jest-haste-map": "^29.6.0", + "jest-leak-detector": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-resolve": "^29.6.0", + "jest-runtime": "^29.6.0", + "jest-util": "^29.6.0", + "jest-watcher": "^29.6.0", + "jest-worker": "^29.6.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -4937,31 +4995,31 @@ } }, "node_modules/jest-runtime": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", - "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/globals": "^29.5.0", - "@jest/source-map": "^29.4.3", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.0.tgz", + "integrity": "sha512-5FavYo3EeXLHIvnJf+r7Cj0buePAbe4mzRB9oeVxDS0uVmouSBjWeGgyRjZkw7ArxOoZI8gO6f8SGMJ2HFlwwg==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.0", + "@jest/fake-timers": "^29.6.0", + "@jest/globals": "^29.6.0", + "@jest/source-map": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", + "jest-haste-map": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-mock": "^29.6.0", "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-resolve": "^29.6.0", + "jest-snapshot": "^29.6.0", + "jest-util": "^29.6.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -5001,34 +5059,32 @@ } }, "node_modules/jest-snapshot": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", - "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.0.tgz", + "integrity": "sha512-H3kUE9NwWDEDoutcOSS921IqdlkdjgnMdj1oMyxAHNflscdLc9dB8OudZHV6kj4OHJxbMxL8CdI5DlwYrs4wQg==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", + "@jest/expect-utils": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.5.0", + "expect": "^29.6.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", + "jest-diff": "^29.6.0", "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-matcher-utils": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-util": "^29.6.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" + "pretty-format": "^29.6.0", + "semver": "^7.5.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -5066,12 +5122,12 @@ } }, "node_modules/jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.0.tgz", + "integrity": "sha512-S0USx9YwcvEm4pQ5suisVm/RVxBmi0GFR7ocJhIeaCuW5AXnAnffXbaVKvIFodyZNOc9ygzVtTxmBf40HsHXaA==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -5114,17 +5170,17 @@ } }, "node_modules/jest-validate": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", - "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.0.tgz", + "integrity": "sha512-MLTrAJsb1+W7svbeZ+A7pAnyXMaQrjvPDKCy7OlfsfB6TMVc69v7WjUWfiR6r3snULFWZASiKgvNVDuATta1dg==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.4.3", "leven": "^3.1.0", - "pretty-format": "^29.5.0" + "pretty-format": "^29.6.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -5174,18 +5230,18 @@ } }, "node_modules/jest-watcher": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", - "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.0.tgz", + "integrity": "sha512-LdsQqFNX60mRdRRe+zsELnYRH1yX6KL+ukbh+u6WSQeTheZZe1TlLJNKRQiZ7e0VbvMkywmMWL/KV35noOJCcw==", "dev": true, "dependencies": { - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/test-result": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.5.0", + "jest-util": "^29.6.0", "string-length": "^4.0.1" }, "engines": { @@ -5224,13 +5280,13 @@ } }, "node_modules/jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.0.tgz", + "integrity": "sha512-oiQHH1SnKmZIwwPnpOrXTq4kHBk3lKGY/07DpnH0sAu+x7J8rXlbLDROZsU6vy9GwB0hPiZeZpu6YlJ48QoKcA==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.6.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -5763,6 +5819,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -6021,18 +6096,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", + "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "dependencies": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + } + }, + "node_modules/openai/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -6434,12 +6526,12 @@ } }, "node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.0.tgz", + "integrity": "sha512-XH+D4n7Ey0iSR6PdAnBs99cWMZdGsdKrR33iUHQNr79w1szKTCIZDVdXuccAsHVwDBp0XeWPfNEoaxP9EZgRmQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.0", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -7638,15 +7730,6 @@ "@types/node": "*" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -7879,6 +7962,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -7899,32 +7988,32 @@ } }, "@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", + "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", "dev": true }, "@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.6.tgz", + "integrity": "sha512-HPIyDa6n+HKw5dEuway3vVAhBboYCtREBMp+IWeseZy6TFtzn6MHkCH2KKYUOC/vKKwgSMHQW4htBOrmuRPXfw==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.6", "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", + "@babel/traverse": "^7.22.6", "@babel/types": "^7.22.5", + "@nicolo-ribaudo/semver-v6": "^6.3.3", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.2" }, "dependencies": { "@babel/generator": { @@ -7940,9 +8029,9 @@ } }, "@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", "dev": true, "requires": { "@babel/code-frame": "^7.22.5", @@ -7950,8 +8039,8 @@ "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", "@babel/types": "^7.22.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -8001,12 +8090,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true } } }, @@ -8022,16 +8105,16 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", + "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", "dev": true, "requires": { - "@babel/compat-data": "^7.22.5", + "@babel/compat-data": "^7.22.6", "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "@nicolo-ribaudo/semver-v6": "^6.3.3", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1" }, "dependencies": { "lru-cache": { @@ -8043,12 +8126,6 @@ "yallist": "^3.0.2" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -8246,9 +8323,9 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { "@babel/types": "^7.22.5" @@ -8286,13 +8363,13 @@ "dev": true }, "@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", "dev": true, "requires": { "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", + "@babel/traverse": "^7.22.6", "@babel/types": "^7.22.5" }, "dependencies": { @@ -8309,9 +8386,9 @@ } }, "@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.6.tgz", + "integrity": "sha512-53CijMvKlLIDlOTrdWiHileRddlIiwUIyCKqYa7lYnnPldXCG5dUSN38uT0cA6i7rHWNKJLH0VU/Kxdr1GzB3w==", "dev": true, "requires": { "@babel/code-frame": "^7.22.5", @@ -8319,8 +8396,8 @@ "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.6", "@babel/types": "^7.22.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -8437,9 +8514,9 @@ } }, "@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz", + "integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -8803,16 +8880,16 @@ "dev": true }, "@jest/console": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", - "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.0.tgz", + "integrity": "sha512-anb6L1yg7uPQpytNVA5skRaXy3BmrsU8icRhTVNbWdjYWDDfy8M1Kq5HIVRpYoABdbpqsc5Dr+jtu4+qWRQBiQ==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-message-util": "^29.6.0", + "jest-util": "^29.6.0", "slash": "^3.0.0" }, "dependencies": { @@ -8838,16 +8915,16 @@ } }, "@jest/core": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", - "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.0.tgz", + "integrity": "sha512-5dbMHfY/5R9m8NbgmB3JlxQqooZ/ooPSOiwEQZZ+HODwJTbIu37seVcZNBK29aMdXtjvTRB3f6LCvkKq+r8uQA==", "dev": true, "requires": { - "@jest/console": "^29.5.0", - "@jest/reporters": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.6.0", + "@jest/reporters": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", @@ -8855,20 +8932,20 @@ "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.5.0", - "jest-config": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", + "jest-config": "^29.6.0", + "jest-haste-map": "^29.6.0", + "jest-message-util": "^29.6.0", "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-resolve-dependencies": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "jest-watcher": "^29.5.0", + "jest-resolve": "^29.6.0", + "jest-resolve-dependencies": "^29.6.0", + "jest-runner": "^29.6.0", + "jest-runtime": "^29.6.0", + "jest-snapshot": "^29.6.0", + "jest-util": "^29.6.0", + "jest-validate": "^29.6.0", + "jest-watcher": "^29.6.0", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.6.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -8910,74 +8987,74 @@ } }, "@jest/environment": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", - "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.0.tgz", + "integrity": "sha512-bUZLYUxYlUIsslBbxII0fq0kr1+friI3Gty+cRLmocGB1jdcAHs7FS8QdCDqedE8q4DZE1g/AJHH6OJZBLGGsg==", "dev": true, "requires": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/fake-timers": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", - "jest-mock": "^29.5.0" + "jest-mock": "^29.6.0" } }, "@jest/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.0.tgz", + "integrity": "sha512-a7pISPW28Q3c0/pLwz4mQ6tbAI+hc8/0CJp9ix6e9U4dQ6TiHQX82CT5DV5BMWaw8bFH4E6zsfZxXdn6Ka23Bw==", "dev": true, "requires": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" + "expect": "^29.6.0", + "jest-snapshot": "^29.6.0" } }, "@jest/expect-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.0.tgz", + "integrity": "sha512-LLSQQN7oypMSETKoPWpsWYVKJd9LQWmSDDAc4hUQ4JocVC7LAMy9R3ZMhlnLwbcFvQORZnZR7HM893Px6cJhvA==", "dev": true, "requires": { "jest-get-type": "^29.4.3" } }, "@jest/fake-timers": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", - "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.0.tgz", + "integrity": "sha512-nuCU46AsZoskthWSDS2Aj6LARgyNcp5Fjx2qxsO/fPl1Wp1CJ+dBDqs0OkEcJK8FBeV/MbjH5efe79M2sHcV+A==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-message-util": "^29.6.0", + "jest-mock": "^29.6.0", + "jest-util": "^29.6.0" } }, "@jest/globals": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", - "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.0.tgz", + "integrity": "sha512-IQQ3hZ2D/hwEwXSMv5GbfhzdH0nTQR3KPYxnuW6gYWbd6+7/zgMz7Okn6EgBbNtJNONq03k5EKA6HqGyzRbpeg==", "dev": true, "requires": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" + "@jest/environment": "^29.6.0", + "@jest/expect": "^29.6.0", + "@jest/types": "^29.6.0", + "jest-mock": "^29.6.0" } }, "@jest/reporters": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", - "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.0.tgz", + "integrity": "sha512-dWEq4HI0VvHcAD6XTtyBKKARLytyyWPIy1SvGOcU91106MfvHPdxZgupFwVHd8TFpZPpA3SebYjtwS5BUS76Rw==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/console": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", @@ -8989,9 +9066,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-message-util": "^29.6.0", + "jest-util": "^29.6.0", + "jest-worker": "^29.6.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -9057,21 +9134,21 @@ } }, "@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", + "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==", "dev": true, "requires": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" } }, "@jest/source-map": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", - "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.0.tgz", + "integrity": "sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.15", + "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" }, @@ -9101,46 +9178,46 @@ } }, "@jest/test-result": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", - "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.0.tgz", + "integrity": "sha512-9qLb7xITeyWhM4yatn2muqfomuoCTOhv0QV9i7XiIyYi3QLfnvPv5NeJp5u0PZeutAOROMLKakOkmoAisOr3YQ==", "dev": true, "requires": { - "@jest/console": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.6.0", + "@jest/types": "^29.6.0", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "@jest/test-sequencer": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", - "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.0.tgz", + "integrity": "sha512-HYCS3LKRQotKWj2mnA3AN13PPevYZu8MJKm12lzYojpJNnn6kI/3PWmr1At/e3tUu+FHQDiOyaDVuR4EV3ezBw==", "dev": true, "requires": { - "@jest/test-result": "^29.5.0", + "@jest/test-result": "^29.6.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.6.0", "slash": "^3.0.0" } }, "@jest/transform": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", - "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.0.tgz", + "integrity": "sha512-bhP/KxPo3e322FJ0nKAcb6WVK76ZYyQd1lWygJzoSqP8SYMSLdxHqP4wnPTI4WvbB8PKPDV30y5y7Tya4RHOBA==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/types": "^29.6.0", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.6.0", "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", + "jest-util": "^29.6.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -9191,12 +9268,12 @@ } }, "@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.0.tgz", + "integrity": "sha512-8XCgL9JhqbJTFnMRjEAO+TuW251+MoMd5BSzLiE3vvzpQ8RlBxy8NoyNkDhs3K3OL3HeVinlOl9or5p7GTeOLg==", "dev": true, "requires": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.0", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -9264,6 +9341,12 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -9291,9 +9374,9 @@ } }, "@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "@sinonjs/commons": { @@ -9794,13 +9877,18 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "babel-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", - "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.0.tgz", + "integrity": "sha512-Jj8Bq2yKsk11XLk06Nm8SdvYkAcecH+GuhxB8DnK5SncjHnJ88TQjSnGgE7jpajpnSvz9DZ6X8hXrDkD/6/TPQ==", "dev": true, "requires": { - "@jest/transform": "^29.5.0", + "@jest/transform": "^29.6.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.5.0", @@ -10128,9 +10216,9 @@ "dev": true }, "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, "color": { @@ -10195,6 +10283,14 @@ "text-hex": "1.0.x" } }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -10256,6 +10352,11 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -10546,16 +10647,17 @@ "dev": true }, "expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.0.tgz", + "integrity": "sha512-AV+HaBtnDJ2YEUhPPo25HyUHBLaetM+y/Dq6pEC8VPQyt1dK+k8MfGkMy46djy2bddcqESc1kl4/K1uLWSfk9g==", "dev": true, "requires": { - "@jest/expect-utils": "^29.5.0", + "@jest/expect-utils": "^29.6.0", + "@types/node": "*", "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" + "jest-matcher-utils": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-util": "^29.6.0" } }, "fast-deep-equal": { @@ -10672,6 +10774,21 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -11036,15 +11153,15 @@ "dev": true }, "jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", - "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.0.tgz", + "integrity": "sha512-do1J9gGrQ68E4UfMz/4OM71p9qCqQxu32N/9ZfeYFSSlx0uUOuxeyZxtJZNaUTW12ZA11ERhmBjBhy1Ho96R4g==", "dev": true, "requires": { - "@jest/core": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.6.0", + "@jest/types": "^29.6.0", "import-local": "^3.0.2", - "jest-cli": "^29.5.0" + "jest-cli": "^29.6.0" } }, "jest-changed-files": { @@ -11119,28 +11236,28 @@ } }, "jest-circus": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", - "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.0.tgz", + "integrity": "sha512-LtG45qEKhse2Ws5zNR4DnZATReLGQXzBZGZnJ0DU37p6d4wDhu41vvczCQ3Ou+llR6CRYDBshsubV7H4jZvIkw==", "dev": true, "requires": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.6.0", + "@jest/expect": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.5.0", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-each": "^29.6.0", + "jest-matcher-utils": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-runtime": "^29.6.0", + "jest-snapshot": "^29.6.0", + "jest-util": "^29.6.0", "p-limit": "^3.1.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.6.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -11168,21 +11285,21 @@ } }, "jest-cli": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", - "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.0.tgz", + "integrity": "sha512-WvZIaanK/abkw6s01924DQ2QLwM5Q4Y4iPbSDb9Zg6smyXGqqcPQ7ft9X8D7B0jICz312eSzM6UlQNxuZJBrMw==", "dev": true, "requires": { - "@jest/core": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/types": "^29.6.0", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-config": "^29.6.0", + "jest-util": "^29.6.0", + "jest-validate": "^29.6.0", "prompts": "^2.0.1", "yargs": "^17.3.1" }, @@ -11209,31 +11326,31 @@ } }, "jest-config": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", - "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.0.tgz", + "integrity": "sha512-fKA4jM91PDqWVkMpb1FVKxIuhg3hC6hgaen57cr1rRZkR96dCatvJZsk3ik7/GNu9ERj9wgAspOmyvkFoGsZhA==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.5.0", - "@jest/types": "^29.5.0", - "babel-jest": "^29.5.0", + "@jest/test-sequencer": "^29.6.0", + "@jest/types": "^29.6.0", + "babel-jest": "^29.6.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.5.0", - "jest-environment-node": "^29.5.0", + "jest-circus": "^29.6.0", + "jest-environment-node": "^29.6.0", "jest-get-type": "^29.4.3", "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-resolve": "^29.6.0", + "jest-runner": "^29.6.0", + "jest-util": "^29.6.0", + "jest-validate": "^29.6.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.6.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -11260,15 +11377,15 @@ } }, "jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.0.tgz", + "integrity": "sha512-ZRm7cd2m9YyZ0N3iMyuo1iUiprxQ/MFpYWXzEEj7hjzL3WnDffKW8192XBDcrAI8j7hnrM1wed3bL/oEnYF/8w==", "dev": true, "requires": { "chalk": "^4.0.0", "diff-sequences": "^29.4.3", "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "pretty-format": "^29.6.0" }, "dependencies": { "ansi-styles": { @@ -11302,16 +11419,16 @@ } }, "jest-each": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", - "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.0.tgz", + "integrity": "sha512-d0Jem4RBAlFUyV6JSXPSHVUpNo5RleSj+iJEy1G3+ZCrzHDjWs/1jUfrbnJKHdJdAx5BCEce/Ju379WqHhQk4w==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "chalk": "^4.0.0", "jest-get-type": "^29.4.3", - "jest-util": "^29.5.0", - "pretty-format": "^29.5.0" + "jest-util": "^29.6.0", + "pretty-format": "^29.6.0" }, "dependencies": { "ansi-styles": { @@ -11336,17 +11453,17 @@ } }, "jest-environment-node": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", - "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.0.tgz", + "integrity": "sha512-BOf5Q2/nFCdBOnyBM5c5/6DbdQYgc+0gyUQ8l8qhUAB8O7pM+4QJXIXJsRZJaxd5SHV6y5VArTVhOfogoqcP8Q==", "dev": true, "requires": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.6.0", + "@jest/fake-timers": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-mock": "^29.6.0", + "jest-util": "^29.6.0" } }, "jest-get-type": { @@ -11356,12 +11473,12 @@ "dev": true }, "jest-haste-map": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", - "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.0.tgz", + "integrity": "sha512-dY1DKufptj7hcJSuhpqlYPGcnN3XjlOy/g0jinpRTMsbb40ivZHiuIPzeminOZkrek8C+oDxC54ILGO3vMLojg==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", @@ -11369,32 +11486,32 @@ "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-util": "^29.6.0", + "jest-worker": "^29.6.0", "micromatch": "^4.0.4", "walker": "^1.0.8" } }, "jest-leak-detector": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", - "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.0.tgz", + "integrity": "sha512-JdV6EZOPxHR1gd6ccxjNowuROkT2jtGU5G/g58RcJX1xe5mrtLj0g6/ZkyMoXF4cs+tTkHMFX6pcIrB1QPQwCw==", "dev": true, "requires": { "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "pretty-format": "^29.6.0" } }, "jest-matcher-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.0.tgz", + "integrity": "sha512-oSlqfGN+sbkB2Q5um/zL7z80w84FEAcLKzXBZIPyRk2F2Srg1ubhrHVKW68JCvb2+xKzAeGw35b+6gciS24PHw==", "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^29.5.0", + "jest-diff": "^29.6.0", "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "pretty-format": "^29.6.0" }, "dependencies": { "ansi-styles": { @@ -11419,18 +11536,18 @@ } }, "jest-message-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.0.tgz", + "integrity": "sha512-mkCp56cETbpoNtsaeWVy6SKzk228mMi9FPHSObaRIhbR2Ujw9PqjW/yqVHD2tN1bHbC8ol6h3UEo7dOPmIYwIA==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.6.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -11457,14 +11574,14 @@ } }, "jest-mock": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", - "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.0.tgz", + "integrity": "sha512-2Pb7R2w24Q0aUVn+2/vdRDL6CqGqpheDZy7zrXav8FotOpSGw/4bS2hyVoKHMEx4xzOn6EyCAGwc5czWxXeN7w==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/node": "*", - "jest-util": "^29.5.0" + "jest-util": "^29.6.0" } }, "jest-pnp-resolver": { @@ -11481,17 +11598,17 @@ "dev": true }, "jest-resolve": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", - "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.0.tgz", + "integrity": "sha512-+hrpY4LzAONoZA/rvB6rnZLkOSA6UgJLpdCWrOZNSgGxWMumzRLu7dLUSCabAHzoHIDQ9qXfr3th1zYNJ0E8sQ==", "dev": true, "requires": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.6.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-util": "^29.6.0", + "jest-validate": "^29.6.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -11519,40 +11636,40 @@ } }, "jest-resolve-dependencies": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", - "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.0.tgz", + "integrity": "sha512-eOfPog9K3hJdJk/3i6O6bQhXS+3uXhMDkLJGX+xmMPp7T1d/zdcFofbDnHgNoEkhD/mSimC5IagLEP7lpLLu/A==", "dev": true, "requires": { "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.5.0" + "jest-snapshot": "^29.6.0" } }, "jest-runner": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", - "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.0.tgz", + "integrity": "sha512-4fZuGV2lOxS2BiqEG9/AI8E6O+jo+QZjMVcgi1x5E6aDql0Gd/EFIbUQ0pSS09y8cya1vJB/qC2xsE468jqtSg==", "dev": true, "requires": { - "@jest/console": "^29.5.0", - "@jest/environment": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.6.0", + "@jest/environment": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-leak-detector": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-resolve": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-util": "^29.5.0", - "jest-watcher": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-environment-node": "^29.6.0", + "jest-haste-map": "^29.6.0", + "jest-leak-detector": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-resolve": "^29.6.0", + "jest-runtime": "^29.6.0", + "jest-util": "^29.6.0", + "jest-watcher": "^29.6.0", + "jest-worker": "^29.6.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -11579,31 +11696,31 @@ } }, "jest-runtime": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", - "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", - "dev": true, - "requires": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/globals": "^29.5.0", - "@jest/source-map": "^29.4.3", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.0.tgz", + "integrity": "sha512-5FavYo3EeXLHIvnJf+r7Cj0buePAbe4mzRB9oeVxDS0uVmouSBjWeGgyRjZkw7ArxOoZI8gO6f8SGMJ2HFlwwg==", + "dev": true, + "requires": { + "@jest/environment": "^29.6.0", + "@jest/fake-timers": "^29.6.0", + "@jest/globals": "^29.6.0", + "@jest/source-map": "^29.6.0", + "@jest/test-result": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", + "jest-haste-map": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-mock": "^29.6.0", "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-resolve": "^29.6.0", + "jest-snapshot": "^29.6.0", + "jest-util": "^29.6.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -11630,34 +11747,32 @@ } }, "jest-snapshot": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", - "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.0.tgz", + "integrity": "sha512-H3kUE9NwWDEDoutcOSS921IqdlkdjgnMdj1oMyxAHNflscdLc9dB8OudZHV6kj4OHJxbMxL8CdI5DlwYrs4wQg==", "dev": true, "requires": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", + "@jest/expect-utils": "^29.6.0", + "@jest/transform": "^29.6.0", + "@jest/types": "^29.6.0", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.5.0", + "expect": "^29.6.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", + "jest-diff": "^29.6.0", "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-matcher-utils": "^29.6.0", + "jest-message-util": "^29.6.0", + "jest-util": "^29.6.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" + "pretty-format": "^29.6.0", + "semver": "^7.5.3" }, "dependencies": { "ansi-styles": { @@ -11682,12 +11797,12 @@ } }, "jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.0.tgz", + "integrity": "sha512-S0USx9YwcvEm4pQ5suisVm/RVxBmi0GFR7ocJhIeaCuW5AXnAnffXbaVKvIFodyZNOc9ygzVtTxmBf40HsHXaA==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -11717,17 +11832,17 @@ } }, "jest-validate": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", - "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.0.tgz", + "integrity": "sha512-MLTrAJsb1+W7svbeZ+A7pAnyXMaQrjvPDKCy7OlfsfB6TMVc69v7WjUWfiR6r3snULFWZASiKgvNVDuATta1dg==", "dev": true, "requires": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.0", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.4.3", "leven": "^3.1.0", - "pretty-format": "^29.5.0" + "pretty-format": "^29.6.0" }, "dependencies": { "ansi-styles": { @@ -11758,18 +11873,18 @@ } }, "jest-watcher": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", - "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.0.tgz", + "integrity": "sha512-LdsQqFNX60mRdRRe+zsELnYRH1yX6KL+ukbh+u6WSQeTheZZe1TlLJNKRQiZ7e0VbvMkywmMWL/KV35noOJCcw==", "dev": true, "requires": { - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/test-result": "^29.6.0", + "@jest/types": "^29.6.0", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.5.0", + "jest-util": "^29.6.0", "string-length": "^4.0.1" }, "dependencies": { @@ -11795,13 +11910,13 @@ } }, "jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.0.tgz", + "integrity": "sha512-oiQHH1SnKmZIwwPnpOrXTq4kHBk3lKGY/07DpnH0sAu+x7J8rXlbLDROZsU6vy9GwB0hPiZeZpu6YlJ48QoKcA==", "dev": true, "requires": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.6.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -12198,6 +12313,19 @@ "picomatch": "^2.3.1" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -12382,18 +12510,37 @@ "mimic-fn": "^4.0.0" } }, + "openai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", + "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "requires": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + }, + "dependencies": { + "axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "requires": { + "follow-redirects": "^1.14.8" + } + } + } + }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-limit": { @@ -12668,12 +12815,12 @@ "dev": true }, "pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.0.tgz", + "integrity": "sha512-XH+D4n7Ey0iSR6PdAnBs99cWMZdGsdKrR33iUHQNr79w1szKTCIZDVdXuccAsHVwDBp0XeWPfNEoaxP9EZgRmQ==", "dev": true, "requires": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.0", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -13494,12 +13641,6 @@ "@types/node": "*" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 17d18d3..1e11fdb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.ts", "scripts": { "build": "tsc", - "start": "nodemon src/index.ts", + "start": "ts-node src/index.ts", + "dev": "nodemon src/index.ts", "test": "jest", "lint": "eslint . --ext .ts", "prettier": "prettier --write .", @@ -27,6 +28,7 @@ }, "dependencies": { "dotenv": "^16.0.3", + "openai": "^3.3.0", "pg": "^8.11.0", "pg-hstore": "^2.3.4", "sequelize": "^6.32.0", diff --git a/src/launchApplication.ts b/src/launchApplication.ts index bc549de..c9bd623 100644 --- a/src/launchApplication.ts +++ b/src/launchApplication.ts @@ -1,5 +1,3 @@ -import { message } from "telegraf/filters"; - import "./connectToDatabase"; import { BotCommandDescription, BotCommands } from "./constants/actions"; import { welcomeMessageText } from "./constants/messages"; @@ -9,6 +7,10 @@ import VacancyModel from "./schemas/vacancy"; import { BotService, SubscribeToActionsService, logger } from "./services"; import config from "./utils/config"; +if (!config.aiApiKey || !config.aiOrganizationId) { + throw Error("You must provide OPENAI_ORGANIZATION_ID and OPENAI_API_KEY"); +} + if (!config.botConsultantUsername) { logger.warn("Variable BOT_CONSULTANT_USERNAME is missing"); } @@ -32,9 +34,7 @@ bot.telegram.setMyCommands([ ]); SubscribeToActionsService.subscribeToCommands(); - -bot.on(message("text"), SubscribeToActionsService.subscribeToTextMessage); - +SubscribeToActionsService.subscribeToTextMessage(); SubscribeToActionsService.subscribeToButtonActions(); SubscribeToActionsService.subscribeToPublishQueueMonitoring(); diff --git a/src/schemas/vacancy.ts b/src/schemas/vacancy.ts index 7a22971..43075d1 100644 --- a/src/schemas/vacancy.ts +++ b/src/schemas/vacancy.ts @@ -70,6 +70,7 @@ export const VacancyModel = db.define( format_of_work_description: DataTypes.STRING, company_name: { type: DataTypes.STRING, allowNull: false }, + company_description: DataTypes.STRING, author_username: { type: DataTypes.STRING, diff --git a/src/services/ai/index.ts b/src/services/ai/index.ts new file mode 100644 index 0000000..dc16714 --- /dev/null +++ b/src/services/ai/index.ts @@ -0,0 +1,12 @@ +import { Configuration, OpenAIApi } from "openai"; + +import config from "../../utils/config"; + +const configuration = new Configuration({ + organization: config.aiOrganizationId, + apiKey: config.aiApiKey, +}); + +const openai = new OpenAIApi(configuration); + +export default openai; diff --git a/src/services/message-preview/sendMessagePreview.ts b/src/services/message-preview/sendMessagePreview.ts index 7e3b1a3..d3081fd 100644 --- a/src/services/message-preview/sendMessagePreview.ts +++ b/src/services/message-preview/sendMessagePreview.ts @@ -1,36 +1,16 @@ import { Markup } from "telegraf"; import { ActionButtonLabels, BotActions } from "../../constants/actions"; -import { EmploymentType, FormatOfWork } from "../../constants/vacancy"; import { IVacancyParsed } from "../../types/vacancy"; import { buildMessageFromVacancy } from "../../utils/buildMessageFromVacancy"; +import { IParsedMessageEntity } from "../../utils/parseMessageEntities"; import logger from "../logger"; import { createNewVacancy } from "./createNewVacancy"; -const MOCK_VACANCY: IVacancyParsed = { - title: "Mock vacancy title", - description: ` -Lorem ipsum dolor sit amet. Aut itaque inventore quo aspernatur possimus et possimus quidem. -Ut modi internos et blanditiis asperiores sed galisum rerum.\n\n -Est atque quos eos modi deleniti rem dolor galisum ut assumenda velit cum dignissimos amet. -Et nihil nihil et accusantium optio et consequatur galisum sit necessitatibus possimus quo -quia velit cum fuga perferendis. Non iure quia vel ipsum debitis At necessitatibus reprehenderit -in alias velit? Est autem aperiam eum natus ipsum rem excepturi corporis ab voluptas libero -ad dolores atque qui minus repellat.\n\n -Aut consequatur molestiae aut veniam inventore nam iusto accusantium ut doloremque aperiam qui -explicabo dicta. Et impedit omnis qui internos aliquam qui facilis perferendis ut vero quia vel -doloremque adipisci vel voluptatem amet sit nobis dicta. - `, - company_name: "Company of your dreams LLC", - format_of_work_title: FormatOfWork.Hybrid, - format_of_work_description: "2 days/week from office", - contact_info: "@marylorian", - type_of_employment: EmploymentType.FullTime, -}; - export const sendMessagePreview = async ( ctx, - parsedVacancy: IVacancyParsed = MOCK_VACANCY + parsedVacancy: IVacancyParsed, + parsedEntities: IParsedMessageEntity[] ) => { const replyMarkupButtons = Markup.inlineKeyboard([ Markup.button.callback( @@ -51,9 +31,10 @@ export const sendMessagePreview = async ( } const response = await ctx.sendMessage( - buildMessageFromVacancy(parsedVacancy), + buildMessageFromVacancy(parsedVacancy, parsedEntities), { ...replyMarkupButtons, + parse_mode: "HTML", } ); diff --git a/src/services/parse-message/index.ts b/src/services/parse-message/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/services/parse-message/parseVacancyWithAI.ts b/src/services/parse-message/parseVacancyWithAI.ts new file mode 100644 index 0000000..13b032d --- /dev/null +++ b/src/services/parse-message/parseVacancyWithAI.ts @@ -0,0 +1,159 @@ +import { + EmploymentType, + FormatOfWork, + SalaryType, +} from "../../constants/vacancy"; +import openai from "../ai"; + +interface IParsedVacancyByAI { + company: { + name: string; + description: string; + }; + format_of_work_title: FormatOfWork; + format_of_work_description: { + description: string; + }; + employment_details: { + type: string; + }; + vacancy_title: { + title: string; + }; + contact_info: { + telegram?: string; + email?: string; + }; + hiring_process: { + description?: string; + }; + salary: { + min: number; + max: number; + currency: string; + taxes: SalaryType; + bonus?: unknown; + }; + hashtags: string[]; + type_of_employment: EmploymentType; + description: string[]; + // forbidden_location: { + // city: string; + // country: string; + // }[]; + location: { + address: string; + city: string; + country: string; + restrictions: string; + }[]; +} + +export const parseVacancyWithAI = async (messageText: string) => { + console.log({ messageText }); + const { data } = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: [ + { + role: "assistant", + content: ` + For vacancy text in input below, extract the following fields + + Input: ${messageText} + + Fields: + - company (dict of company name and description) + - location (array of information about office address, city or country or special restrictions to work from some country, city) + ${ + /*`// - location (array of dicts of full address, city, country if present) + // - forbidden_location (array of dicts of city, country which are restricted or partially restricted or forbidden if present)`*/ "" + } + - vacancy_title (dict of job title) + - employment_details (dict of employment type as employee by local laws or as individual entrepreneur or as b2b if present) + - format_of_work_title (hybrid or remote or on-site (if value is not in english translate into english), modify to lowercase) + - format_of_work_description (dict of any details about work from office or home and employment process information by local laws (for example "по ТК РФ") or individual entrepreneur or b2b) (if present) + - contact_info (dict of mobile and landline phone numbers if present, or telegram nickname (add @ before it) or email address or full site url) + - hiring_process (dict of description of hiring process, if present) + - salary (dict of salary or wage for job done as range of numbers from 0 to positive infinity (as dict of max and min) and currency and taxes (net or gross), note that "до вычета" is equal to gross and "чистыми" is equal to net) + - type_of_employment (fulltime or parttime or contract or internship) + - hashtags (array of hashtags (words started from #) if present) + - description (array of the rest of the information about skills, offers, job information, benefits, bonuses as a text with all newline symbols saved) + + Valid JSON Output, omit fields that are not present, return empty response if required fields are not presented`, + }, + ], + temperature: 0, + }); + const parsedVacancy = data.choices[0].message + ? (JSON.parse( + data.choices[0].message.content || "{}" + ) as IParsedVacancyByAI) + : undefined; + + console.log({ + response: data, + choises: data.choices.map(({ message }) => message), + parsedVacancy, + location: parsedVacancy?.location, + // forbidden_location: parsedVacancy?.forbidden_location, + salary: parsedVacancy?.salary, + bonus: parsedVacancy?.salary?.bonus, + }); + + if (!parsedVacancy) { + return undefined; + } + + const { + vacancy_title, + location, + // forbidden_location, + salary, + description, + company, + contact_info, + type_of_employment, + format_of_work_title, + format_of_work_description, + hiring_process, + hashtags, + employment_details, + } = parsedVacancy; + + return { + title: vacancy_title.title, + location: location + ?.map(({ country, city, restrictions, address }) => + [ + country, + city, + address, + restrictions + ? `\nОграничения по локации: ${restrictions}` + : undefined, + ].filter(Boolean) + ) + .join(", "), + // .join(", ") + + // `\nОграничения по локации: ${forbidden_location?.map( + // ({ country, city }) => `${country ? `${country}, ` : ""}${city}` + // )}`, + salary_amount_from: salary?.min, + salary_amount_to: salary?.max, + salary_currency: salary.currency, + salary_type: salary.taxes, + description: + description?.join("\n") || + "" + `\n${hashtags?.map((w) => `#${w}`).join(" ")}`, + company_name: company.name, + company_description: company.description, + type_of_employment: type_of_employment, + contact_info: [contact_info.telegram, contact_info.email] + .filter(Boolean) + .join(", "), + format_of_work_title: format_of_work_title, + format_of_work_description: + format_of_work_description.description || employment_details.type, + hiring_process: hiring_process?.description, + }; +}; diff --git a/src/services/parse-message/processIncomingMessage.ts b/src/services/parse-message/processIncomingMessage.ts new file mode 100644 index 0000000..9c57c1b --- /dev/null +++ b/src/services/parse-message/processIncomingMessage.ts @@ -0,0 +1,53 @@ +import { parseMessageEntities } from "../../utils/parseMessageEntities"; +import { onVacancyEdit } from "../edit-vacancy"; +import { logger } from "../index"; +import { sendMessagePreview } from "../message-preview"; +import { parseVacancyWithAI } from "./parseVacancyWithAI"; + +export const processIncomingMessage = async (ctx) => { + const { message_id, from, text, chat, entities } = ctx?.update?.message || {}; + + try { + if (!message_id || !from?.username || !chat?.id) { + throw Error("cannot retrieve required message info"); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [techInfoLine, disclaimerLine, gapLine, ...updatedInfoText] = + text.split("\n"); + + // edited existing vacancy + if (techInfoLine && techInfoLine.startsWith(`@${ctx?.botInfo?.username}`)) { + const [, messageId] = techInfoLine.split(" > "); + + await onVacancyEdit(ctx, { + messageId, + updatedText: updatedInfoText.join("\n"), + }); + return; + } + + const parsedMessage = await parseVacancyWithAI(text); + + if (!parsedMessage) { + throw Error("failed to parse vacancy with AI"); + } + + await sendMessagePreview( + ctx, + parsedMessage, + parseMessageEntities(text, entities) + ); + } catch (err) { + logger.error( + `Failed to process incoming message ${from?.username}::${ + chat?.id + }::${message_id} - ${(err as Error)?.message || JSON.stringify(err)}` + ); + ctx.sendMessage( + `Не удалось распознать вакансию, попробуйте еще раз - ${ + (err as Error)?.message || JSON.stringify(err) + }` + ); + } +}; diff --git a/src/services/subscribe-to-actions/subscribeToTextMessage.ts b/src/services/subscribe-to-actions/subscribeToTextMessage.ts index 53010f9..dd0969c 100644 --- a/src/services/subscribe-to-actions/subscribeToTextMessage.ts +++ b/src/services/subscribe-to-actions/subscribeToTextMessage.ts @@ -1,38 +1,8 @@ -import { EditVacancyService, MessagePreviewService, logger } from "../index"; +import { message } from "telegraf/filters"; -export const subscribeToTextMessage = async (ctx) => { - const { message_id, from, text, chat } = ctx?.update?.message || {}; +import bot from "../../launchBot"; +import { processIncomingMessage } from "../parse-message/processIncomingMessage"; - try { - if (!message_id || !from?.username || !chat?.id) { - throw Error("cannot retrieve required message info"); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [techInfoLine, disclaimerLine, gapLine, ...updatedInfoText] = - text.split("\n"); - - // edited existing vacancy - if (techInfoLine && techInfoLine.startsWith(`@${ctx?.botInfo?.username}`)) { - const [, messageId] = techInfoLine.split(" > "); - - await EditVacancyService.onVacancyEdit(ctx, { - messageId, - updatedText: updatedInfoText.join("\n"), - }); - return; - } - - // here we generate vacancy text from message somehow - - await MessagePreviewService.sendMessagePreview( - ctx /* , parsedVacancyObject: IVacancyParsed */ - ); - } catch (err) { - logger.error( - `Failed to process incoming message ${from?.username}::${ - chat?.id - }::${message_id} - ${(err as Error)?.message || JSON.stringify(err)}` - ); - } +export const subscribeToTextMessage = () => { + bot.on(message("text"), processIncomingMessage); }; diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index c0575ca..4fd3ae1 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -15,6 +15,8 @@ declare global { USER_MONTH_VACANCY_LIMIT?: string | number; DAILY_VACANCY_LIMIT?: string | number; PUBLISH_CONFIG?: string; + OPENAI_ORGANIZATION_ID: string; + OPENAI_API_KEY: string; } } } diff --git a/src/types/vacancy.ts b/src/types/vacancy.ts index a577ec9..5b7b3d9 100644 --- a/src/types/vacancy.ts +++ b/src/types/vacancy.ts @@ -22,6 +22,7 @@ export type TVacancyAttributes = { published_tg_message_id?: string[]; published_tg_chat_id?: string[]; company_name: string; + company_description?: string; hiring_process?: string; salary_amount_from?: number; diff --git a/src/utils/buildMessageFromVacancy.ts b/src/utils/buildMessageFromVacancy.ts index adbd9ae..c01a6de 100644 --- a/src/utils/buildMessageFromVacancy.ts +++ b/src/utils/buildMessageFromVacancy.ts @@ -1,49 +1,76 @@ import { VacancyFieldLabel } from "../constants/labels"; import { IVacancyParsed } from "../types/vacancy"; +import { IParsedMessageEntity } from "./parseMessageEntities"; -export const buildMessageFromVacancy = ({ - title, - description, - company_name, +const getSalaryInfo = ({ salary_amount_from, salary_amount_to, salary_currency, salary_type, - format_of_work_description, - format_of_work_title, - type_of_employment, - contact_info, - hiring_process, - location, -}: IVacancyParsed): string => { - const header = `${title}` + `${location ? `\n${location}` : ""}`; - const formatOfWork = - `${VacancyFieldLabel.FormatOfWork}: #${format_of_work_title} #${type_of_employment}\n` + - `${format_of_work_description ? `${format_of_work_description}\n` : ""}`; - const salaryBlock = - salary_amount_from || salary_amount_to - ? `${VacancyFieldLabel.Salary}: ${ - salary_amount_from - ? `от ${salary_amount_from}${salary_currency || ""} ` - : "" - }${ - salary_amount_to - ? `до ${salary_amount_to}${salary_currency || ""}` - : "" - }${salary_type ? ` (${salary_type})` : ""}\n` - : ""; +}: Pick< + IVacancyParsed, + "salary_amount_from" | "salary_amount_to" | "salary_currency" | "salary_type" +>): string => + salary_amount_from || salary_amount_to + ? `${VacancyFieldLabel.Salary}: ${ + salary_amount_from + ? `от ${salary_amount_from}${salary_currency || ""} ` + : "" + }${ + salary_amount_to ? `до ${salary_amount_to}${salary_currency || ""}` : "" + }${salary_type ? ` (${salary_type})` : ""}\n` + : ""; - return ( - `${header}\n\n` + +export const buildMessageFromVacancy = ( + { + title, + description, + company_name, + company_description, + salary_amount_from, + salary_amount_to, + salary_currency, + salary_type, + format_of_work_description, + format_of_work_title, + type_of_employment, + contact_info, + hiring_process, + location, + }: IVacancyParsed, + parsedEntities?: IParsedMessageEntity[] +): string => { + let result = + `${title}\n\n` + `${VacancyFieldLabel.Company}: ${company_name}\n` + - `${formatOfWork}` + - `${salaryBlock}` + - `${VacancyFieldLabel.Contacts}: ${contact_info}\n\n` + - `${VacancyFieldLabel.Description}: ${description}\n\n` + + `${location ? `${VacancyFieldLabel.Location}: ${location}\n` : ""}` + + `${ + VacancyFieldLabel.FormatOfWork + }: #${format_of_work_title} #${type_of_employment}${ + format_of_work_description ? `, ${format_of_work_description}` : "" + }\n` + + `${getSalaryInfo({ + salary_amount_from, + salary_amount_to, + salary_currency, + salary_type, + })}` + + `${VacancyFieldLabel.Contacts}: ${contact_info}\n` + `${ hiring_process - ? `${VacancyFieldLabel.HiringProcess}: ${hiring_process}` + ? `${VacancyFieldLabel.HiringProcess}: ${hiring_process}\n\n` : "" - }` - ); + }` + + `${company_description ? `${company_description}\n\n` : ""}` + + `${VacancyFieldLabel.Description}: ${description}`; + + if (parsedEntities?.length) { + parsedEntities.forEach(({ word, value, entity_type }) => { + if (entity_type === "text_link") { + result = result.replace(word, `${word}`); + } + }); + } + + return result; }; diff --git a/src/utils/config.ts b/src/utils/config.ts index bf9e808..47734ca 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -6,6 +6,8 @@ export interface IConfig { botToken?: string; dbUrl?: string; dbSslEnabled: boolean; + aiOrganizationId: string; + aiApiKey: string; /** contacts list of Tg chat IDs to publish vacancies to */ botContactsList: string[]; @@ -43,6 +45,8 @@ const buildConfig = (): IConfig => ({ : true, botToken: process.env.BOT_TOKEN, botConsultantUsername: process.env.BOT_CONSULTANT_USERNAME || "", + aiOrganizationId: process.env.OPENAI_ORGANIZATION_ID!, + aiApiKey: process.env.OPENAI_API_KEY!, botContactsList: (process.env.BOT_CONTACTS || "").split(",").filter(Boolean), publishConfig: { schedule: JSON.parse(process.env.PUBLISH_CONFIG || "{}"), diff --git a/src/utils/parseMessageEntities.ts b/src/utils/parseMessageEntities.ts new file mode 100644 index 0000000..25bb91a --- /dev/null +++ b/src/utils/parseMessageEntities.ts @@ -0,0 +1,40 @@ +enum MessageEntityType { + TextLink = "text_link", + Mention = "mention", + Email = "email", +} + +interface IMessageEntity { + offset: number; + length: number; + type: MessageEntityType; + url?: string; +} + +export interface IParsedMessageEntity { + word: string; + value: string; + entity_type: MessageEntityType; +} + +const SUPPORTED_MSG_ENTITIES = [MessageEntityType.TextLink]; + +export const parseMessageEntities = ( + messageText: string, + entities: IMessageEntity[] +): IParsedMessageEntity[] => { + const supportedEntities: IMessageEntity[] = entities.filter(({ type }) => + SUPPORTED_MSG_ENTITIES.includes(type) + ); + return supportedEntities.reduce( + (acc, { url, offset, length, type }) => [ + ...acc, + { + word: messageText.slice(offset, offset + length - 1), + value: url || "", + entity_type: type, + }, + ], + [] as IParsedMessageEntity[] + ); +}; From 3a7f8e7755adb817d88aeb206793380b61f02d2f Mon Sep 17 00:00:00 2001 From: marylorian Date: Wed, 12 Jul 2023 23:49:16 +0200 Subject: [PATCH 02/11] issue(2): adds logic to parse edited vacancy --- package.json | 2 +- src/constants/labels.ts | 1 + src/constants/messages.ts | 6 ++ src/services/ai/index.ts | 13 +-- src/services/ai/openai.ts | 12 +++ .../parseNewVacancyWithAI.ts} | 100 +++--------------- src/services/ai/parseUpdatedVacancyWithAI.ts | 94 ++++++++++++++++ src/services/ai/types.ts | 49 +++++++++ src/services/edit-vacancy/editVacancy.ts | 10 +- .../edit-vacancy/getVacancyEditButton.ts | 38 +++++++ .../parseUpdatedFieldsFromText.ts | 78 -------------- .../updatePrivateVacancyMessage.ts | 16 ++- .../processIncomingMessage.ts | 8 +- src/services/parse-message/index.ts | 0 .../addVacancyToPublishQueue.ts | 3 +- .../publishNextVacancyFromQueue.ts | 3 +- .../utils/getStructuredEditableVacancyText.ts | 49 +++------ .../utils/updateButtonsUnderMessage.ts | 18 ++-- .../subscribeToTextMessage.ts | 2 +- src/utils/__tests__/config.test.ts | 8 ++ src/utils/buildMessageFromVacancy.ts | 9 +- src/utils/config.ts | 4 +- src/utils/parseMessageEntities.ts | 6 +- 23 files changed, 278 insertions(+), 251 deletions(-) create mode 100644 src/services/ai/openai.ts rename src/services/{parse-message/parseVacancyWithAI.ts => ai/parseNewVacancyWithAI.ts} (58%) create mode 100644 src/services/ai/parseUpdatedVacancyWithAI.ts create mode 100644 src/services/ai/types.ts create mode 100644 src/services/edit-vacancy/getVacancyEditButton.ts delete mode 100644 src/services/edit-vacancy/parseUpdatedFieldsFromText.ts rename src/services/{parse-message => message-preview}/processIncomingMessage.ts (86%) delete mode 100644 src/services/parse-message/index.ts diff --git a/package.json b/package.json index 1e11fdb..28ffc74 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "tsc", "start": "ts-node src/index.ts", - "dev": "nodemon src/index.ts", + "dev": "nodemon --signal SIGTERM src/index.ts", "test": "jest", "lint": "eslint . --ext .ts", "prettier": "prettier --write .", diff --git a/src/constants/labels.ts b/src/constants/labels.ts index 44d28f1..b9af827 100644 --- a/src/constants/labels.ts +++ b/src/constants/labels.ts @@ -2,6 +2,7 @@ export enum VacancyFieldLabel { Title = "Название", Description = "Описание", Company = "Компания", + CompanyDescription = "О компании", HiringProcess = "Процесс найма", Salary = "Зарплата", FormatOfWork = "Формат работы", diff --git a/src/constants/messages.ts b/src/constants/messages.ts index 5a9d61b..b1b0a8a 100644 --- a/src/constants/messages.ts +++ b/src/constants/messages.ts @@ -2,6 +2,12 @@ import config from "../utils/config"; import { BotCommands } from "./actions"; import { VacancyFieldLabel } from "./labels"; +export enum MessageEntityType { + TextLink = "text_link", + Mention = "mention", + Email = "email", +} + export const welcomeMessageText = `Привет! Это бот размещения вакансий в @razrabsjobs.\n` + `Достаточно отправить текст, чтобы я сформировал объявление, ` + diff --git a/src/services/ai/index.ts b/src/services/ai/index.ts index dc16714..d296776 100644 --- a/src/services/ai/index.ts +++ b/src/services/ai/index.ts @@ -1,12 +1 @@ -import { Configuration, OpenAIApi } from "openai"; - -import config from "../../utils/config"; - -const configuration = new Configuration({ - organization: config.aiOrganizationId, - apiKey: config.aiApiKey, -}); - -const openai = new OpenAIApi(configuration); - -export default openai; +export { default as OpenAI } from "./openai"; diff --git a/src/services/ai/openai.ts b/src/services/ai/openai.ts new file mode 100644 index 0000000..dc16714 --- /dev/null +++ b/src/services/ai/openai.ts @@ -0,0 +1,12 @@ +import { Configuration, OpenAIApi } from "openai"; + +import config from "../../utils/config"; + +const configuration = new Configuration({ + organization: config.aiOrganizationId, + apiKey: config.aiApiKey, +}); + +const openai = new OpenAIApi(configuration); + +export default openai; diff --git a/src/services/parse-message/parseVacancyWithAI.ts b/src/services/ai/parseNewVacancyWithAI.ts similarity index 58% rename from src/services/parse-message/parseVacancyWithAI.ts rename to src/services/ai/parseNewVacancyWithAI.ts index 13b032d..fd5ffa8 100644 --- a/src/services/parse-message/parseVacancyWithAI.ts +++ b/src/services/ai/parseNewVacancyWithAI.ts @@ -1,56 +1,11 @@ -import { - EmploymentType, - FormatOfWork, - SalaryType, -} from "../../constants/vacancy"; -import openai from "../ai"; +import { Maybe } from "../../types/mixins"; +import { IVacancyParsed } from "../../types/vacancy"; +import openai from "./openai"; +import { IParsedVacancyByAI } from "./types"; -interface IParsedVacancyByAI { - company: { - name: string; - description: string; - }; - format_of_work_title: FormatOfWork; - format_of_work_description: { - description: string; - }; - employment_details: { - type: string; - }; - vacancy_title: { - title: string; - }; - contact_info: { - telegram?: string; - email?: string; - }; - hiring_process: { - description?: string; - }; - salary: { - min: number; - max: number; - currency: string; - taxes: SalaryType; - bonus?: unknown; - }; - hashtags: string[]; - type_of_employment: EmploymentType; - description: string[]; - // forbidden_location: { - // city: string; - // country: string; - // }[]; - location: { - address: string; - city: string; - country: string; - restrictions: string; - }[]; -} - -export const parseVacancyWithAI = async (messageText: string) => { - console.log({ messageText }); +export const parseNewVacancyWithAI = async ( + messageText: string +): Promise> => { const { data } = await openai.createChatCompletion({ model: "gpt-3.5-turbo", messages: [ @@ -64,10 +19,6 @@ export const parseVacancyWithAI = async (messageText: string) => { Fields: - company (dict of company name and description) - location (array of information about office address, city or country or special restrictions to work from some country, city) - ${ - /*`// - location (array of dicts of full address, city, country if present) - // - forbidden_location (array of dicts of city, country which are restricted or partially restricted or forbidden if present)`*/ "" - } - vacancy_title (dict of job title) - employment_details (dict of employment type as employee by local laws or as individual entrepreneur or as b2b if present) - format_of_work_title (hybrid or remote or on-site (if value is not in english translate into english), modify to lowercase) @@ -85,29 +36,16 @@ export const parseVacancyWithAI = async (messageText: string) => { temperature: 0, }); const parsedVacancy = data.choices[0].message - ? (JSON.parse( - data.choices[0].message.content || "{}" - ) as IParsedVacancyByAI) + ? JSON.parse(data.choices[0].message.content || "{}") : undefined; - console.log({ - response: data, - choises: data.choices.map(({ message }) => message), - parsedVacancy, - location: parsedVacancy?.location, - // forbidden_location: parsedVacancy?.forbidden_location, - salary: parsedVacancy?.salary, - bonus: parsedVacancy?.salary?.bonus, - }); - - if (!parsedVacancy) { + if (!parsedVacancy || !Object.keys(parsedVacancy).length) { return undefined; } const { vacancy_title, location, - // forbidden_location, salary, description, company, @@ -118,10 +56,10 @@ export const parseVacancyWithAI = async (messageText: string) => { hiring_process, hashtags, employment_details, - } = parsedVacancy; + } = parsedVacancy as IParsedVacancyByAI; return { - title: vacancy_title.title, + title: vacancy_title?.title, location: location ?.map(({ country, city, restrictions, address }) => [ @@ -134,26 +72,22 @@ export const parseVacancyWithAI = async (messageText: string) => { ].filter(Boolean) ) .join(", "), - // .join(", ") + - // `\nОграничения по локации: ${forbidden_location?.map( - // ({ country, city }) => `${country ? `${country}, ` : ""}${city}` - // )}`, salary_amount_from: salary?.min, salary_amount_to: salary?.max, - salary_currency: salary.currency, - salary_type: salary.taxes, + salary_currency: salary?.currency, + salary_type: salary?.taxes, description: description?.join("\n") || "" + `\n${hashtags?.map((w) => `#${w}`).join(" ")}`, - company_name: company.name, - company_description: company.description, + company_name: company?.name, + company_description: company?.description, type_of_employment: type_of_employment, - contact_info: [contact_info.telegram, contact_info.email] + contact_info: [contact_info?.telegram, contact_info?.email] .filter(Boolean) .join(", "), format_of_work_title: format_of_work_title, format_of_work_description: - format_of_work_description.description || employment_details.type, + format_of_work_description?.description || employment_details?.type, hiring_process: hiring_process?.description, }; }; diff --git a/src/services/ai/parseUpdatedVacancyWithAI.ts b/src/services/ai/parseUpdatedVacancyWithAI.ts new file mode 100644 index 0000000..91f0e13 --- /dev/null +++ b/src/services/ai/parseUpdatedVacancyWithAI.ts @@ -0,0 +1,94 @@ +import { VacancyFieldLabel } from "../../constants/labels"; +import { Maybe } from "../../types/mixins"; +import { IVacancyParsed } from "../../types/vacancy"; +import openai from "./openai"; +import { IParsedVacancyByAI } from "./types"; + +export const parseUpdatedVacancyWithAI = async ( + messageText: string +): Promise> => { + const { data } = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: [ + { + role: "assistant", + content: ` + For vacancy text in input below, extract the following fields + + Input: ${messageText} + + Fields: + - company_name (text after "${VacancyFieldLabel.Company}") + - company_description (text describing company, from text after "${VacancyFieldLabel.CompanyDescription}" but before "${VacancyFieldLabel.Description}" + - location (text after "${VacancyFieldLabel.Location}", extract information about office address, city or country or special restrictions to work from some country, city) + - vacancy_title (text from the first line, probably alike of job title) + - format_of_work_title (from text after "${VacancyFieldLabel.FormatOfWork}", hybrid or remote or onsite (if value is not in english translate into english), modify to lowercase) + - format_of_work_description (from text after "${VacancyFieldLabel.FormatOfWork}" and after hastags) + - contact_info (text after "${VacancyFieldLabel.Contacts}") + - hiring_process (text after "${VacancyFieldLabel.HiringProcess}") + - salary (from text after "${VacancyFieldLabel.Salary}", dict of salary or wage for job done as range of numbers from 0 to positive infinity (as dict of max and min) and currency and taxes (net or gross), note that "до вычета" is equal to gross and "чистыми" is equal to net) + - type_of_employment (from text after "${VacancyFieldLabel.FormatOfWork}", one of the following - fulltime or parttime or contract or internship) + - hashtags (array of hashtags (words started from #) if present) + - description (text strictly after "${VacancyFieldLabel.Description}:", with all newline symbols saved) + + Valid JSON Output, omit fields that are not present, return empty response if required fields are not presented`, + }, + ], + temperature: 0, + }); + const parsedVacancy = data.choices[0].message + ? JSON.parse(data.choices[0].message.content || "{}") + : undefined; + + if (!parsedVacancy || !Object.keys(parsedVacancy).length) { + return undefined; + } + + const { + vacancy_title, + location, + salary, + description, + company, + contact_info, + type_of_employment, + format_of_work_title, + format_of_work_description, + hiring_process, + hashtags, + employment_details, + } = parsedVacancy as IParsedVacancyByAI; + + return { + title: vacancy_title.title, + location: location + ?.map(({ country, city, restrictions, address }) => + [ + country, + city, + address, + restrictions + ? `\nОграничения по локации: ${restrictions}` + : undefined, + ].filter(Boolean) + ) + .join(", "), + salary_amount_from: salary?.min, + salary_amount_to: salary?.max, + salary_currency: salary?.currency, + salary_type: salary?.taxes, + description: + description?.join("\n") || + "" + `\n${hashtags?.map((w) => `#${w}`).join(" ")}`, + company_name: company?.name, + company_description: company?.description, + type_of_employment: type_of_employment, + contact_info: [contact_info?.telegram, contact_info?.email] + .filter(Boolean) + .join(", "), + format_of_work_title: format_of_work_title, + format_of_work_description: + format_of_work_description?.description || employment_details?.type, + hiring_process: hiring_process?.description, + }; +}; diff --git a/src/services/ai/types.ts b/src/services/ai/types.ts new file mode 100644 index 0000000..af366c8 --- /dev/null +++ b/src/services/ai/types.ts @@ -0,0 +1,49 @@ +import { + EmploymentType, + FormatOfWork, + SalaryType, +} from "../../constants/vacancy"; + +export interface IParsedVacancyByAI { + company: { + name: string; + description: string; + }; + format_of_work_title: FormatOfWork; + format_of_work_description: { + description: string; + }; + employment_details: { + type: string; + }; + vacancy_title: { + title: string; + }; + contact_info: { + telegram?: string; + email?: string; + }; + hiring_process: { + description?: string; + }; + salary: { + min: number; + max: number; + currency: string; + taxes: SalaryType; + bonus?: unknown; + }; + hashtags: string[]; + type_of_employment: EmploymentType; + description: string[]; + // forbidden_location: { + // city: string; + // country: string; + // }[]; + location: { + address: string; + city: string; + country: string; + restrictions: string; + }[]; +} diff --git a/src/services/edit-vacancy/editVacancy.ts b/src/services/edit-vacancy/editVacancy.ts index b37e701..1f17fcd 100644 --- a/src/services/edit-vacancy/editVacancy.ts +++ b/src/services/edit-vacancy/editVacancy.ts @@ -1,6 +1,6 @@ import Vacancies from "../../schemas/vacancy"; +import { parseUpdatedVacancyWithAI } from "../ai/parseUpdatedVacancyWithAI"; import logger from "../logger"; -import { parseUpdatedFieldsFromText } from "./parseUpdatedFieldsFromText"; import { updatePrivateVacancyMessage } from "./updatePrivateVacancyMessage"; import { updatePublicGroupVacancyMessage } from "./updatePublicGroupVacancyMessage"; @@ -38,10 +38,10 @@ export const onVacancyEdit = async ( } vacancyTitle = vacancy.title; - const updatedVacancyFields = parseUpdatedFieldsFromText( - vacancy, - updatedText - ); + const updatedVacancyFields = await parseUpdatedVacancyWithAI(updatedText); + + // TODO: add some validation here + // https://github.com/openworld-community/rzrbs-vacancy-bot/issues/24 logger.info(`Edited fields parsed, updating vacancy in DB...`); for (const key in updatedVacancyFields) { diff --git a/src/services/edit-vacancy/getVacancyEditButton.ts b/src/services/edit-vacancy/getVacancyEditButton.ts new file mode 100644 index 0000000..31f54c7 --- /dev/null +++ b/src/services/edit-vacancy/getVacancyEditButton.ts @@ -0,0 +1,38 @@ +import { Markup } from "telegraf"; +import { InlineKeyboardButton } from "telegraf/typings/core/types/typegram"; + +import { ActionButtonLabels, BotActions } from "../../constants/actions"; +import { getStructuredEditableVacancyText } from "../publish-vacancy/utils/getStructuredEditableVacancyText"; + +interface GetVacancyEditButtonParams { + messageId: number; + chatId: number; + fromUsername: string; + text?: string; +} + +export const getVacancyEditButton = async ({ + messageId, + chatId, + fromUsername, + text, +}: GetVacancyEditButtonParams): Promise => { + if (!messageId || !chatId || !fromUsername) { + throw Error( + `getVacancyEditButton: cannot retrieve required info - ` + + `messageId: ${messageId}, ` + + `chatId: ${chatId}, ` + + `fromUsername: ${fromUsername}` + ); + } + + return Markup.button.switchToCurrentChat( + ActionButtonLabels[BotActions.EditVacancy], + await getStructuredEditableVacancyText({ + messageId, + chatId, + fromUsername, + text, + }) + ); +}; diff --git a/src/services/edit-vacancy/parseUpdatedFieldsFromText.ts b/src/services/edit-vacancy/parseUpdatedFieldsFromText.ts deleted file mode 100644 index 5c4cd07..0000000 --- a/src/services/edit-vacancy/parseUpdatedFieldsFromText.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { VacancyFieldLabel } from "../../constants/labels"; -import { - EmploymentType, - FormatOfWork, - SalaryType, -} from "../../constants/vacancy"; -import { DeepPartial } from "../../types/mixins"; -import { IVacancyModel } from "../../types/vacancy"; -import logger from "../logger"; - -const getFieldByLabel = (str, label): string | undefined => - str?.split(`${label}: `)?.[1]; - -export const parseUpdatedFieldsFromText = ( - vacancy: IVacancyModel, - updatedText: string -): DeepPartial => { - try { - const [ - title, - companyName, - hiringProcess, - salaryInfo, - formatOfWorkInfo, - typeOfEmployment, - location, - contactInfo, - ] = updatedText?.split("\n") || []; - - const salaryString = getFieldByLabel(salaryInfo, VacancyFieldLabel.Salary); - const [salaryAmount, salaryCurrency, salaryType] = salaryString - ? salaryString?.split(" ") || [] - : []; - const [salaryAmountFrom, salaryAmountTo] = salaryAmount?.split("-") || []; - - const formatOfWorkString = getFieldByLabel( - formatOfWorkInfo, - VacancyFieldLabel.FormatOfWork - ); - const [formatOfWorkTitle, formatOfWorkDescription] = - formatOfWorkString?.split(". ") || []; - - const parsedFields: DeepPartial = { - title: getFieldByLabel(title, VacancyFieldLabel.Title), - description: getFieldByLabel(updatedText, VacancyFieldLabel.Description), - company_name: getFieldByLabel(companyName, VacancyFieldLabel.Company), - hiring_process: getFieldByLabel( - hiringProcess, - VacancyFieldLabel.HiringProcess - ), - salary_amount_from: Number(salaryAmountFrom) || undefined, - salary_amount_to: Number(salaryAmountTo) || undefined, - salary_currency: salaryCurrency, - salary_type: salaryType - ? (salaryType.slice(1, salaryType.length - 1) as SalaryType) - : undefined, - format_of_work_title: formatOfWorkTitle as FormatOfWork, - format_of_work_description: formatOfWorkDescription, - type_of_employment: getFieldByLabel( - typeOfEmployment, - VacancyFieldLabel.TypeOfEmployment - ) as EmploymentType, - location: getFieldByLabel(location, VacancyFieldLabel.Location), - contact_info: getFieldByLabel(contactInfo, VacancyFieldLabel.Contacts), - }; - - return parsedFields; - } catch (err) { - logger.error( - `Failed to parse edited ${vacancy?.author_username}::${ - vacancy?.tg_chat_id - }::${vacancy?.tg_message_id} vacancy fields - ${ - (err as Error)?.message || JSON.stringify(err) - }}` - ); - return {}; - } -}; diff --git a/src/services/edit-vacancy/updatePrivateVacancyMessage.ts b/src/services/edit-vacancy/updatePrivateVacancyMessage.ts index 18ede3c..5c95a68 100644 --- a/src/services/edit-vacancy/updatePrivateVacancyMessage.ts +++ b/src/services/edit-vacancy/updatePrivateVacancyMessage.ts @@ -5,7 +5,7 @@ import { BotContext } from "../../types/context"; import { IVacancyModel } from "../../types/vacancy"; import { buildMessageFromVacancy } from "../../utils/buildMessageFromVacancy"; import logger from "../logger"; -import { getStructuredEditableVacancyText } from "../publish-vacancy/utils/getStructuredEditableVacancyText"; +import { getVacancyEditButton } from "./getVacancyEditButton"; export const updatePrivateVacancyMessage = async ({ ctx, @@ -22,14 +22,12 @@ export const updatePrivateVacancyMessage = async ({ }) => { try { const updatedInlineMarkup = Markup.inlineKeyboard([ - Markup.button.switchToCurrentChat( - ActionButtonLabels[BotActions.EditVacancy], - await getStructuredEditableVacancyText({ - messageId, - chatId, - fromUsername, - }) - ), + await getVacancyEditButton({ + messageId, + chatId, + fromUsername, + text: buildMessageFromVacancy(vacancy), + }), Markup.button.callback( ActionButtonLabels[BotActions.RevokeVacancy], BotActions.RevokeVacancy diff --git a/src/services/parse-message/processIncomingMessage.ts b/src/services/message-preview/processIncomingMessage.ts similarity index 86% rename from src/services/parse-message/processIncomingMessage.ts rename to src/services/message-preview/processIncomingMessage.ts index 9c57c1b..fd353f6 100644 --- a/src/services/parse-message/processIncomingMessage.ts +++ b/src/services/message-preview/processIncomingMessage.ts @@ -1,8 +1,8 @@ import { parseMessageEntities } from "../../utils/parseMessageEntities"; +import { parseNewVacancyWithAI } from "../ai/parseNewVacancyWithAI"; import { onVacancyEdit } from "../edit-vacancy"; -import { logger } from "../index"; -import { sendMessagePreview } from "../message-preview"; -import { parseVacancyWithAI } from "./parseVacancyWithAI"; +import logger from "../logger"; +import { sendMessagePreview } from "./sendMessagePreview"; export const processIncomingMessage = async (ctx) => { const { message_id, from, text, chat, entities } = ctx?.update?.message || {}; @@ -27,7 +27,7 @@ export const processIncomingMessage = async (ctx) => { return; } - const parsedMessage = await parseVacancyWithAI(text); + const parsedMessage = await parseNewVacancyWithAI(text); if (!parsedMessage) { throw Error("failed to parse vacancy with AI"); diff --git a/src/services/parse-message/index.ts b/src/services/parse-message/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/services/publish-vacancy/addVacancyToPublishQueue.ts b/src/services/publish-vacancy/addVacancyToPublishQueue.ts index 19aeba5..0aedcc6 100644 --- a/src/services/publish-vacancy/addVacancyToPublishQueue.ts +++ b/src/services/publish-vacancy/addVacancyToPublishQueue.ts @@ -31,8 +31,7 @@ export const onPublishVacancy = async (ctx) => { } const isPublishingAllowed = await isPublishingAllowedForUser(chat.username); - - if (isPublishingAllowed) { + if (!isPublishingAllowed) { await ctx.sendMessage(vacancyLimitExceededMessageText); logger.warn( diff --git a/src/services/publish-vacancy/publishNextVacancyFromQueue.ts b/src/services/publish-vacancy/publishNextVacancyFromQueue.ts index 4645eea..f6ec7db 100644 --- a/src/services/publish-vacancy/publishNextVacancyFromQueue.ts +++ b/src/services/publish-vacancy/publishNextVacancyFromQueue.ts @@ -18,7 +18,8 @@ export const publishNextVacancyFromQueue = async () => { logger.info("Publish Queue: working day finished"); } - if (await isVacancyPublishingAllowedToday()) { + const isPublishingAllowed = await isVacancyPublishingAllowedToday(); + if (!isPublishingAllowed) { logger.info( `Publish Next Vacancy: canceled, because daily limit reached ${config.publishConfig.dailyVacancyLimit}` ); diff --git a/src/services/publish-vacancy/utils/getStructuredEditableVacancyText.ts b/src/services/publish-vacancy/utils/getStructuredEditableVacancyText.ts index b82e178..c843cd8 100644 --- a/src/services/publish-vacancy/utils/getStructuredEditableVacancyText.ts +++ b/src/services/publish-vacancy/utils/getStructuredEditableVacancyText.ts @@ -1,19 +1,28 @@ -import { VacancyFieldLabel } from "../../../constants/labels"; import VacancyModel from "../../../schemas/vacancy"; +import { buildMessageFromVacancy } from "../../../utils/buildMessageFromVacancy"; import logger from "../../logger"; export const getStructuredEditableVacancyText = async ({ messageId, chatId, fromUsername, + text, }: { messageId: number; chatId: number; fromUsername: string; + text?: string; }): Promise => { try { - if (!chatId || !messageId || !fromUsername) { - throw Error("cannot retrieve required info"); + if (!chatId || !messageId || !fromUsername || !text) { + throw Error( + `cannot retrieve required info - ${{ + chatId, + messageId, + fromUsername, + text, + }}` + ); } const vacancy = await VacancyModel.findOne({ @@ -28,41 +37,9 @@ export const getStructuredEditableVacancyText = async ({ throw Error("vacancy not found"); } - const { - title, - description, - company_name, - hiring_process, - salary_amount_from, - salary_amount_to, - salary_currency, - salary_type, - format_of_work_description, - format_of_work_title, - type_of_employment, - location, - contact_info, - } = vacancy; - const salaryString = - salary_amount_from || salary_amount_to - ? `${salary_amount_from}-${salary_amount_to} ${salary_currency || ""}${ - salary_type ? ` (${salary_type})` : "" - }` - : ""; - return ( `> ${messageId}\nПожалуйста, не изменяйте информацию выше.\n\n` + - `${VacancyFieldLabel.Title}: ${title}\n` + - `${VacancyFieldLabel.Company}: ${company_name || ""}\n` + - `${VacancyFieldLabel.HiringProcess}: ${hiring_process || ""}\n` + - `${VacancyFieldLabel.Salary}: ${salaryString}\n` + - `${VacancyFieldLabel.FormatOfWork}: ${format_of_work_title}. ${ - format_of_work_description || "" - }\n` + - `${VacancyFieldLabel.TypeOfEmployment}: ${type_of_employment}\n` + - `${VacancyFieldLabel.Location}: ${location || ""}\n` + - `${VacancyFieldLabel.Contacts}: ${contact_info}\n` + - `${VacancyFieldLabel.Description}: ${description}` + `${buildMessageFromVacancy(vacancy)}` ); } catch (err) { logger.error( diff --git a/src/services/publish-vacancy/utils/updateButtonsUnderMessage.ts b/src/services/publish-vacancy/utils/updateButtonsUnderMessage.ts index 98092b1..7ba18ed 100644 --- a/src/services/publish-vacancy/utils/updateButtonsUnderMessage.ts +++ b/src/services/publish-vacancy/utils/updateButtonsUnderMessage.ts @@ -1,24 +1,22 @@ import { Markup } from "telegraf"; import { ActionButtonLabels, BotActions } from "../../../constants/actions"; +import { getVacancyEditButton } from "../../edit-vacancy/getVacancyEditButton"; import logger from "../../logger"; -import { getStructuredEditableVacancyText } from "./getStructuredEditableVacancyText"; export const updateButtonsUnderMessage = async (ctx) => { - const { message_id, chat } = ctx?.update?.callback_query?.message || {}; + const { message_id, chat, text } = ctx?.update?.callback_query?.message || {}; try { await ctx?.editMessageReplyMarkup({ inline_keyboard: [ [ - Markup.button.switchToCurrentChat( - ActionButtonLabels[BotActions.EditVacancy], - await getStructuredEditableVacancyText({ - messageId: message_id, - chatId: chat?.id, - fromUsername: chat?.username, - }) - ), + await getVacancyEditButton({ + messageId: message_id, + chatId: chat?.id, + fromUsername: chat?.username, + text, + }), Markup.button.callback( ActionButtonLabels[BotActions.RevokeVacancy], BotActions.RevokeVacancy diff --git a/src/services/subscribe-to-actions/subscribeToTextMessage.ts b/src/services/subscribe-to-actions/subscribeToTextMessage.ts index dd0969c..9164763 100644 --- a/src/services/subscribe-to-actions/subscribeToTextMessage.ts +++ b/src/services/subscribe-to-actions/subscribeToTextMessage.ts @@ -1,7 +1,7 @@ import { message } from "telegraf/filters"; import bot from "../../launchBot"; -import { processIncomingMessage } from "../parse-message/processIncomingMessage"; +import { processIncomingMessage } from "../message-preview/processIncomingMessage"; export const subscribeToTextMessage = () => { bot.on(message("text"), processIncomingMessage); diff --git a/src/utils/__tests__/config.test.ts b/src/utils/__tests__/config.test.ts index 3fbeb69..bd39c61 100644 --- a/src/utils/__tests__/config.test.ts +++ b/src/utils/__tests__/config.test.ts @@ -15,6 +15,8 @@ describe("utils/config", () => { '{ "mon": [10,18], "tue": [10,18], "wed": [10,18], "thu": [10,18], "fri": [10,18], "sat": [17, 18] }'; process.env.DB_URL = "postgres://test-path:5432/mydb"; process.env.DB_SSL_ENABLED = "false"; + process.env.OPENAI_API_KEY = "test-api-key"; + process.env.OPENAI_ORGANIZATION_ID = "test-org"; // eslint-disable-next-line @typescript-eslint/no-var-requires const config = require("../config").default; @@ -26,6 +28,8 @@ describe("utils/config", () => { dbSslEnabled: false, botContactsList: ["chat1", "chat2", "chat3"], botConsultantUsername: "test_username", + aiApiKey: "test-api-key", + aiOrganizationId: "test-org", publishConfig: { dailyVacancyLimit: 10, minPublishInterval: 10, @@ -49,6 +53,8 @@ describe("utils/config", () => { process.env.NODE_ENV = Environment.Test; process.env.BOT_TOKEN = "test-token:123"; process.env.DB_URL = "postgres://test-path:5432/mydb"; + process.env.OPENAI_API_KEY = "test-api-key"; + process.env.OPENAI_ORGANIZATION_ID = "test-org"; delete process.env.BOT_CONTACTS; delete process.env.MIN_PUBLISH_INTERVAL; delete process.env.PUBLISH_INTERVAL; @@ -65,6 +71,8 @@ describe("utils/config", () => { environment: Environment.Test, botToken: "test-token:123", dbUrl: "postgres://test-path:5432/mydb", + aiApiKey: "test-api-key", + aiOrganizationId: "test-org", dbSslEnabled: true, botContactsList: [], botConsultantUsername: "", diff --git a/src/utils/buildMessageFromVacancy.ts b/src/utils/buildMessageFromVacancy.ts index c01a6de..b282cd3 100644 --- a/src/utils/buildMessageFromVacancy.ts +++ b/src/utils/buildMessageFromVacancy.ts @@ -1,4 +1,5 @@ import { VacancyFieldLabel } from "../constants/labels"; +import { MessageEntityType } from "../constants/messages"; import { IVacancyParsed } from "../types/vacancy"; import { IParsedMessageEntity } from "./parseMessageEntities"; @@ -61,12 +62,16 @@ export const buildMessageFromVacancy = ( ? `${VacancyFieldLabel.HiringProcess}: ${hiring_process}\n\n` : "" }` + - `${company_description ? `${company_description}\n\n` : ""}` + + `${ + company_description + ? `${VacancyFieldLabel.CompanyDescription}: ${company_description}\n\n` + : "" + }` + `${VacancyFieldLabel.Description}: ${description}`; if (parsedEntities?.length) { parsedEntities.forEach(({ word, value, entity_type }) => { - if (entity_type === "text_link") { + if (entity_type === MessageEntityType.TextLink) { result = result.replace(word, `${word}`); } }); diff --git a/src/utils/config.ts b/src/utils/config.ts index 47734ca..5f79754 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -45,8 +45,8 @@ const buildConfig = (): IConfig => ({ : true, botToken: process.env.BOT_TOKEN, botConsultantUsername: process.env.BOT_CONSULTANT_USERNAME || "", - aiOrganizationId: process.env.OPENAI_ORGANIZATION_ID!, - aiApiKey: process.env.OPENAI_API_KEY!, + aiOrganizationId: process.env.OPENAI_ORGANIZATION_ID, + aiApiKey: process.env.OPENAI_API_KEY, botContactsList: (process.env.BOT_CONTACTS || "").split(",").filter(Boolean), publishConfig: { schedule: JSON.parse(process.env.PUBLISH_CONFIG || "{}"), diff --git a/src/utils/parseMessageEntities.ts b/src/utils/parseMessageEntities.ts index 25bb91a..6576238 100644 --- a/src/utils/parseMessageEntities.ts +++ b/src/utils/parseMessageEntities.ts @@ -1,8 +1,4 @@ -enum MessageEntityType { - TextLink = "text_link", - Mention = "mention", - Email = "email", -} +import { MessageEntityType } from "../constants/messages"; interface IMessageEntity { offset: number; From 6f3a9a78ae46500ddd3126d06dbad38e56672cc2 Mon Sep 17 00:00:00 2001 From: marylorian Date: Thu, 13 Jul 2023 23:22:58 +0200 Subject: [PATCH 03/11] issue(2): minor tsx fix --- src/utils/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/config.ts b/src/utils/config.ts index 5f79754..4406e36 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -45,8 +45,8 @@ const buildConfig = (): IConfig => ({ : true, botToken: process.env.BOT_TOKEN, botConsultantUsername: process.env.BOT_CONSULTANT_USERNAME || "", - aiOrganizationId: process.env.OPENAI_ORGANIZATION_ID, - aiApiKey: process.env.OPENAI_API_KEY, + aiOrganizationId: process.env.OPENAI_ORGANIZATION_ID || "", + aiApiKey: process.env.OPENAI_API_KEY || "", botContactsList: (process.env.BOT_CONTACTS || "").split(",").filter(Boolean), publishConfig: { schedule: JSON.parse(process.env.PUBLISH_CONFIG || "{}"), From de6351576973cb0ae97a63132a5e4f949e055982 Mon Sep 17 00:00:00 2001 From: marylorian Date: Sun, 23 Jul 2023 01:09:28 +0200 Subject: [PATCH 04/11] issue(2): hides Edit btn for Prod env, fixes Edit prompt --- src/services/ai/parseUpdatedVacancyWithAI.ts | 35 ++++++------------- src/services/ai/types.ts | 17 ++++++--- .../edit-vacancy/getVacancyEditButton.ts | 15 +++++++- .../updatePrivateVacancyMessage.ts | 27 +++++++------- .../utils/updateButtonsUnderMessage.ts | 3 +- 5 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/services/ai/parseUpdatedVacancyWithAI.ts b/src/services/ai/parseUpdatedVacancyWithAI.ts index 91f0e13..a4f126b 100644 --- a/src/services/ai/parseUpdatedVacancyWithAI.ts +++ b/src/services/ai/parseUpdatedVacancyWithAI.ts @@ -2,7 +2,7 @@ import { VacancyFieldLabel } from "../../constants/labels"; import { Maybe } from "../../types/mixins"; import { IVacancyParsed } from "../../types/vacancy"; import openai from "./openai"; -import { IParsedVacancyByAI } from "./types"; +import { IParsedEditedVacancyByAI } from "./types"; export const parseUpdatedVacancyWithAI = async ( messageText: string @@ -18,10 +18,10 @@ export const parseUpdatedVacancyWithAI = async ( Input: ${messageText} Fields: + - vacancy_title (text from the first line, probably alike of job title) - company_name (text after "${VacancyFieldLabel.Company}") - company_description (text describing company, from text after "${VacancyFieldLabel.CompanyDescription}" but before "${VacancyFieldLabel.Description}" - location (text after "${VacancyFieldLabel.Location}", extract information about office address, city or country or special restrictions to work from some country, city) - - vacancy_title (text from the first line, probably alike of job title) - format_of_work_title (from text after "${VacancyFieldLabel.FormatOfWork}", hybrid or remote or onsite (if value is not in english translate into english), modify to lowercase) - format_of_work_description (from text after "${VacancyFieldLabel.FormatOfWork}" and after hastags) - contact_info (text after "${VacancyFieldLabel.Contacts}") @@ -49,7 +49,8 @@ export const parseUpdatedVacancyWithAI = async ( location, salary, description, - company, + company_name, + company_description, contact_info, type_of_employment, format_of_work_title, @@ -57,35 +58,21 @@ export const parseUpdatedVacancyWithAI = async ( hiring_process, hashtags, employment_details, - } = parsedVacancy as IParsedVacancyByAI; + } = parsedVacancy as IParsedEditedVacancyByAI; return { - title: vacancy_title.title, - location: location - ?.map(({ country, city, restrictions, address }) => - [ - country, - city, - address, - restrictions - ? `\nОграничения по локации: ${restrictions}` - : undefined, - ].filter(Boolean) - ) - .join(", "), + title: vacancy_title, + location, salary_amount_from: salary?.min, salary_amount_to: salary?.max, salary_currency: salary?.currency, salary_type: salary?.taxes, description: - description?.join("\n") || - "" + `\n${hashtags?.map((w) => `#${w}`).join(" ")}`, - company_name: company?.name, - company_description: company?.description, + description || "" + `\n${hashtags?.map((w) => `#${w}`).join(" ")}`, + company_name, + company_description, type_of_employment: type_of_employment, - contact_info: [contact_info?.telegram, contact_info?.email] - .filter(Boolean) - .join(", "), + contact_info, format_of_work_title: format_of_work_title, format_of_work_description: format_of_work_description?.description || employment_details?.type, diff --git a/src/services/ai/types.ts b/src/services/ai/types.ts index af366c8..c6b01e0 100644 --- a/src/services/ai/types.ts +++ b/src/services/ai/types.ts @@ -36,10 +36,6 @@ export interface IParsedVacancyByAI { hashtags: string[]; type_of_employment: EmploymentType; description: string[]; - // forbidden_location: { - // city: string; - // country: string; - // }[]; location: { address: string; city: string; @@ -47,3 +43,16 @@ export interface IParsedVacancyByAI { restrictions: string; }[]; } + +export interface IParsedEditedVacancyByAI + extends Omit< + IParsedVacancyByAI, + "location" | "description" | "contact_info" | "company" | "vacancy_title" + > { + location: string; + description: string; + contact_info: string; + company_name: string; + company_description: string; + vacancy_title: string; +} diff --git a/src/services/edit-vacancy/getVacancyEditButton.ts b/src/services/edit-vacancy/getVacancyEditButton.ts index 31f54c7..eb71649 100644 --- a/src/services/edit-vacancy/getVacancyEditButton.ts +++ b/src/services/edit-vacancy/getVacancyEditButton.ts @@ -2,6 +2,9 @@ import { Markup } from "telegraf"; import { InlineKeyboardButton } from "telegraf/typings/core/types/typegram"; import { ActionButtonLabels, BotActions } from "../../constants/actions"; +import { Environment } from "../../types/common"; +import config from "../../utils/config"; +import logger from "../logger"; import { getStructuredEditableVacancyText } from "../publish-vacancy/utils/getStructuredEditableVacancyText"; interface GetVacancyEditButtonParams { @@ -16,7 +19,17 @@ export const getVacancyEditButton = async ({ chatId, fromUsername, text, -}: GetVacancyEditButtonParams): Promise => { +}: GetVacancyEditButtonParams): Promise< + InlineKeyboardButton.SwitchInlineCurrentChatButton | undefined +> => { + const isButtonHidden = config.environment === Environment.Prod; + if (isButtonHidden) { + logger.warn( + `getVacancyEditButton: Vacancy Edit function is disabled for Prod env` + ); + return undefined; + } + if (!messageId || !chatId || !fromUsername) { throw Error( `getVacancyEditButton: cannot retrieve required info - ` + diff --git a/src/services/edit-vacancy/updatePrivateVacancyMessage.ts b/src/services/edit-vacancy/updatePrivateVacancyMessage.ts index 5c95a68..ba08bb6 100644 --- a/src/services/edit-vacancy/updatePrivateVacancyMessage.ts +++ b/src/services/edit-vacancy/updatePrivateVacancyMessage.ts @@ -1,4 +1,5 @@ import { Markup, Telegraf } from "telegraf"; +import { InlineKeyboardButton } from "telegraf/typings/core/types/typegram"; import { ActionButtonLabels, BotActions } from "../../constants/actions"; import { BotContext } from "../../types/context"; @@ -21,18 +22,20 @@ export const updatePrivateVacancyMessage = async ({ vacancy: IVacancyModel; }) => { try { - const updatedInlineMarkup = Markup.inlineKeyboard([ - await getVacancyEditButton({ - messageId, - chatId, - fromUsername, - text: buildMessageFromVacancy(vacancy), - }), - Markup.button.callback( - ActionButtonLabels[BotActions.RevokeVacancy], - BotActions.RevokeVacancy - ), - ]); + const updatedInlineMarkup = Markup.inlineKeyboard( + [ + await getVacancyEditButton({ + messageId, + chatId, + fromUsername, + text: buildMessageFromVacancy(vacancy), + }), + Markup.button.callback( + ActionButtonLabels[BotActions.RevokeVacancy], + BotActions.RevokeVacancy + ), + ].filter(Boolean) as InlineKeyboardButton[] + ); await ctx.telegram.editMessageText( chatId, diff --git a/src/services/publish-vacancy/utils/updateButtonsUnderMessage.ts b/src/services/publish-vacancy/utils/updateButtonsUnderMessage.ts index 7ba18ed..b6d5a82 100644 --- a/src/services/publish-vacancy/utils/updateButtonsUnderMessage.ts +++ b/src/services/publish-vacancy/utils/updateButtonsUnderMessage.ts @@ -1,4 +1,5 @@ import { Markup } from "telegraf"; +import { InlineKeyboardButton } from "telegraf/typings/core/types/typegram"; import { ActionButtonLabels, BotActions } from "../../../constants/actions"; import { getVacancyEditButton } from "../../edit-vacancy/getVacancyEditButton"; @@ -21,7 +22,7 @@ export const updateButtonsUnderMessage = async (ctx) => { ActionButtonLabels[BotActions.RevokeVacancy], BotActions.RevokeVacancy ), - ], + ].filter(Boolean) as InlineKeyboardButton[], ], }); } catch (err) { From ca3f481fced2c87b2b052a1ca746bf69ca360d1f Mon Sep 17 00:00:00 2001 From: marylorian Date: Thu, 27 Jul 2023 22:12:41 +0200 Subject: [PATCH 05/11] issue(24): adds logic to validate new vacancy, adds error message --- src/constants/labels.ts | 3 ++ src/constants/messages.ts | 7 +++ src/schemas/vacancy.ts | 8 +++- src/services/ai/parseNewVacancyWithAI.ts | 14 +++--- src/services/ai/parseUpdatedVacancyWithAI.ts | 13 +++-- src/services/ai/types.ts | 3 +- .../message-preview/processIncomingMessage.ts | 48 +++++++++++++++++-- src/types/vacancy.ts | 2 + 8 files changed, 80 insertions(+), 18 deletions(-) diff --git a/src/constants/labels.ts b/src/constants/labels.ts index b9af827..437b833 100644 --- a/src/constants/labels.ts +++ b/src/constants/labels.ts @@ -9,4 +9,7 @@ export enum VacancyFieldLabel { TypeOfEmployment = "Тип трудоустройства", Location = "Локация", Contacts = "Контакты", + WorkExperience = "Опыт работы", } + +export const NEGOTIABLE_SALARY = "по договоренности"; diff --git a/src/constants/messages.ts b/src/constants/messages.ts index b1b0a8a..f6a7b43 100644 --- a/src/constants/messages.ts +++ b/src/constants/messages.ts @@ -34,3 +34,10 @@ export const vacancyTemplateHTMLMessageText = `${VacancyFieldLabel.Description}: Всё, что описывает вакансию, обязанности и предложения`; export const vacancyLimitExceededMessageText = `Достигнут лимит бесплатных публикаций в этом месяце. Свяжись с @${config.botConsultantUsername} чтобы разместить больше`; + +export const getMissingRequiredFieldsMessage = ( + fieldLabels: VacancyFieldLabel[] +) => + `Не указаны: ${fieldLabels.join(", ")}. ` + + `Соискатели часто обращают на эти поля свое внимание, пожалуйста, заполни их. ` + + `Для ознакомления с правилами публикации и форматом вакансии воспользуйся командой /help`; diff --git a/src/schemas/vacancy.ts b/src/schemas/vacancy.ts index 43075d1..11bfcba 100644 --- a/src/schemas/vacancy.ts +++ b/src/schemas/vacancy.ts @@ -42,7 +42,7 @@ export const VacancyModel = db.define( }, tg_message_id: { type: DataTypes.INTEGER, validate: { notEmpty: true } }, tg_chat_id: { type: DataTypes.INTEGER, validate: { notEmpty: true } }, - hiring_process: DataTypes.STRING, + hiring_process: DataTypes.STRING(500), location: DataTypes.STRING, contact_info: { type: DataTypes.STRING, @@ -59,6 +59,7 @@ export const VacancyModel = db.define( salary_amount_to: { type: DataTypes.INTEGER, validate: { min: 0 } }, salary_currency: DataTypes.STRING, salary_type: { type: DataTypes.ENUM(...Object.values(SalaryType)) }, + salary_negotiable: { type: DataTypes.BOOLEAN, defaultValue: false }, /* Format of work */ format_of_work_title: { @@ -68,6 +69,11 @@ export const VacancyModel = db.define( }, // in case we want to explain it more - like "hybrid, 2 days a week work from office" format_of_work_description: DataTypes.STRING, + work_experience: { + type: DataTypes.STRING(500), + allowNull: false, + validate: { notEmpty: true }, + }, company_name: { type: DataTypes.STRING, allowNull: false }, company_description: DataTypes.STRING, diff --git a/src/services/ai/parseNewVacancyWithAI.ts b/src/services/ai/parseNewVacancyWithAI.ts index fd5ffa8..8cfb5c5 100644 --- a/src/services/ai/parseNewVacancyWithAI.ts +++ b/src/services/ai/parseNewVacancyWithAI.ts @@ -26,9 +26,10 @@ export const parseNewVacancyWithAI = async ( - contact_info (dict of mobile and landline phone numbers if present, or telegram nickname (add @ before it) or email address or full site url) - hiring_process (dict of description of hiring process, if present) - salary (dict of salary or wage for job done as range of numbers from 0 to positive infinity (as dict of max and min) and currency and taxes (net or gross), note that "до вычета" is equal to gross and "чистыми" is equal to net) + - salary_negotiable (set true if find "по договоренности" or "по результатам собеседования" instead of salary numbers) - type_of_employment (fulltime or parttime or contract or internship) - - hashtags (array of hashtags (words started from #) if present) - - description (array of the rest of the information about skills, offers, job information, benefits, bonuses as a text with all newline symbols saved) + - work_experience (text with information about required work experience for the job) + - description (array of the rest of the information about skills, offers, job information, benefits, bonuses as a text with all newline symbols saved, please sanitize any emoji and hashtags) Valid JSON Output, omit fields that are not present, return empty response if required fields are not presented`, }, @@ -54,8 +55,9 @@ export const parseNewVacancyWithAI = async ( format_of_work_title, format_of_work_description, hiring_process, - hashtags, + salary_negotiable, employment_details, + work_experience, } = parsedVacancy as IParsedVacancyByAI; return { @@ -76,9 +78,8 @@ export const parseNewVacancyWithAI = async ( salary_amount_to: salary?.max, salary_currency: salary?.currency, salary_type: salary?.taxes, - description: - description?.join("\n") || - "" + `\n${hashtags?.map((w) => `#${w}`).join(" ")}`, + salary_negotiable, + description: description?.join("\n"), company_name: company?.name, company_description: company?.description, type_of_employment: type_of_employment, @@ -89,5 +90,6 @@ export const parseNewVacancyWithAI = async ( format_of_work_description: format_of_work_description?.description || employment_details?.type, hiring_process: hiring_process?.description, + work_experience, }; }; diff --git a/src/services/ai/parseUpdatedVacancyWithAI.ts b/src/services/ai/parseUpdatedVacancyWithAI.ts index a4f126b..80b99d5 100644 --- a/src/services/ai/parseUpdatedVacancyWithAI.ts +++ b/src/services/ai/parseUpdatedVacancyWithAI.ts @@ -27,9 +27,10 @@ export const parseUpdatedVacancyWithAI = async ( - contact_info (text after "${VacancyFieldLabel.Contacts}") - hiring_process (text after "${VacancyFieldLabel.HiringProcess}") - salary (from text after "${VacancyFieldLabel.Salary}", dict of salary or wage for job done as range of numbers from 0 to positive infinity (as dict of max and min) and currency and taxes (net or gross), note that "до вычета" is equal to gross and "чистыми" is equal to net) + - salary_negotiable (set true if find "по договоренности" or "по результатам собеседования" instead of salary numbers) - type_of_employment (from text after "${VacancyFieldLabel.FormatOfWork}", one of the following - fulltime or parttime or contract or internship) - - hashtags (array of hashtags (words started from #) if present) - - description (text strictly after "${VacancyFieldLabel.Description}:", with all newline symbols saved) + - work_experience (text with information about required work experience for the job) + - description (text strictly after "${VacancyFieldLabel.Description}:", with all newline symbols saved, please sanitize any emoji and hashtags) Valid JSON Output, omit fields that are not present, return empty response if required fields are not presented`, }, @@ -56,7 +57,8 @@ export const parseUpdatedVacancyWithAI = async ( format_of_work_title, format_of_work_description, hiring_process, - hashtags, + salary_negotiable, + work_experience, employment_details, } = parsedVacancy as IParsedEditedVacancyByAI; @@ -67,8 +69,7 @@ export const parseUpdatedVacancyWithAI = async ( salary_amount_to: salary?.max, salary_currency: salary?.currency, salary_type: salary?.taxes, - description: - description || "" + `\n${hashtags?.map((w) => `#${w}`).join(" ")}`, + description, company_name, company_description, type_of_employment: type_of_employment, @@ -77,5 +78,7 @@ export const parseUpdatedVacancyWithAI = async ( format_of_work_description: format_of_work_description?.description || employment_details?.type, hiring_process: hiring_process?.description, + salary_negotiable, + work_experience, }; }; diff --git a/src/services/ai/types.ts b/src/services/ai/types.ts index c6b01e0..2db17be 100644 --- a/src/services/ai/types.ts +++ b/src/services/ai/types.ts @@ -33,7 +33,6 @@ export interface IParsedVacancyByAI { taxes: SalaryType; bonus?: unknown; }; - hashtags: string[]; type_of_employment: EmploymentType; description: string[]; location: { @@ -42,6 +41,8 @@ export interface IParsedVacancyByAI { country: string; restrictions: string; }[]; + salary_negotiable?: boolean; + work_experience?: string; } export interface IParsedEditedVacancyByAI diff --git a/src/services/message-preview/processIncomingMessage.ts b/src/services/message-preview/processIncomingMessage.ts index fd353f6..a99c8ed 100644 --- a/src/services/message-preview/processIncomingMessage.ts +++ b/src/services/message-preview/processIncomingMessage.ts @@ -1,9 +1,42 @@ +import { VacancyFieldLabel } from "../../constants/labels"; +import { getMissingRequiredFieldsMessage } from "../../constants/messages"; +import { IVacancyParsed } from "../../types/vacancy"; import { parseMessageEntities } from "../../utils/parseMessageEntities"; import { parseNewVacancyWithAI } from "../ai/parseNewVacancyWithAI"; import { onVacancyEdit } from "../edit-vacancy"; import logger from "../logger"; import { sendMessagePreview } from "./sendMessagePreview"; +export const isRequiredVacancyFieldsFilled = ( + parsedVacancy: IVacancyParsed +): { isRequiredFieldsFilled: boolean; missingFields: VacancyFieldLabel[] } => { + if (!parsedVacancy) { + return { isRequiredFieldsFilled: false, missingFields: [] }; + } + + const missingFields: VacancyFieldLabel[] = []; + const { + title, + salary_amount_from, + salary_amount_to, + salary_negotiable, + company_name, + contact_info, + hiring_process, + work_experience, + } = parsedVacancy; + + if (!title) missingFields.push(VacancyFieldLabel.Title); + if (!company_name) missingFields.push(VacancyFieldLabel.Company); + if (!contact_info) missingFields.push(VacancyFieldLabel.Contacts); + if (!hiring_process) missingFields.push(VacancyFieldLabel.HiringProcess); + if (!salary_amount_from && !salary_amount_to && !salary_negotiable) + missingFields.push(VacancyFieldLabel.Salary); + if (!work_experience) missingFields.push(VacancyFieldLabel.WorkExperience); + + return { isRequiredFieldsFilled: !!missingFields.length, missingFields }; +}; + export const processIncomingMessage = async (ctx) => { const { message_id, from, text, chat, entities } = ctx?.update?.message || {}; @@ -30,9 +63,19 @@ export const processIncomingMessage = async (ctx) => { const parsedMessage = await parseNewVacancyWithAI(text); if (!parsedMessage) { + await ctx.sendMessage( + `Не удалось распознать вакансию с помощью AI, попробуйте еще раз` + ); throw Error("failed to parse vacancy with AI"); } + const { isRequiredFieldsFilled, missingFields } = + isRequiredVacancyFieldsFilled(parsedMessage); + if (!isRequiredFieldsFilled) { + await ctx.sendMessage(getMissingRequiredFieldsMessage(missingFields)); + throw Error(`missing fields - ${missingFields.join(", ")}`); + } + await sendMessagePreview( ctx, parsedMessage, @@ -44,10 +87,5 @@ export const processIncomingMessage = async (ctx) => { chat?.id }::${message_id} - ${(err as Error)?.message || JSON.stringify(err)}` ); - ctx.sendMessage( - `Не удалось распознать вакансию, попробуйте еще раз - ${ - (err as Error)?.message || JSON.stringify(err) - }` - ); } }; diff --git a/src/types/vacancy.ts b/src/types/vacancy.ts index 5b7b3d9..cabd422 100644 --- a/src/types/vacancy.ts +++ b/src/types/vacancy.ts @@ -29,10 +29,12 @@ export type TVacancyAttributes = { salary_amount_to?: number; salary_currency?: string; salary_type?: SalaryType; + salary_negotiable?: boolean; format_of_work_title?: FormatOfWork; format_of_work_description?: string; + work_experience?: string; type_of_employment: EmploymentType; location?: string; contact_info: string; From b49b62db9cb590d5e1cf66de1358915db1a3d535 Mon Sep 17 00:00:00 2001 From: marylorian Date: Thu, 27 Jul 2023 22:23:57 +0200 Subject: [PATCH 06/11] issue(24): adds negotiable salary to preview --- src/utils/buildMessageFromVacancy.ts | 43 ++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/utils/buildMessageFromVacancy.ts b/src/utils/buildMessageFromVacancy.ts index b282cd3..d9df976 100644 --- a/src/utils/buildMessageFromVacancy.ts +++ b/src/utils/buildMessageFromVacancy.ts @@ -1,4 +1,4 @@ -import { VacancyFieldLabel } from "../constants/labels"; +import { NEGOTIABLE_SALARY, VacancyFieldLabel } from "../constants/labels"; import { MessageEntityType } from "../constants/messages"; import { IVacancyParsed } from "../types/vacancy"; import { IParsedMessageEntity } from "./parseMessageEntities"; @@ -8,19 +8,30 @@ const getSalaryInfo = ({ salary_amount_to, salary_currency, salary_type, + salary_negotiable, }: Pick< IVacancyParsed, - "salary_amount_from" | "salary_amount_to" | "salary_currency" | "salary_type" ->): string => - salary_amount_from || salary_amount_to - ? `${VacancyFieldLabel.Salary}: ${ - salary_amount_from - ? `от ${salary_amount_from}${salary_currency || ""} ` - : "" - }${ - salary_amount_to ? `до ${salary_amount_to}${salary_currency || ""}` : "" - }${salary_type ? ` (${salary_type})` : ""}\n` - : ""; + | "salary_amount_from" + | "salary_amount_to" + | "salary_currency" + | "salary_type" + | "salary_negotiable" +>): string => { + if (salary_amount_from || salary_amount_to) { + return `${VacancyFieldLabel.Salary}: ${ + salary_amount_from + ? `от ${salary_amount_from}${salary_currency || ""} ` + : "" + }${ + salary_amount_to ? `до ${salary_amount_to}${salary_currency || ""}` : "" + }${salary_type ? ` (${salary_type})` : ""}\n`; + } + + if (salary_negotiable) + return `${VacancyFieldLabel.Salary}: ${NEGOTIABLE_SALARY}\n`; + + return ""; +}; export const buildMessageFromVacancy = ( { @@ -32,12 +43,14 @@ export const buildMessageFromVacancy = ( salary_amount_to, salary_currency, salary_type, + salary_negotiable, format_of_work_description, format_of_work_title, type_of_employment, contact_info, hiring_process, location, + work_experience, }: IVacancyParsed, parsedEntities?: IParsedMessageEntity[] ): string => { @@ -55,7 +68,13 @@ export const buildMessageFromVacancy = ( salary_amount_to, salary_currency, salary_type, + salary_negotiable, })}` + + `${ + work_experience + ? `${VacancyFieldLabel.WorkExperience}: ${work_experience}\n` + : "" + }` + `${VacancyFieldLabel.Contacts}: ${contact_info}\n` + `${ hiring_process From 5e330002b9bbab06901456dd8d3f9fa6b17ce7f6 Mon Sep 17 00:00:00 2001 From: marylorian Date: Wed, 2 Aug 2023 17:00:02 +0200 Subject: [PATCH 07/11] issue(37): adds retry parsing button with new texts --- src/constants/actions.ts | 2 + src/constants/messages.ts | 16 ++- .../constructPreviewMessage.ts | 66 ++++++++++ .../message-preview/editNewVacancy.ts | 56 +++++++++ src/services/message-preview/index.ts | 2 +- .../isRequiredVacancyFieldsFilled.ts | 32 +++++ .../message-preview/parseMessageToVacancy.ts | 14 +++ .../message-preview/processIncomingMessage.ts | 118 ++++++++++-------- .../message-preview/retryVacancyParsing.ts | 88 +++++++++++++ .../message-preview/sendMessagePreview.ts | 62 --------- .../subscribeToButtonActions.ts | 4 + 11 files changed, 343 insertions(+), 117 deletions(-) create mode 100644 src/services/message-preview/constructPreviewMessage.ts create mode 100644 src/services/message-preview/editNewVacancy.ts create mode 100644 src/services/message-preview/isRequiredVacancyFieldsFilled.ts create mode 100644 src/services/message-preview/parseMessageToVacancy.ts create mode 100644 src/services/message-preview/retryVacancyParsing.ts delete mode 100644 src/services/message-preview/sendMessagePreview.ts diff --git a/src/constants/actions.ts b/src/constants/actions.ts index 46e38fe..2d3d248 100644 --- a/src/constants/actions.ts +++ b/src/constants/actions.ts @@ -3,6 +3,7 @@ export enum BotActions { RevokeVacancy = "revoke", PublishVacancy = "publish", CancelVacancy = "cancel", + RetryParsing = "retry_parsing", } export const ActionButtonLabels: Record = { @@ -10,6 +11,7 @@ export const ActionButtonLabels: Record = { [BotActions.PublishVacancy]: "Опубликовать", [BotActions.RevokeVacancy]: "Отозвать вакансию", [BotActions.CancelVacancy]: "Отменить", + [BotActions.RetryParsing]: "Попробовать еще раз", }; export enum BotCommands { diff --git a/src/constants/messages.ts b/src/constants/messages.ts index f6a7b43..053c1a7 100644 --- a/src/constants/messages.ts +++ b/src/constants/messages.ts @@ -33,7 +33,9 @@ export const vacancyTemplateHTMLMessageText = `\n` + `${VacancyFieldLabel.Description}: Всё, что описывает вакансию, обязанности и предложения`; -export const vacancyLimitExceededMessageText = `Достигнут лимит бесплатных публикаций в этом месяце. Свяжись с @${config.botConsultantUsername} чтобы разместить больше`; +export const vacancyLimitExceededMessageText = + `Достигнут лимит бесплатных публикаций в этом месяце. ` + + `Свяжись с @${config.botConsultantUsername} чтобы разместить больше`; export const getMissingRequiredFieldsMessage = ( fieldLabels: VacancyFieldLabel[] @@ -41,3 +43,15 @@ export const getMissingRequiredFieldsMessage = ( `Не указаны: ${fieldLabels.join(", ")}. ` + `Соискатели часто обращают на эти поля свое внимание, пожалуйста, заполни их. ` + `Для ознакомления с правилами публикации и форматом вакансии воспользуйся командой /help`; + +export const parsedVacancyToReviewMessage = + "Вакансия сформирована. " + + "В случае некорректной обработке текста попробуй снова или свяжись с администратором после нескольких попыток"; + +export const validationFailedMessage = + "Текст не прошел проверку, пожалуйста, убедись, " + + "что в сообщении указаны основные поля. Для ознакомления с правилами публикации " + + "вызови команду /help, либо свяжись с администратором для решения возникшей проблемы"; + +export const systemErrorMessage = + "Что-то пошло не так, я плохо себя чувствую! Позови администратора!"; diff --git a/src/services/message-preview/constructPreviewMessage.ts b/src/services/message-preview/constructPreviewMessage.ts new file mode 100644 index 0000000..af2ff9c --- /dev/null +++ b/src/services/message-preview/constructPreviewMessage.ts @@ -0,0 +1,66 @@ +import { Markup } from "telegraf"; +import { InlineKeyboardMarkup } from "telegraf/typings/core/types/typegram"; + +import { ActionButtonLabels, BotActions } from "../../constants/actions"; +import { IVacancyParsed } from "../../types/vacancy"; +import { buildMessageFromVacancy } from "../../utils/buildMessageFromVacancy"; +import { IParsedMessageEntity } from "../../utils/parseMessageEntities"; +import logger from "../logger"; + +export const constructPreviewMessage = ( + ctx, + parsedVacancy: IVacancyParsed, + parsedEntities: IParsedMessageEntity[] +): + | { + previewMessageText: string; + messageOptions: { + parse_mode: string; + reply_markup: Markup.Markup["reply_markup"]; + }; + } + | undefined => { + const { message_id, chat, from } = ctx?.update?.message || {}; + + try { + if (!message_id || !chat?.id || !from?.username) { + throw Error("cannot retrieve message_id, chat.id of from.username"); + } + + const replyMarkupButtons = Markup.inlineKeyboard([ + Markup.button.callback( + ActionButtonLabels[BotActions.PublishVacancy], + BotActions.PublishVacancy + ), + Markup.button.callback( + ActionButtonLabels[BotActions.CancelVacancy], + BotActions.CancelVacancy + ), + Markup.button.callback( + ActionButtonLabels[BotActions.RetryParsing], + `${BotActions.RetryParsing}-${message_id}` + ), + ]); + + logger.info( + `Successfully constructed vacancy preview message for - ${from.username}::${chat?.id}::${message_id}` + ); + + return { + previewMessageText: buildMessageFromVacancy( + parsedVacancy, + parsedEntities + ), + messageOptions: { + ...replyMarkupButtons, + parse_mode: "HTML", + }, + }; + } catch (err) { + logger.error( + `Failed to create vacancy from message ${from.username}::${ + chat?.id + }::${message_id} - ${(err as Error).message || JSON.stringify(err)}` + ); + } +}; diff --git a/src/services/message-preview/editNewVacancy.ts b/src/services/message-preview/editNewVacancy.ts new file mode 100644 index 0000000..36ee91d --- /dev/null +++ b/src/services/message-preview/editNewVacancy.ts @@ -0,0 +1,56 @@ +import VacancyModel from "../../schemas/vacancy"; +import { TVacancyCreationAttributes } from "../../types/vacancy"; +import logger from "../logger"; +import { createNewVacancy } from "./createNewVacancy"; + +export const editNewVacancy = async ({ + vacancy, + messageId, + chatId, + fromUsername, +}: { + vacancy: TVacancyCreationAttributes; + messageId: number; + chatId: number; + fromUsername: string; +}) => { + try { + if (!chatId || !messageId || !fromUsername) { + throw Error(`cannot retrieve required info`); + } + + const existingVacancy = await VacancyModel.findOne({ + where: { + author_username: fromUsername, + tg_chat_id: chatId, + tg_message_id: messageId, + }, + }); + + if (!existingVacancy) { + logger.info( + `vacancy for ${messageId}::${chatId}::${fromUsername} doesn't exist, creating the new one` + ); + await createNewVacancy({ vacancy, messageId, chatId }); + return; + } + + const newVacancy = await VacancyModel.create(vacancy, { + isNewRecord: true, + }); + + if (!newVacancy) { + throw Error("creation failed on DB side"); + } + + logger.info( + `Vacancy from message ${messageId}::${chatId} succesfully updated - ${newVacancy.id}` + ); + } catch (err) { + logger.error( + `Failed to update vacancy from message ${messageId}::${chatId}::${fromUsername} - ${ + (err as Error)?.message || JSON.stringify(err) + }` + ); + } +}; diff --git a/src/services/message-preview/index.ts b/src/services/message-preview/index.ts index b440d93..abf04f5 100644 --- a/src/services/message-preview/index.ts +++ b/src/services/message-preview/index.ts @@ -1 +1 @@ -export { sendMessagePreview } from "./sendMessagePreview"; +export { onRetryParsing } from "./retryVacancyParsing"; diff --git a/src/services/message-preview/isRequiredVacancyFieldsFilled.ts b/src/services/message-preview/isRequiredVacancyFieldsFilled.ts new file mode 100644 index 0000000..6146128 --- /dev/null +++ b/src/services/message-preview/isRequiredVacancyFieldsFilled.ts @@ -0,0 +1,32 @@ +import { VacancyFieldLabel } from "../../constants/labels"; +import { IVacancyParsed } from "../../types/vacancy"; + +export const isRequiredVacancyFieldsFilled = ( + parsedVacancy: IVacancyParsed +): { isRequiredFieldsFilled: boolean; missingFields: VacancyFieldLabel[] } => { + if (!parsedVacancy) { + return { isRequiredFieldsFilled: false, missingFields: [] }; + } + + const missingFields: VacancyFieldLabel[] = []; + const { + title, + salary_amount_from, + salary_amount_to, + salary_negotiable, + company_name, + contact_info, + hiring_process, + work_experience, + } = parsedVacancy; + + if (!title) missingFields.push(VacancyFieldLabel.Title); + if (!company_name) missingFields.push(VacancyFieldLabel.Company); + if (!contact_info) missingFields.push(VacancyFieldLabel.Contacts); + if (!hiring_process) missingFields.push(VacancyFieldLabel.HiringProcess); + if (!salary_amount_from && !salary_amount_to && !salary_negotiable) + missingFields.push(VacancyFieldLabel.Salary); + if (!work_experience) missingFields.push(VacancyFieldLabel.WorkExperience); + + return { isRequiredFieldsFilled: !!missingFields.length, missingFields }; +}; diff --git a/src/services/message-preview/parseMessageToVacancy.ts b/src/services/message-preview/parseMessageToVacancy.ts new file mode 100644 index 0000000..d41d40c --- /dev/null +++ b/src/services/message-preview/parseMessageToVacancy.ts @@ -0,0 +1,14 @@ +import { validationFailedMessage } from "../../constants/messages"; +import { parseNewVacancyWithAI } from "../ai/parseNewVacancyWithAI"; + +export const parseMessageToVacancy = async (text: string, ctx) => { + const parsedMessage = await parseNewVacancyWithAI(text); + + if (!parsedMessage) { + await ctx.sendMessage(validationFailedMessage); + + throw Error("failed to parse vacancy with AI"); + } + + return parsedMessage; +}; diff --git a/src/services/message-preview/processIncomingMessage.ts b/src/services/message-preview/processIncomingMessage.ts index a99c8ed..59d1c84 100644 --- a/src/services/message-preview/processIncomingMessage.ts +++ b/src/services/message-preview/processIncomingMessage.ts @@ -1,40 +1,35 @@ -import { VacancyFieldLabel } from "../../constants/labels"; -import { getMissingRequiredFieldsMessage } from "../../constants/messages"; -import { IVacancyParsed } from "../../types/vacancy"; +import { + getMissingRequiredFieldsMessage, + parsedVacancyToReviewMessage, +} from "../../constants/messages"; import { parseMessageEntities } from "../../utils/parseMessageEntities"; -import { parseNewVacancyWithAI } from "../ai/parseNewVacancyWithAI"; import { onVacancyEdit } from "../edit-vacancy"; import logger from "../logger"; -import { sendMessagePreview } from "./sendMessagePreview"; +import { constructPreviewMessage } from "./constructPreviewMessage"; +import { createNewVacancy } from "./createNewVacancy"; +import { isRequiredVacancyFieldsFilled } from "./isRequiredVacancyFieldsFilled"; +import { parseMessageToVacancy } from "./parseMessageToVacancy"; -export const isRequiredVacancyFieldsFilled = ( - parsedVacancy: IVacancyParsed -): { isRequiredFieldsFilled: boolean; missingFields: VacancyFieldLabel[] } => { - if (!parsedVacancy) { - return { isRequiredFieldsFilled: false, missingFields: [] }; - } - - const missingFields: VacancyFieldLabel[] = []; - const { - title, - salary_amount_from, - salary_amount_to, - salary_negotiable, - company_name, - contact_info, - hiring_process, - work_experience, - } = parsedVacancy; +const getEditedMessageInfo = ( + text: string, + ctx +): { + messageId?: string; + updatedVacancyText?: string; + isEditedExistingVacancy: boolean; +} => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [techInfoLine, disclaimerLine, gapLine, ...updatedVacancyText] = + text?.split("\n") || []; + const [, messageId] = techInfoLine?.split(" > ") || []; - if (!title) missingFields.push(VacancyFieldLabel.Title); - if (!company_name) missingFields.push(VacancyFieldLabel.Company); - if (!contact_info) missingFields.push(VacancyFieldLabel.Contacts); - if (!hiring_process) missingFields.push(VacancyFieldLabel.HiringProcess); - if (!salary_amount_from && !salary_amount_to && !salary_negotiable) - missingFields.push(VacancyFieldLabel.Salary); - if (!work_experience) missingFields.push(VacancyFieldLabel.WorkExperience); - - return { isRequiredFieldsFilled: !!missingFields.length, missingFields }; + return { + messageId, + updatedVacancyText: updatedVacancyText?.join("\n"), + isEditedExistingVacancy: techInfoLine?.startsWith( + `@${ctx?.botInfo?.username}` + ), + }; }; export const processIncomingMessage = async (ctx) => { @@ -45,42 +40,59 @@ export const processIncomingMessage = async (ctx) => { throw Error("cannot retrieve required message info"); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [techInfoLine, disclaimerLine, gapLine, ...updatedInfoText] = - text.split("\n"); - - // edited existing vacancy - if (techInfoLine && techInfoLine.startsWith(`@${ctx?.botInfo?.username}`)) { - const [, messageId] = techInfoLine.split(" > "); + const { messageId, updatedVacancyText, isEditedExistingVacancy } = + getEditedMessageInfo(text, ctx); + if (isEditedExistingVacancy && updatedVacancyText && messageId) { await onVacancyEdit(ctx, { - messageId, - updatedText: updatedInfoText.join("\n"), + messageId: Number(messageId), + updatedText: updatedVacancyText, }); return; } - const parsedMessage = await parseNewVacancyWithAI(text); - - if (!parsedMessage) { - await ctx.sendMessage( - `Не удалось распознать вакансию с помощью AI, попробуйте еще раз` - ); - throw Error("failed to parse vacancy with AI"); - } + const parsedMessage = await parseMessageToVacancy(text, ctx); const { isRequiredFieldsFilled, missingFields } = isRequiredVacancyFieldsFilled(parsedMessage); + if (!isRequiredFieldsFilled) { await ctx.sendMessage(getMissingRequiredFieldsMessage(missingFields)); throw Error(`missing fields - ${missingFields.join(", ")}`); } - await sendMessagePreview( - ctx, - parsedMessage, - parseMessageEntities(text, entities) + const { previewMessageText, messageOptions } = + constructPreviewMessage( + ctx, + parsedMessage, + parseMessageEntities(text, entities) + ) || {}; + + await ctx.sendMessage(parsedVacancyToReviewMessage); + + const response = await ctx.sendMessage(previewMessageText, { + ...messageOptions, + reply_to_message_id: message_id, + }); + + logger.info( + `Successfully sent vacancy preview message for - ${from.username}::${chat?.id}::${message_id}` ); + + if (!response.message_id) { + throw Error("preview message sending was failed"); + } + + await createNewVacancy({ + vacancy: { + ...parsedMessage, + author_username: from.username, + tg_message_id: response.message_id, + tg_chat_id: response.chat.id, + }, + messageId: response.message_id, + chatId: response.chat.id, + }); } catch (err) { logger.error( `Failed to process incoming message ${from?.username}::${ diff --git a/src/services/message-preview/retryVacancyParsing.ts b/src/services/message-preview/retryVacancyParsing.ts new file mode 100644 index 0000000..1bc6b4a --- /dev/null +++ b/src/services/message-preview/retryVacancyParsing.ts @@ -0,0 +1,88 @@ +import { + getMissingRequiredFieldsMessage, + systemErrorMessage, +} from "../../constants/messages"; +import { parseMessageEntities } from "../../utils/parseMessageEntities"; +import logger from "../logger"; +import { constructPreviewMessage } from "./constructPreviewMessage"; +import { editNewVacancy } from "./editNewVacancy"; +import { isRequiredVacancyFieldsFilled } from "./isRequiredVacancyFieldsFilled"; +import { parseMessageToVacancy } from "./parseMessageToVacancy"; + +export const onRetryParsing = async (ctx) => { + const [, messageIdToParse] = ctx?.match || []; + const { message_id, chat, reply_to_message } = + ctx?.update?.callback_query?.message || {}; + const { sourceMessageId, sourceText, sourceEntities } = + reply_to_message || {}; + + try { + if (!reply_to_message || !sourceText) { + throw Error( + `cannot retrieve required info - reply_to_message, sourceText` + ); + } + + if (!messageIdToParse) { + await ctx.sendMessage(systemErrorMessage); + throw Error( + `messageIdToParse is missing, check button regexp. messageIdToParse=${messageIdToParse}` + ); + } + + if (messageIdToParse !== sourceMessageId) { + await ctx.sendMessage(systemErrorMessage); + throw Error( + `messageIdToParse is wrong, check reply regexp. messageIdToParse=${messageIdToParse}` + ); + } + + const parsedVacancy = await parseMessageToVacancy(sourceText, ctx); + + const { isRequiredFieldsFilled, missingFields } = + isRequiredVacancyFieldsFilled(parsedVacancy); + + if (!isRequiredFieldsFilled) { + await ctx.sendMessage(getMissingRequiredFieldsMessage(missingFields)); + throw Error(`missing fields - ${missingFields.join(", ")}`); + } + + const { previewMessageText, messageOptions } = + constructPreviewMessage( + ctx, + parsedVacancy, + parseMessageEntities(sourceText, sourceEntities) + ) || {}; + + const response = await ctx.editMessageText(previewMessageText, { + ...messageOptions, + reply_to_message_id: message_id, + }); + + logger.info( + `Successfully updated vacancy preview message for - ${chat?.username}::${chat?.id}::${sourceMessageId}` + ); + + if (!response.message_id) { + throw Error("preview message sending was failed"); + } + + await editNewVacancy({ + vacancy: { + ...parsedVacancy, + author_username: chat?.username, + tg_message_id: response.message_id, + tg_chat_id: response.chat.id, + }, + messageId: response.message_id, + chatId: response.chat.id, + fromUsername: chat?.username, + }); + } catch (err) { + logger.error( + `Failed to re-parse vacancy ${messageIdToParse}::${chat?.username} - ${ + (err as Error).message || JSON.stringify(err) + }` + ); + } +}; diff --git a/src/services/message-preview/sendMessagePreview.ts b/src/services/message-preview/sendMessagePreview.ts deleted file mode 100644 index d3081fd..0000000 --- a/src/services/message-preview/sendMessagePreview.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Markup } from "telegraf"; - -import { ActionButtonLabels, BotActions } from "../../constants/actions"; -import { IVacancyParsed } from "../../types/vacancy"; -import { buildMessageFromVacancy } from "../../utils/buildMessageFromVacancy"; -import { IParsedMessageEntity } from "../../utils/parseMessageEntities"; -import logger from "../logger"; -import { createNewVacancy } from "./createNewVacancy"; - -export const sendMessagePreview = async ( - ctx, - parsedVacancy: IVacancyParsed, - parsedEntities: IParsedMessageEntity[] -) => { - const replyMarkupButtons = Markup.inlineKeyboard([ - Markup.button.callback( - ActionButtonLabels[BotActions.PublishVacancy], - BotActions.PublishVacancy - ), - Markup.button.callback( - ActionButtonLabels[BotActions.CancelVacancy], - BotActions.CancelVacancy - ), - ]); - - const { message_id, chat, from } = ctx?.update?.message || {}; - - try { - if (!message_id || !chat?.id || !from?.username) { - throw Error("cannot retrieve message_id, chat.id of from.username"); - } - - const response = await ctx.sendMessage( - buildMessageFromVacancy(parsedVacancy, parsedEntities), - { - ...replyMarkupButtons, - parse_mode: "HTML", - } - ); - - if (!response.message_id) { - throw Error("preview message sending was failed"); - } - - await createNewVacancy({ - vacancy: { - ...parsedVacancy, - author_username: from.username, - tg_message_id: response.message_id, - tg_chat_id: response.chat.id, - }, - messageId: response.message_id, - chatId: response.chat.id, - }); - } catch (err) { - logger.error( - `Failed to create vacancy from message ${from.username}::${ - chat?.id - }::${message_id} - ${(err as Error).message || JSON.stringify(err)}` - ); - } -}; diff --git a/src/services/subscribe-to-actions/subscribeToButtonActions.ts b/src/services/subscribe-to-actions/subscribeToButtonActions.ts index ff8e38a..a7e6b5b 100644 --- a/src/services/subscribe-to-actions/subscribeToButtonActions.ts +++ b/src/services/subscribe-to-actions/subscribeToButtonActions.ts @@ -2,12 +2,16 @@ import { BotActions } from "../../constants/actions"; import bot from "../../launchBot"; import { CancelVacancyService, + MessagePreviewService, PublishVacancyService, RevokeVacancyService, } from "../index"; +const RETRY_PARSING_REGEXP = new RegExp(`^${BotActions.RetryParsing}-(.+)$`); + export const subscribeToButtonActions = () => { bot.action(BotActions.PublishVacancy, PublishVacancyService.onPublishVacancy); bot.action(BotActions.RevokeVacancy, RevokeVacancyService.onVacancyRevoke); bot.action(BotActions.CancelVacancy, CancelVacancyService.onVacancyCancel); + bot.action(RETRY_PARSING_REGEXP, MessagePreviewService.onRetryParsing); }; From 3cf8ec9e5fe8b81a03d1f5b6c0681af533978250 Mon Sep 17 00:00:00 2001 From: marylorian Date: Wed, 2 Aug 2023 19:00:09 +0200 Subject: [PATCH 08/11] issue(37): fixes err message text --- src/constants/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/messages.ts b/src/constants/messages.ts index 053c1a7..da623fd 100644 --- a/src/constants/messages.ts +++ b/src/constants/messages.ts @@ -46,7 +46,7 @@ export const getMissingRequiredFieldsMessage = ( export const parsedVacancyToReviewMessage = "Вакансия сформирована. " + - "В случае некорректной обработке текста попробуй снова или свяжись с администратором после нескольких попыток"; + "В случае некорректной обработки текста, попробуй снова или свяжись с администратором после нескольких попыток"; export const validationFailedMessage = "Текст не прошел проверку, пожалуйста, убедись, " + From ca774bca26789e4a8ebf783a8907e0fb0a53141b Mon Sep 17 00:00:00 2001 From: marylorian Date: Wed, 2 Aug 2023 19:18:17 +0200 Subject: [PATCH 09/11] issue(2): fixes pipeline --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c3ac811..6cff9e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,6 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build --if-present - - run: npm run prettier + - run: npm run prettier-check - run: npm run lint - run: npm test From 84198c6930e80df986bee36e9986ad319eace115 Mon Sep 17 00:00:00 2001 From: marylorian Date: Wed, 2 Aug 2023 19:21:51 +0200 Subject: [PATCH 10/11] issue(2): fixes package.json github links --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 28ffc74..d39a749 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,13 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/openworld-community/rzrbs-vacancy-bot.git" + "url": "git+https://github.com/razrabs-media/rzrbs-vacancy-bot.git" }, "license": "GPL-3.0", "bugs": { - "url": "https://github.com/openworld-community/rzrbs-vacancy-bot/issues" + "url": "https://github.com/razrabs-media/rzrbs-vacancy-bot/issues" }, - "homepage": "https://github.com/openworld-community/rzrbs-vacancy-bot#readme", + "homepage": "https://github.com/razrabs-media/rzrbs-vacancy-bot#readme", "lint-staged": { "*.ts": "eslint . --ext .ts" }, From df5507cb310bed14b616b5a40fef8f34467f341c Mon Sep 17 00:00:00 2001 From: marylorian Date: Wed, 2 Aug 2023 19:34:43 +0200 Subject: [PATCH 11/11] issue(2): fixes missing todo, adds there a validation by AI after editing --- src/services/edit-vacancy/editVacancy.ts | 15 +++++++++++++-- .../message-preview/processIncomingMessage.ts | 2 +- .../message-preview/retryVacancyParsing.ts | 2 +- .../isRequiredVacancyFieldsFilled.ts | 4 ++-- 4 files changed, 17 insertions(+), 6 deletions(-) rename src/{services/message-preview => utils}/isRequiredVacancyFieldsFilled.ts (90%) diff --git a/src/services/edit-vacancy/editVacancy.ts b/src/services/edit-vacancy/editVacancy.ts index 1f17fcd..4c9e807 100644 --- a/src/services/edit-vacancy/editVacancy.ts +++ b/src/services/edit-vacancy/editVacancy.ts @@ -1,4 +1,6 @@ +import { getMissingRequiredFieldsMessage } from "../../constants/messages"; import Vacancies from "../../schemas/vacancy"; +import { isRequiredVacancyFieldsFilled } from "../../utils/isRequiredVacancyFieldsFilled"; import { parseUpdatedVacancyWithAI } from "../ai/parseUpdatedVacancyWithAI"; import logger from "../logger"; import { updatePrivateVacancyMessage } from "./updatePrivateVacancyMessage"; @@ -40,8 +42,17 @@ export const onVacancyEdit = async ( const updatedVacancyFields = await parseUpdatedVacancyWithAI(updatedText); - // TODO: add some validation here - // https://github.com/openworld-community/rzrbs-vacancy-bot/issues/24 + if (!updatedVacancyFields) { + throw Error("failed to parse by AI"); + } + + const { isRequiredFieldsFilled, missingFields } = + isRequiredVacancyFieldsFilled(updatedVacancyFields); + + if (!isRequiredFieldsFilled) { + await ctx.sendMessage(getMissingRequiredFieldsMessage(missingFields)); + throw Error(`missing fields - ${missingFields.join(", ")}`); + } logger.info(`Edited fields parsed, updating vacancy in DB...`); for (const key in updatedVacancyFields) { diff --git a/src/services/message-preview/processIncomingMessage.ts b/src/services/message-preview/processIncomingMessage.ts index 59d1c84..c838267 100644 --- a/src/services/message-preview/processIncomingMessage.ts +++ b/src/services/message-preview/processIncomingMessage.ts @@ -2,12 +2,12 @@ import { getMissingRequiredFieldsMessage, parsedVacancyToReviewMessage, } from "../../constants/messages"; +import { isRequiredVacancyFieldsFilled } from "../../utils/isRequiredVacancyFieldsFilled"; import { parseMessageEntities } from "../../utils/parseMessageEntities"; import { onVacancyEdit } from "../edit-vacancy"; import logger from "../logger"; import { constructPreviewMessage } from "./constructPreviewMessage"; import { createNewVacancy } from "./createNewVacancy"; -import { isRequiredVacancyFieldsFilled } from "./isRequiredVacancyFieldsFilled"; import { parseMessageToVacancy } from "./parseMessageToVacancy"; const getEditedMessageInfo = ( diff --git a/src/services/message-preview/retryVacancyParsing.ts b/src/services/message-preview/retryVacancyParsing.ts index 1bc6b4a..2744f57 100644 --- a/src/services/message-preview/retryVacancyParsing.ts +++ b/src/services/message-preview/retryVacancyParsing.ts @@ -2,11 +2,11 @@ import { getMissingRequiredFieldsMessage, systemErrorMessage, } from "../../constants/messages"; +import { isRequiredVacancyFieldsFilled } from "../../utils/isRequiredVacancyFieldsFilled"; import { parseMessageEntities } from "../../utils/parseMessageEntities"; import logger from "../logger"; import { constructPreviewMessage } from "./constructPreviewMessage"; import { editNewVacancy } from "./editNewVacancy"; -import { isRequiredVacancyFieldsFilled } from "./isRequiredVacancyFieldsFilled"; import { parseMessageToVacancy } from "./parseMessageToVacancy"; export const onRetryParsing = async (ctx) => { diff --git a/src/services/message-preview/isRequiredVacancyFieldsFilled.ts b/src/utils/isRequiredVacancyFieldsFilled.ts similarity index 90% rename from src/services/message-preview/isRequiredVacancyFieldsFilled.ts rename to src/utils/isRequiredVacancyFieldsFilled.ts index 6146128..36b5969 100644 --- a/src/services/message-preview/isRequiredVacancyFieldsFilled.ts +++ b/src/utils/isRequiredVacancyFieldsFilled.ts @@ -1,5 +1,5 @@ -import { VacancyFieldLabel } from "../../constants/labels"; -import { IVacancyParsed } from "../../types/vacancy"; +import { VacancyFieldLabel } from "../constants/labels"; +import { IVacancyParsed } from "../types/vacancy"; export const isRequiredVacancyFieldsFilled = ( parsedVacancy: IVacancyParsed