From fe425156fd336d2b4c9d6bd4ed194f0c7cf6494e Mon Sep 17 00:00:00 2001 From: Tycho Bellers Date: Thu, 4 Aug 2022 21:39:00 -0700 Subject: [PATCH] Update packages. Update eslint rules. --- .eslintignore | 6 - .eslintrc.cjs | 4 +- .husky/pre-commit | 4 +- package-lock.json | 625 +++++++++++++++++------------------- package.json | 10 +- src/configuration_parser.ts | 36 +-- src/index.tsx | 164 +++++----- src/logger.ts | 4 +- src/options.ts | 18 +- src/twitch_drops_bot.ts | 136 ++++---- src/update_games.ts | 118 +++---- src/utils.ts | 24 +- src/watchdog.ts | 6 +- src/web_socket_listener.ts | 142 ++++---- test/options.test.ts | 2 +- test/update_games.test.ts | 10 +- 16 files changed, 626 insertions(+), 683 deletions(-) delete mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index c2efcdb..0000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -# Ignore all files -/* - -# Lint only these files -!src -!test diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 2eee55f..0957130 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -18,6 +18,8 @@ module.exports = { "eslint-plugin-jest" ], "rules": { - "unused-imports/no-unused-imports": "error" + "unused-imports/no-unused-imports": "error", + "semi": ["error", "always"], + "quotes": ["error", "double"] } } diff --git a/.husky/pre-commit b/.husky/pre-commit index e04809a..4bf2a50 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -7,8 +7,8 @@ PATH=$PATH:node_modules/.bin # Check for unused dependencies node tools/check-unused-dependencies.js -# Check of linter errors -if ! eslint . --ext js,ts,jsx,tsx; then +# Check for linter errors +if ! eslint "src/*" "test/" --ext js,ts,jsx,tsx; then exit 1 fi diff --git a/package-lock.json b/package-lock.json index 06b9592..c745806 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "pidtree": "^0.6.0", "pidusage": "^3.0.0", "prompt": "^1.2.2", - "puppeteer": "^15.5.0", + "puppeteer": "^16.0.0", "puppeteer-extra": "^3.3.4", "puppeteer-extra-plugin-stealth": "^2.10.4", "react": "^17.0.2", @@ -40,11 +40,11 @@ "@types/prompt": "^1.1.2", "@types/react": "^17.0.41", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.30.7", - "@typescript-eslint/parser": "^5.30.4", + "@typescript-eslint/eslint-plugin": "^5.32.0", + "@typescript-eslint/parser": "^5.32.0", "depcheck": "^1.4.3", - "eslint": "^8.19.0", - "eslint-plugin-jest": "^26.1.5", + "eslint": "^8.21.0", + "eslint-plugin-jest": "^26.7.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-unused-imports": "^2.0.0", "husky": "^8.0.1", @@ -667,9 +667,9 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", + "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -680,6 +680,16 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -1648,9 +1658,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.0.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", - "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", + "version": "17.0.48", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.48.tgz", + "integrity": "sha512-zJ6IYlJ8cYYxiJfUaZOQee4lh99mFihBoqkOSEGV+dFi9leROW6+PgstzQ+w3gWTnUfskALtQPGHK6dYmPj+2A==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -1712,14 +1722,14 @@ "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.31.0.tgz", - "integrity": "sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.32.0.tgz", + "integrity": "sha512-CHLuz5Uz7bHP2WgVlvoZGhf0BvFakBJKAD/43Ty0emn4wXWv5k01ND0C0fHcl/Im8Td2y/7h44E9pca9qAu2ew==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.31.0", - "@typescript-eslint/type-utils": "5.31.0", - "@typescript-eslint/utils": "5.31.0", + "@typescript-eslint/scope-manager": "5.32.0", + "@typescript-eslint/type-utils": "5.32.0", + "@typescript-eslint/utils": "5.32.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", @@ -1744,62 +1754,15 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz", - "integrity": "sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.31.0", - "@typescript-eslint/visitor-keys": "5.31.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.31.0.tgz", - "integrity": "sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz", - "integrity": "sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.31.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.5.tgz", - "integrity": "sha512-zj251pcPXI8GO9NDKWWmygP6+UjwWmrdf9qMW/L/uQJBM/0XbU2inxe5io/234y/RCvwpKEYjZ6c1YrXERkK4Q==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.32.0.tgz", + "integrity": "sha512-IxRtsehdGV9GFQ35IGm5oKKR2OGcazUoiNBxhRV160iF9FoyuXxjY+rIqs1gfnd+4eL98OjeGnMpE7RF/NBb3A==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.30.5", - "@typescript-eslint/types": "5.30.5", - "@typescript-eslint/typescript-estree": "5.30.5", + "@typescript-eslint/scope-manager": "5.32.0", + "@typescript-eslint/types": "5.32.0", + "@typescript-eslint/typescript-estree": "5.32.0", "debug": "^4.3.4" }, "engines": { @@ -1819,13 +1782,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.5.tgz", - "integrity": "sha512-NJ6F+YHHFT/30isRe2UTmIGGAiXKckCyMnIV58cE3JkHmaD6e5zyEYm5hBDv0Wbin+IC0T1FWJpD3YqHUG/Ydg==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.32.0.tgz", + "integrity": "sha512-KyAE+tUON0D7tNz92p1uetRqVJiiAkeluvwvZOqBmW9z2XApmk5WSMV9FrzOroAcVxJZB3GfUwVKr98Dr/OjOg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.30.5", - "@typescript-eslint/visitor-keys": "5.30.5" + "@typescript-eslint/types": "5.32.0", + "@typescript-eslint/visitor-keys": "5.32.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1836,12 +1799,12 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.31.0.tgz", - "integrity": "sha512-7ZYqFbvEvYXFn9ax02GsPcEOmuWNg+14HIf4q+oUuLnMbpJ6eHAivCg7tZMVwzrIuzX3QCeAOqKoyMZCv5xe+w==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.32.0.tgz", + "integrity": "sha512-0gSsIhFDduBz3QcHJIp3qRCvVYbqzHg8D6bHFsDMrm0rURYDj+skBK2zmYebdCp+4nrd9VWd13egvhYFJj/wZg==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "5.31.0", + "@typescript-eslint/utils": "5.32.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -1862,9 +1825,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.5.tgz", - "integrity": "sha512-kZ80w/M2AvsbRvOr3PjaNh6qEW1LFqs2pLdo2s5R38B2HYXG8Z0PP48/4+j1QHJFL3ssHIbJ4odPRS8PlHrFfw==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.32.0.tgz", + "integrity": "sha512-EBUKs68DOcT/EjGfzywp+f8wG9Zw6gj6BjWu7KV/IYllqKJFPlZlLSYw/PTvVyiRw50t6wVbgv4p9uE2h6sZrQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1875,13 +1838,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.5.tgz", - "integrity": "sha512-qGTc7QZC801kbYjAr4AgdOfnokpwStqyhSbiQvqGBLixniAKyH+ib2qXIVo4P9NgGzwyfD9I0nlJN7D91E1VpQ==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.32.0.tgz", + "integrity": "sha512-ZVAUkvPk3ITGtCLU5J4atCw9RTxK+SRc6hXqLtllC2sGSeMFWN+YwbiJR9CFrSFJ3w4SJfcWtDwNb/DmUIHdhg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.30.5", - "@typescript-eslint/visitor-keys": "5.30.5", + "@typescript-eslint/types": "5.32.0", + "@typescript-eslint/visitor-keys": "5.32.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1902,15 +1865,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.31.0.tgz", - "integrity": "sha512-kcVPdQS6VIpVTQ7QnGNKMFtdJdvnStkqS5LeALr4rcwx11G6OWb2HB17NMPnlRHvaZP38hL9iK8DdE9Fne7NYg==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.32.0.tgz", + "integrity": "sha512-W7lYIAI5Zlc5K082dGR27Fczjb3Q57ECcXefKU/f0ajM5ToM0P+N9NmJWip8GmGu/g6QISNT+K6KYB+iSHjXCQ==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.31.0", - "@typescript-eslint/types": "5.31.0", - "@typescript-eslint/typescript-estree": "5.31.0", + "@typescript-eslint/scope-manager": "5.32.0", + "@typescript-eslint/types": "5.32.0", + "@typescript-eslint/typescript-estree": "5.32.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -1925,87 +1888,13 @@ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz", - "integrity": "sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.31.0", - "@typescript-eslint/visitor-keys": "5.31.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.31.0.tgz", - "integrity": "sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.31.0.tgz", - "integrity": "sha512-3S625TMcARX71wBc2qubHaoUwMEn+l9TCsaIzYI/ET31Xm2c9YQ+zhGgpydjorwQO9pLfR/6peTzS/0G3J/hDw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.31.0", - "@typescript-eslint/visitor-keys": "5.31.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz", - "integrity": "sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.31.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.5.tgz", - "integrity": "sha512-D+xtGo9HUMELzWIUqcQc0p2PO4NyvTrgIOK/VnSH083+8sq0tiLozNRKuLarwHYGRuA6TVBQSuuLwJUDWd3aaA==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.32.0.tgz", + "integrity": "sha512-S54xOHZgfThiZ38/ZGTgB2rqx51CMJ5MCfVT2IplK4Q7hgzGfe0nLzLCcenDnc/cSjP568hdeKfeDcBgqNHD/g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.30.5", + "@typescript-eslint/types": "5.32.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2086,9 +1975,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3391,13 +3280,14 @@ } }, "node_modules/eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", - "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz", + "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -3407,14 +3297,17 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.3.3", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -3443,9 +3336,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "26.5.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.5.3.tgz", - "integrity": "sha512-sICclUqJQnR1bFRZGLN2jnSVsYOsmPYYnroGCIMVSvTS3y8XR3yjzy1EcTQmk6typ5pRgyIWzbjqxK6cZHEZuQ==", + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.7.0.tgz", + "integrity": "sha512-/YNitdfG3o3cC6juZziAdkk6nfJt01jXVfj4AgaYVLs7bupHzRDL5K+eipdzhDXtQsiqaX1TzfwSuRlEgeln1A==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -3694,6 +3587,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/globals": { "version": "13.16.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", @@ -3730,6 +3639,51 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3755,17 +3709,20 @@ } }, "node_modules/espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", + "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", "dev": true, "dependencies": { - "acorn": "^8.7.1", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -4316,6 +4273,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -7689,9 +7652,9 @@ } }, "node_modules/puppeteer": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-15.5.0.tgz", - "integrity": "sha512-+vZPU8iBSdCx1Kn5hHas80fyo0TiVyMeqLGv/1dygX2HKhAZjO9YThadbRTCoTYq0yWw+w/CysldPsEekDtjDQ==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-16.0.0.tgz", + "integrity": "sha512-FgSe21IHNHkqv1SiJiob4ANsxVujcINa4p3MaDEMyoZsocbgSgwYE0c9lnF8eoinw4id3vx4DOXwhFdOOwVlDg==", "hasInstallScript": true, "dependencies": { "cross-fetch": "3.1.5", @@ -7705,7 +7668,7 @@ "rimraf": "3.0.2", "tar-fs": "2.1.1", "unbzip2-stream": "1.4.3", - "ws": "8.8.0" + "ws": "8.8.1" }, "engines": { "node": ">=14.1.0" @@ -7843,9 +7806,9 @@ } }, "node_modules/puppeteer/node_modules/ws": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", - "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", + "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", "engines": { "node": ">=10.0.0" }, @@ -7892,11 +7855,12 @@ ] }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "dependencies": { - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" }, "engines": { "node": ">=0.10.0" @@ -9770,9 +9734,9 @@ } }, "@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", + "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -9780,6 +9744,12 @@ "minimatch": "^3.0.4" } }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true + }, "@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -10567,9 +10537,9 @@ "devOptional": true }, "@types/react": { - "version": "18.0.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", - "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", + "version": "17.0.48", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.48.tgz", + "integrity": "sha512-zJ6IYlJ8cYYxiJfUaZOQee4lh99mFihBoqkOSEGV+dFi9leROW6+PgstzQ+w3gWTnUfskALtQPGHK6dYmPj+2A==", "devOptional": true, "requires": { "@types/prop-types": "*", @@ -10631,97 +10601,69 @@ "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==" }, "@typescript-eslint/eslint-plugin": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.31.0.tgz", - "integrity": "sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.32.0.tgz", + "integrity": "sha512-CHLuz5Uz7bHP2WgVlvoZGhf0BvFakBJKAD/43Ty0emn4wXWv5k01ND0C0fHcl/Im8Td2y/7h44E9pca9qAu2ew==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.31.0", - "@typescript-eslint/type-utils": "5.31.0", - "@typescript-eslint/utils": "5.31.0", + "@typescript-eslint/scope-manager": "5.32.0", + "@typescript-eslint/type-utils": "5.32.0", + "@typescript-eslint/utils": "5.32.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz", - "integrity": "sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.31.0", - "@typescript-eslint/visitor-keys": "5.31.0" - } - }, - "@typescript-eslint/types": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.31.0.tgz", - "integrity": "sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==", - "dev": true - }, - "@typescript-eslint/visitor-keys": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz", - "integrity": "sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.31.0", - "eslint-visitor-keys": "^3.3.0" - } - } } }, "@typescript-eslint/parser": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.5.tgz", - "integrity": "sha512-zj251pcPXI8GO9NDKWWmygP6+UjwWmrdf9qMW/L/uQJBM/0XbU2inxe5io/234y/RCvwpKEYjZ6c1YrXERkK4Q==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.32.0.tgz", + "integrity": "sha512-IxRtsehdGV9GFQ35IGm5oKKR2OGcazUoiNBxhRV160iF9FoyuXxjY+rIqs1gfnd+4eL98OjeGnMpE7RF/NBb3A==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.30.5", - "@typescript-eslint/types": "5.30.5", - "@typescript-eslint/typescript-estree": "5.30.5", + "@typescript-eslint/scope-manager": "5.32.0", + "@typescript-eslint/types": "5.32.0", + "@typescript-eslint/typescript-estree": "5.32.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.5.tgz", - "integrity": "sha512-NJ6F+YHHFT/30isRe2UTmIGGAiXKckCyMnIV58cE3JkHmaD6e5zyEYm5hBDv0Wbin+IC0T1FWJpD3YqHUG/Ydg==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.32.0.tgz", + "integrity": "sha512-KyAE+tUON0D7tNz92p1uetRqVJiiAkeluvwvZOqBmW9z2XApmk5WSMV9FrzOroAcVxJZB3GfUwVKr98Dr/OjOg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.30.5", - "@typescript-eslint/visitor-keys": "5.30.5" + "@typescript-eslint/types": "5.32.0", + "@typescript-eslint/visitor-keys": "5.32.0" } }, "@typescript-eslint/type-utils": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.31.0.tgz", - "integrity": "sha512-7ZYqFbvEvYXFn9ax02GsPcEOmuWNg+14HIf4q+oUuLnMbpJ6eHAivCg7tZMVwzrIuzX3QCeAOqKoyMZCv5xe+w==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.32.0.tgz", + "integrity": "sha512-0gSsIhFDduBz3QcHJIp3qRCvVYbqzHg8D6bHFsDMrm0rURYDj+skBK2zmYebdCp+4nrd9VWd13egvhYFJj/wZg==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.31.0", + "@typescript-eslint/utils": "5.32.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.5.tgz", - "integrity": "sha512-kZ80w/M2AvsbRvOr3PjaNh6qEW1LFqs2pLdo2s5R38B2HYXG8Z0PP48/4+j1QHJFL3ssHIbJ4odPRS8PlHrFfw==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.32.0.tgz", + "integrity": "sha512-EBUKs68DOcT/EjGfzywp+f8wG9Zw6gj6BjWu7KV/IYllqKJFPlZlLSYw/PTvVyiRw50t6wVbgv4p9uE2h6sZrQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.5.tgz", - "integrity": "sha512-qGTc7QZC801kbYjAr4AgdOfnokpwStqyhSbiQvqGBLixniAKyH+ib2qXIVo4P9NgGzwyfD9I0nlJN7D91E1VpQ==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.32.0.tgz", + "integrity": "sha512-ZVAUkvPk3ITGtCLU5J4atCw9RTxK+SRc6hXqLtllC2sGSeMFWN+YwbiJR9CFrSFJ3w4SJfcWtDwNb/DmUIHdhg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.30.5", - "@typescript-eslint/visitor-keys": "5.30.5", + "@typescript-eslint/types": "5.32.0", + "@typescript-eslint/visitor-keys": "5.32.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -10730,69 +10672,26 @@ } }, "@typescript-eslint/utils": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.31.0.tgz", - "integrity": "sha512-kcVPdQS6VIpVTQ7QnGNKMFtdJdvnStkqS5LeALr4rcwx11G6OWb2HB17NMPnlRHvaZP38hL9iK8DdE9Fne7NYg==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.32.0.tgz", + "integrity": "sha512-W7lYIAI5Zlc5K082dGR27Fczjb3Q57ECcXefKU/f0ajM5ToM0P+N9NmJWip8GmGu/g6QISNT+K6KYB+iSHjXCQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.31.0", - "@typescript-eslint/types": "5.31.0", - "@typescript-eslint/typescript-estree": "5.31.0", + "@typescript-eslint/scope-manager": "5.32.0", + "@typescript-eslint/types": "5.32.0", + "@typescript-eslint/typescript-estree": "5.32.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz", - "integrity": "sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.31.0", - "@typescript-eslint/visitor-keys": "5.31.0" - } - }, - "@typescript-eslint/types": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.31.0.tgz", - "integrity": "sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.31.0.tgz", - "integrity": "sha512-3S625TMcARX71wBc2qubHaoUwMEn+l9TCsaIzYI/ET31Xm2c9YQ+zhGgpydjorwQO9pLfR/6peTzS/0G3J/hDw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.31.0", - "@typescript-eslint/visitor-keys": "5.31.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.31.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz", - "integrity": "sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.31.0", - "eslint-visitor-keys": "^3.3.0" - } - } } }, "@typescript-eslint/visitor-keys": { - "version": "5.30.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.5.tgz", - "integrity": "sha512-D+xtGo9HUMELzWIUqcQc0p2PO4NyvTrgIOK/VnSH083+8sq0tiLozNRKuLarwHYGRuA6TVBQSuuLwJUDWd3aaA==", + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.32.0.tgz", + "integrity": "sha512-S54xOHZgfThiZ38/ZGTgB2rqx51CMJ5MCfVT2IplK4Q7hgzGfe0nLzLCcenDnc/cSjP568hdeKfeDcBgqNHD/g==", "dev": true, "requires": { - "@typescript-eslint/types": "5.30.5", + "@typescript-eslint/types": "5.32.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -10866,9 +10765,9 @@ "dev": true }, "acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true }, "acorn-jsx": { @@ -11842,13 +11741,14 @@ "dev": true }, "eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", - "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz", + "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -11858,14 +11758,17 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.3.3", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -11934,6 +11837,16 @@ "estraverse": "^5.2.0" } }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, "globals": { "version": "13.16.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", @@ -11958,6 +11871,33 @@ "argparse": "^2.0.1" } }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11976,9 +11916,9 @@ } }, "eslint-plugin-jest": { - "version": "26.5.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.5.3.tgz", - "integrity": "sha512-sICclUqJQnR1bFRZGLN2jnSVsYOsmPYYnroGCIMVSvTS3y8XR3yjzy1EcTQmk6typ5pRgyIWzbjqxK6cZHEZuQ==", + "version": "26.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.7.0.tgz", + "integrity": "sha512-/YNitdfG3o3cC6juZziAdkk6nfJt01jXVfj4AgaYVLs7bupHzRDL5K+eipdzhDXtQsiqaX1TzfwSuRlEgeln1A==", "dev": true, "requires": { "@typescript-eslint/utils": "^5.10.0" @@ -12091,12 +12031,12 @@ "dev": true }, "espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", + "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", "dev": true, "requires": { - "acorn": "^8.7.1", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" } @@ -12503,6 +12443,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -14976,9 +14922,9 @@ "dev": true }, "puppeteer": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-15.5.0.tgz", - "integrity": "sha512-+vZPU8iBSdCx1Kn5hHas80fyo0TiVyMeqLGv/1dygX2HKhAZjO9YThadbRTCoTYq0yWw+w/CysldPsEekDtjDQ==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-16.0.0.tgz", + "integrity": "sha512-FgSe21IHNHkqv1SiJiob4ANsxVujcINa4p3MaDEMyoZsocbgSgwYE0c9lnF8eoinw4id3vx4DOXwhFdOOwVlDg==", "requires": { "cross-fetch": "3.1.5", "debug": "4.3.4", @@ -14991,13 +14937,13 @@ "rimraf": "3.0.2", "tar-fs": "2.1.1", "unbzip2-stream": "1.4.3", - "ws": "8.8.0" + "ws": "8.8.1" }, "dependencies": { "ws": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", - "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", + "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", "requires": {} } } @@ -15070,11 +15016,12 @@ "dev": true }, "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "requires": { - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" } }, "react-devtools-core": { diff --git a/package.json b/package.json index 5165528..1a88b99 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "pidtree": "^0.6.0", "pidusage": "^3.0.0", "prompt": "^1.2.2", - "puppeteer": "^15.5.0", + "puppeteer": "^16.0.0", "puppeteer-extra": "^3.3.4", "puppeteer-extra-plugin-stealth": "^2.10.4", "react": "^17.0.2", @@ -60,11 +60,11 @@ "@types/prompt": "^1.1.2", "@types/react": "^17.0.41", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.30.7", - "@typescript-eslint/parser": "^5.30.4", + "@typescript-eslint/eslint-plugin": "^5.32.0", + "@typescript-eslint/parser": "^5.32.0", "depcheck": "^1.4.3", - "eslint": "^8.19.0", - "eslint-plugin-jest": "^26.1.5", + "eslint": "^8.21.0", + "eslint-plugin-jest": "^26.7.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-unused-imports": "^2.0.0", "husky": "^8.0.1", diff --git a/src/configuration_parser.ts b/src/configuration_parser.ts index bc68475..c80d160 100644 --- a/src/configuration_parser.ts +++ b/src/configuration_parser.ts @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -import fs from 'fs'; +import fs from "fs"; -import {ArgumentParser} from 'argparse'; +import {ArgumentParser} from "argparse"; -import logger from './logger.js'; +import logger from "./logger.js"; import {Option, StringListOption} from "./options.js"; export class ConfigurationParser { @@ -24,7 +24,7 @@ export class ConfigurationParser { // Parse arguments const parser = new ArgumentParser(); - parser.add_argument('--config', '-c', {default: 'config.json'}); + parser.add_argument("--config", "-c", {default: "config.json"}); for (const option of this.#options) { if (option.alias) { parser.add_argument(option.name, option.alias, option.argparseOptions); @@ -35,8 +35,8 @@ export class ConfigurationParser { const args = parser.parse_args(); const getJsonKey = (option: Option): string => { - return option.name.replace(/^-+/g, '').replace(/-/g, '_'); - } + return option.name.replace(/^-+/g, "").replace(/-/g, "_"); + }; const getOptionByName = (name: string): Option | null => { for (const option of this.#options) { @@ -45,15 +45,15 @@ export class ConfigurationParser { } } return null; - } + }; // Load config from file if it exists let config: any = {}; - logger.info('Loading config file: ' + args['config']); - const configFileExists = fs.existsSync(args['config']); + logger.info("Loading config file: " + args["config"]); + const configFileExists = fs.existsSync(args["config"]); if (configFileExists) { try { - config = JSON.parse(fs.readFileSync(args['config'], {encoding: 'utf-8'})); + config = JSON.parse(fs.readFileSync(args["config"], {encoding: "utf-8"})); // Check for unknown options for (const key of Object.keys(config)) { @@ -71,19 +71,19 @@ export class ConfigurationParser { continue; } for (const item of value) { - if (typeof item !== 'string') { + if (typeof item !== "string") { throw new Error(`Error parsing option "${key}": Item is not a string: ${item}`); } } } } } catch (error) { - logger.error('Failed to read config file!'); + logger.error("Failed to read config file!"); logger.error(error); process.exit(1); } } else { - logger.warn('Config file not found! Creating a default one...'); + logger.warn("Config file not found! Creating a default one..."); } // Override options from config with options from arguments and set defaults @@ -92,14 +92,14 @@ export class ConfigurationParser { if (args[key] === undefined) { if (config[key] === undefined) { const defaultValue = option.defaultValue; - if (typeof defaultValue === 'function') { + if (typeof defaultValue === "function") { config[key] = defaultValue(); } else { config[key] = defaultValue; } } } else { - if (typeof args[key] === 'string') { + if (typeof args[key] === "string") { config[key] = option.parse(args[key]); } else { config[key] = args[key]; @@ -110,8 +110,8 @@ export class ConfigurationParser { // Save config file if it didn't exist if (this.#saveIfNotExist) { if (!configFileExists) { - fs.writeFileSync(args['config'], JSON.stringify(config, null, 4)); - logger.info('Config saved to ' + args['config']); + fs.writeFileSync(args["config"], JSON.stringify(config, null, 4)); + logger.info("Config saved to " + args["config"]); } } diff --git a/src/index.tsx b/src/index.tsx index 9ec943a..8a44543 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,8 +3,8 @@ import path from "node:path"; import {fileURLToPath} from "node:url"; import url from "node:url"; -import puppeteer from 'puppeteer-extra'; -import StealthPlugin from 'puppeteer-extra-plugin-stealth'; +import puppeteer from "puppeteer-extra"; +import StealthPlugin from "puppeteer-extra-plugin-stealth"; puppeteer.use(StealthPlugin()); @@ -14,11 +14,11 @@ import cliProgress from "cli-progress"; const {BarFormat} = cliProgress.Format; -import logger from './logger.js'; -import {DropCampaign, getDropBenefitNames, TimeBasedDrop} from './twitch.js'; -import {StringOption, BooleanOption, IntegerOption, StringListOption, JsonOption} from './options.js'; -import {TwitchDropsBot} from './twitch_drops_bot.js'; -import {ConfigurationParser} from './configuration_parser.js'; +import logger from "./logger.js"; +import {DropCampaign, getDropBenefitNames, TimeBasedDrop} from "./twitch.js"; +import {StringOption, BooleanOption, IntegerOption, StringListOption, JsonOption} from "./options.js"; +import {TwitchDropsBot} from "./twitch_drops_bot.js"; +import {ConfigurationParser} from "./configuration_parser.js"; import {LoginPage} from "./pages/login.js"; import {Application} from "./ui/ui.js"; import {compareVersionString, getLatestDevelopmentVersion, getLatestReleaseVersion} from "./utils.js"; @@ -35,14 +35,14 @@ try { } function onBrowserOrPageClosed() { - logger.info('Browser was disconnected or tab was closed! Exiting...'); + logger.info("Browser was disconnected or tab was closed! Exiting..."); process.exit(1); } function getUsernameFromCookies(cookies: any) { for (const cookie of cookies) { - if (cookie['name'] === 'name' || cookie['name'] === 'login') { - return cookie['value']; + if (cookie["name"] === "name" || cookie["name"] === "login") { + return cookie["value"]; } } } @@ -51,7 +51,7 @@ function areCookiesValid(cookies: any) { let isOauthTokenFound = false; for (const cookie of cookies) { // Check if we have an OAuth token - if (cookie['name'] === 'auth-token') { + if (cookie["name"] === "auth-token") { isOauthTokenFound = true; } } @@ -105,7 +105,7 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) { logger.debug("Done checking for updates"); }); setTimeout(checkForUpdates, 1000 * 60 * 60 * 24); - } + }; checkForUpdates(); } @@ -118,7 +118,7 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) { const progressBarHeight: number = 3; function ansiEscape(code: string): string { - return '\x1B[' + code; + return "\x1B[" + code; } const startProgressBar = (p = payload) => { @@ -127,26 +127,26 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) { isProgressBarStarted = true; isFirstOutput = true; for (let i = 0; i < progressBarHeight; ++i) { - process.stdout.write('\n'); + process.stdout.write("\n"); } process.stdout.write(ansiEscape(`${progressBarHeight}A`)); progressBar.start(1, 0, p); } - } + }; const updateProgressBar = (p = payload) => { payload = p; if (progressBar !== null) { progressBar.update(0, p); } - } + }; const stopProgressBar = (clear: boolean = false) => { if (isProgressBarStarted) { isProgressBarStarted = false; progressBar.stop(); for (let i = 0; i < progressBarHeight - 1; ++i) { - process.stdout.write(ansiEscape(`1B`) + ansiEscape("2K")); + process.stdout.write(ansiEscape("1B") + ansiEscape("2K")); } process.stdout.write(ansiEscape(`${progressBarHeight - 1}A`)); } @@ -154,15 +154,15 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) { progressBar = null; payload = null; } - } + }; // Intercept logging messages to stop/start the progress bar const onBeforeLogMessage = () => { stopProgressBar(); - } + }; const onAfterLogMessage = () => { startProgressBar(); - } + }; for (const level of Object.keys(logger.levels)) { // @ts-ignore const og = logger[level]; @@ -173,7 +173,7 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) { const result = og(args); onAfterLogMessage(); return result; - } + }; } let currentDrop: TimeBasedDrop | null = null; @@ -196,7 +196,7 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) { stream: process.stdout, gracefulExit: false, // fixes too many sigint listeners format: (options: any, params: any, payload: any) => { - let result = 'Watching ' + payload['stream_url'] + ` | Viewers: ${payload['viewers']} | Uptime: ${payload['uptime']}` + ansiEscape('0K') + '\n'; + let result = "Watching " + payload["stream_url"] + ` | Viewers: ${payload["viewers"]} | Uptime: ${payload["uptime"]}` + ansiEscape("0K") + "\n"; const drop = currentDrop; if (drop) { @@ -205,11 +205,11 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) { if (campaign) { result += `${ansiEscape("36m")}${campaign.game.name ?? campaign.game.displayName}${ansiEscape("39m")} | ${ansiEscape("35m")}${campaign.name}${ansiEscape("39m")}\n`; } else { - result += ansiEscape("2K") + '\n' + result += ansiEscape("2K") + "\n"; } - result += `${getDropBenefitNames(drop)} ${BarFormat((drop.self.currentMinutesWatched ?? 0) / drop.requiredMinutesWatched, options)} ${drop.self.currentMinutesWatched ?? 0} / ${drop.requiredMinutesWatched} minutes` + ansiEscape('0K') + '\n'; + result += `${getDropBenefitNames(drop)} ${BarFormat((drop.self.currentMinutesWatched ?? 0) / drop.requiredMinutesWatched, options)} ${drop.self.currentMinutesWatched ?? 0} / ${drop.requiredMinutesWatched} minutes` + ansiEscape("0K") + "\n"; } else { - result += ansiEscape("2K") + `- No Drops Active -\n`; + result += ansiEscape("2K") + "- No Drops Active -\n"; result += ansiEscape("2K") +" \n"; } @@ -222,7 +222,7 @@ function startProgressBarMode(bot: TwitchDropsBot, config: Config) { }, cliProgress.Presets.shades_classic ); - progressBar.on('redraw-post', () => { + progressBar.on("redraw-post", () => { isFirstOutput = false; }); startProgressBar(data); @@ -257,11 +257,11 @@ function startUiMode(bot: TwitchDropsBot, config: Config) { // Options defined here can be configured in either the config file or as command-line arguments const options = [ - new StringOption('--username', {alias: '-u'}), - new StringOption('--password', {alias: '-p'}), + new StringOption("--username", {alias: "-u"}), + new StringOption("--password", {alias: "-p"}), new StringOption("--auth-token"), - new StringOption('--browser', { - alias: '-b', + new StringOption("--browser", { + alias: "-b", defaultValue: () => { switch (process.platform) { case "win32": @@ -285,20 +285,20 @@ const options = [ } } }), - new StringListOption('--games', {alias: '-g'}), - new BooleanOption('--headless', false, {defaultValue: true}), - new BooleanOption('--headless-login'), - new IntegerOption('--interval', {alias: '-i', defaultValue: 15}), - new IntegerOption('--load-timeout-secs', {alias: '-t', defaultValue: 30}), - new IntegerOption('--failed-stream-retry', {defaultValue: 3}), - new IntegerOption('--failed-stream-timeout', {defaultValue: 30}), - new StringListOption('--browser-args'), + new StringListOption("--games", {alias: "-g"}), + new BooleanOption("--headless", false, {defaultValue: true}), + new BooleanOption("--headless-login"), + new IntegerOption("--interval", {alias: "-i", defaultValue: 15}), + new IntegerOption("--load-timeout-secs", {alias: "-t", defaultValue: 30}), + new IntegerOption("--failed-stream-retry", {defaultValue: 3}), + new IntegerOption("--failed-stream-timeout", {defaultValue: 30}), + new StringListOption("--browser-args"), /* new BooleanOption('--update-games', null, false), TODO: auto update games.csv ? */ - new BooleanOption('--watch-unlisted-games'), - new BooleanOption('--hide-video'), - new StringOption('--cookies-path'), - new StringOption('--log-level'), - new BooleanOption('--show-account-not-linked-warning', false, {defaultValue: true, alias: '-sanlw'}), + new BooleanOption("--watch-unlisted-games"), + new BooleanOption("--hide-video"), + new StringOption("--cookies-path"), + new StringOption("--log-level"), + new BooleanOption("--show-account-not-linked-warning", false, {defaultValue: true, alias: "-sanlw"}), new StringListOption("--ignored-games"), new BooleanOption("--attempt-impossible-campaigns", false, {defaultValue: true}), new BooleanOption("--watch-streams-when-no-drop-campaigns-active", true, {alias: "-wswndca"}), @@ -324,7 +324,7 @@ const options = [ defaultValue: { enabled: true, file: undefined, - level: 'debug' + level: "debug" } }), new JsonOption<{ @@ -436,7 +436,7 @@ async function main() { filename: fileName, level: level, options: { - flags: 'w' // Overwrite file + flags: "w" // Overwrite file } })); } @@ -455,11 +455,11 @@ async function main() { // Add default browser args const defaultBrowserArgs = [ - '--mute-audio', - '--disable-background-timer-throttling', - '--disable-backgrounding-occluded-windows', - '--disable-renderer-backgrounding', - '--window-size=1920,1080', + "--mute-audio", + "--disable-background-timer-throttling", + "--disable-backgrounding-occluded-windows", + "--disable-renderer-backgrounding", + "--window-size=1920,1080", "--disable-features=HardwareMediaKeyHandling" ]; @@ -475,7 +475,7 @@ async function main() { for (const arg of defaultBrowserArgs) { const argName = arg.split("=")[0]; if (!argNames.includes(argName)) { - config['browser_args'].push(arg); + config["browser_args"].push(arg); } } @@ -496,7 +496,7 @@ async function main() { config["headless_login"] = requiredHeadlessLogin; } - const requiredBrowserArgs = ["--no-sandbox"] + const requiredBrowserArgs = ["--no-sandbox"]; const actualBrowserArgs = config["browser_args"]; const actualBrowserArgsNames = getArgNames(actualBrowserArgs); for (const arg of requiredBrowserArgs) { @@ -510,29 +510,29 @@ async function main() { } // Make username lowercase - if (config['username']) { - config['username'] = config['username'].toLowerCase(); + if (config["username"]) { + config["username"] = config["username"].toLowerCase(); } // Print masked config - logger.debug('Using config: ' + JSON.stringify(createMaskedConfig(config), null, 4)); + logger.debug("Using config: " + JSON.stringify(createMaskedConfig(config), null, 4)); // Start browser const browser = await puppeteer.launch({ - headless: config['headless'], - executablePath: config['browser'], - args: config['browser_args'] + headless: config["headless"], + executablePath: config["browser"], + args: config["browser_args"] }); // Automatically stop this program if the browser is closed - browser.on('disconnected', onBrowserOrPageClosed); + browser.on("disconnected", onBrowserOrPageClosed); // Check if we have saved cookies let cookiesPath = null; if (config.cookies_path) { cookiesPath = config.cookies_path; } else if (config.username) { - cookiesPath = `./cookies-${config['username']}.json`; + cookiesPath = `./cookies-${config["username"]}.json`; } else { // The user has not specified a cookies path or a username, lets see if there are any saved cookies in this directory. // If there are, lets use those. @@ -547,26 +547,26 @@ async function main() { if (cookiesPath && fs.existsSync(cookiesPath)) { // Load cookies - cookies = JSON.parse(fs.readFileSync(cookiesPath, 'utf-8')); + cookies = JSON.parse(fs.readFileSync(cookiesPath, "utf-8")); // Make sure these cookies are valid if (areCookiesValid(cookies)) { // If both cookies and a username are provided and the provided username does not match the username stored in the cookies, warn the user and prefer to use the one from the cookies. - const username = config['username']; + const username = config["username"]; if (username && (username !== getUsernameFromCookies(cookies))) { - logger.warn('Provided username does not match the one found in the cookies! Using the cookies to login...'); + logger.warn("Provided username does not match the one found in the cookies! Using the cookies to login..."); config.username = getUsernameFromCookies(cookies); } // Restore cookies from previous session - logger.info('Restoring cookies from last session.'); + logger.info("Restoring cookies from last session."); areSavedCookiesValid = true; } else { // Saved cookies are invalid, let's delete them - logger.info('Saved cookies are invalid.') + logger.info("Saved cookies are invalid."); fs.unlinkSync(cookiesPath); } @@ -604,27 +604,27 @@ async function main() { // If the saved cookies are not valid, and we don't have an auth token, then we have to log in with username and password if (!areSavedCookiesValid) { - logger.info('Logging in...'); + logger.info("Logging in..."); // Validate options - if (config['headless_login'] && (config['username'] === undefined || config['password'] === undefined) && config.auth_token === undefined) { + if (config["headless_login"] && (config["username"] === undefined || config["password"] === undefined) && config.auth_token === undefined) { logger.error("You must provide a username and password or an auth token to use headless login!"); process.exit(1); } // Check if we need to create a new headful browser for the login - const needNewBrowser = config['headless'] && !config['headless_login']; + const needNewBrowser = config["headless"] && !config["headless_login"]; let loginBrowser = browser; if (needNewBrowser) { loginBrowser = await puppeteer.launch({ headless: false, - executablePath: config['browser'], - args: config['browser_args'] + executablePath: config["browser"], + args: config["browser_args"] }); } const loginPage = new LoginPage(await loginBrowser.newPage()); - cookies = await loginPage.login(config['username'], config['password'], config['headless_login'], config['load_timeout_secs']); + cookies = await loginPage.login(config["username"], config["password"], config["headless_login"], config["load_timeout_secs"]); if (needNewBrowser) { await loginBrowser.close(); @@ -635,20 +635,20 @@ async function main() { // Save cookies cookiesPath = `./cookies-${config.username}.json`; fs.writeFileSync(cookiesPath, JSON.stringify(cookies, null, 4)); - logger.info('Saved cookies to ' + cookiesPath); + logger.info("Saved cookies to " + cookiesPath); } const bot = await TwitchDropsBot.create(browser, cookies, { - gameIds: config['games'], - failedStreamBlacklistTimeout: config['failed_stream_timeout'], - failedStreamRetryCount: config['failed_stream_retry'], - dropCampaignPollingInterval: config['interval'], - loadTimeoutSeconds: config['load_timeout_secs'], - hideVideo: config['hide_video'], - watchUnlistedGames: config['watch_unlisted_games'], - showAccountNotLinkedWarning: config['show_account_not_linked_warning'], - ignoredGameIds: config['ignored_games'], - attemptImpossibleDropCampaigns: config['attempt_impossible_campaigns'], + gameIds: config["games"], + failedStreamBlacklistTimeout: config["failed_stream_timeout"], + failedStreamRetryCount: config["failed_stream_retry"], + dropCampaignPollingInterval: config["interval"], + loadTimeoutSeconds: config["load_timeout_secs"], + hideVideo: config["hide_video"], + watchUnlistedGames: config["watch_unlisted_games"], + showAccountNotLinkedWarning: config["show_account_not_linked_warning"], + ignoredGameIds: config["ignored_games"], + attemptImpossibleDropCampaigns: config["attempt_impossible_campaigns"], watchStreamsWhenNoDropCampaignsActive: config["watch_streams_when_no_drop_campaigns_active"], broadcasterIds: config["broadcasters"] }); diff --git a/src/logger.ts b/src/logger.ts index 93f5ee7..b377e1f 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,9 +1,9 @@ -import {transports, createLogger, format} from 'winston'; +import {transports, createLogger, format} from "winston"; // Set up logger const logger = createLogger({ format: format.combine( - format.timestamp({format: 'YYYY-MM-DD HH:mm:ss'}), + format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}), format.printf(info => { let result = `[${info.timestamp}] [${info.level}]`; if (info.stack) { diff --git a/src/options.ts b/src/options.ts index a39240c..2c9c5e8 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; export abstract class Option { @@ -77,7 +77,7 @@ export class BooleanOption extends Option { // Both 'store_true' and 'store_false' actions automatically create a default of false/true when no argument is // passed. This interferes with our custom default argument handling because we expect to get 'undefined' when no // argument is passed. Changing the actions to 'store_const' avoids this problem. - const argparseOptions = {action: 'store_const', const: true}; + const argparseOptions = {action: "store_const", const: true}; optional ??= {}; @@ -89,12 +89,12 @@ export class BooleanOption extends Option { parse(string: string): boolean { switch (string) { - case 'true': + case "true": return true; - case 'false': + case "false": return false; } - throw new Error('Invalid boolean string: ' + string); + throw new Error("Invalid boolean string: " + string); } } @@ -105,7 +105,7 @@ export class IntegerOption extends Option { optional ??= {}; optional.defaultValue ??= 0; if (optional.argparseOptions) { - optional.argparseOptions['type'] = 'int'; + optional.argparseOptions["type"] = "int"; } super(name, optional); } @@ -130,11 +130,11 @@ export class StringListOption extends Option { let isEscaped = false; for (const c of string) { if (!isEscaped) { - if (c === '\\') { + if (c === "\\") { isEscaped = true; continue; } - if (c === ',') { + if (c === ",") { items.push(item); item = ""; continue; @@ -146,7 +146,7 @@ export class StringListOption extends Option { if (item.length > 0) { items.push(item); } - return items.filter((item) => {return item.length > 0}); + return items.filter((item) => {return item.length > 0;}); } } diff --git a/src/twitch_drops_bot.ts b/src/twitch_drops_bot.ts index be174bb..53f4f69 100644 --- a/src/twitch_drops_bot.ts +++ b/src/twitch_drops_bot.ts @@ -18,7 +18,7 @@ import CommunityPointsComponent from "./components/community_points.js"; import WebSocketListener from "./web_socket_listener.js"; import {TwitchDropsWatchdog} from "./watchdog.js"; import {StreamPage} from "./pages/stream.js"; -import utils, {TimedSet, waitForResponseWithOperationName} from './utils.js'; +import utils, {TimedSet, waitForResponseWithOperationName} from "./utils.js"; import logger from "./logger.js"; import {Client, TimeBasedDrop, DropCampaign, StreamTag, getInventoryDrop, Tag, Inventory, isDropCompleted, getStreamUrl} from "./twitch.js"; import {NoStreamsError, NoProgressError, HighPriorityError, StreamLoadFailedError, StreamDownError} from "./errors.js"; @@ -338,13 +338,13 @@ export class TwitchDropsBot extends EventEmitter { let oauthToken: string | undefined = undefined; let channelLogin: string | undefined = undefined; for (const cookie of cookies) { - switch (cookie['name']) { - case 'auth-token': // OAuth token - oauthToken = cookie['value']; + switch (cookie["name"]) { + case "auth-token": // OAuth token + oauthToken = cookie["value"]; break; - case 'persistent': // "channelLogin" Used for "DropCampaignDetails" operation - channelLogin = cookie['value'].split('%3A')[0]; + case "persistent": // "channelLogin" Used for "DropCampaignDetails" operation + channelLogin = cookie["value"].split("%3A")[0]; break; } } @@ -355,7 +355,7 @@ export class TwitchDropsBot extends EventEmitter { // Seems to be the default hard-coded client ID // Found in sources / static.twitchcdn.net / assets / minimal-cc607a041bc4ae8d6723.js - const client = new Client('kimne78kx3ncx6brgo4mv6wki5h1ko', oauthToken, channelLogin); + const client = new Client("kimne78kx3ncx6brgo4mv6wki5h1ko", oauthToken, channelLogin); if (!channelLogin) { await client.autoDetectUserId(); logger.info("auto detected user id"); @@ -376,7 +376,7 @@ export class TwitchDropsBot extends EventEmitter { if (this.#isFirstWatchdogFinished) { this.emit("new_drops_campaign_found", campaign); } - }) + }); const getDropCampaigns = this.#twitchClient.getDropCampaigns.bind(this.#twitchClient); this.#twitchClient.getDropCampaigns = async () => { @@ -385,13 +385,13 @@ export class TwitchDropsBot extends EventEmitter { this.#database.addOrUpdateDropCampaign(campaign); } return campaigns; - } + }; const getDropCampaignDetails = this.#twitchClient.getDropCampaignDetails.bind(this.#twitchClient); this.#twitchClient.getDropCampaignDetails = async (dropId: string) => { const details = await getDropCampaignDetails(dropId); this.#database.addOrUpdateDropCampaign(details); return details; - } + }; const getInventory = this.#twitchClient.getInventory.bind(this.#twitchClient); this.#twitchClient.getInventory = async () => { const inventory = await getInventory(); @@ -408,7 +408,7 @@ export class TwitchDropsBot extends EventEmitter { } } return inventory; - } + }; options?.gameIds?.forEach((id => { this.#gameIds.push(id); @@ -435,16 +435,16 @@ export class TwitchDropsBot extends EventEmitter { // Set up Twitch Drops Watchdog this.#twitchDropsWatchdog = new TwitchDropsWatchdog(this.#twitchClient, this.#dropCampaignPollingInterval); - this.#twitchDropsWatchdog.on('before_update', () => { - logger.debug('Updating drop campaigns...'); + this.#twitchDropsWatchdog.on("before_update", () => { + logger.debug("Updating drop campaigns..."); this.emit("before_drop_campaigns_updated"); }); - this.#twitchDropsWatchdog.on('error', (error) => { + this.#twitchDropsWatchdog.on("error", (error) => { logger.debug("Error checking twitch drops: " + error); - }) - this.#twitchDropsWatchdog.on('update', async (campaigns: DropCampaign[]) => { + }); + this.#twitchDropsWatchdog.on("update", async (campaigns: DropCampaign[]) => { - logger.debug('Found ' + campaigns.length + ' campaigns.'); + logger.debug("Found " + campaigns.length + " campaigns."); // Claim any Drops that are ready to be claimed let inventory = null; @@ -501,7 +501,7 @@ export class TwitchDropsBot extends EventEmitter { // Ignore campaigns that are not either active or upcoming const campaignStatus = campaign.status; - if (campaignStatus !== 'ACTIVE' && campaignStatus !== 'UPCOMING') { + if (campaignStatus !== "ACTIVE" && campaignStatus !== "UPCOMING") { continue; } @@ -529,9 +529,9 @@ export class TwitchDropsBot extends EventEmitter { this.#pendingDropCampaignIds.insert(dropCampaignId); } } - logger.debug('Found ' + this.#pendingDropCampaignIds.length + ' pending campaigns.'); + logger.debug("Found " + this.#pendingDropCampaignIds.length + " pending campaigns."); - this.emit('pending_drop_campaigns_updated', this.#pendingDropCampaignIds.map((item: string) => { + this.emit("pending_drop_campaigns_updated", this.#pendingDropCampaignIds.map((item: string) => { return this.#database.getDropCampaignById(item); })); @@ -563,7 +563,7 @@ export class TwitchDropsBot extends EventEmitter { if (isDropCompleted(drop, inventory)) { if (!this.#completedDropIds.has(drop.id)) { this.#completedDropIds.add(drop.id); - logger.debug('discovered completed drop: ' + drop.id); + logger.debug("discovered completed drop: " + drop.id); } } else { return false; @@ -653,7 +653,7 @@ export class TwitchDropsBot extends EventEmitter { } // Check if this drop campaign is active - if (campaign.status !== 'ACTIVE') { + if (campaign.status !== "ACTIVE") { continue; } @@ -714,7 +714,7 @@ export class TwitchDropsBot extends EventEmitter { logger.debug(error); } - logger.info('Higher priority campaign found: ' + this.#getDropCampaignFullName(dropCampaignId) + ' id: ' + dropCampaignId); + logger.info("Higher priority campaign found: " + this.#getDropCampaignFullName(dropCampaignId) + " id: " + dropCampaignId); return true; } @@ -736,7 +736,7 @@ export class TwitchDropsBot extends EventEmitter { } // Check if this drop campaign is active - if (campaign.status !== 'ACTIVE') { + if (campaign.status !== "ACTIVE") { continue; } @@ -809,14 +809,14 @@ export class TwitchDropsBot extends EventEmitter { */ async waitForDropCampaignUpdateOrTimeout(sleepTime: number) { const timeout = setTimeout(() => { - logger.debug('notify all!'); + logger.debug("notify all!"); this.#pendingDropCampaignIdsNotifier.notifyAll(); }, sleepTime); - logger.debug('waiting for waitNotify: ' + timeout); + logger.debug("waiting for waitNotify: " + timeout); await this.#pendingDropCampaignIdsNotifier.wait(); - logger.debug('clear: ' + timeout) + logger.debug("clear: " + timeout); clearTimeout(timeout); - logger.debug('done'); + logger.debug("done"); } /** @@ -855,7 +855,7 @@ export class TwitchDropsBot extends EventEmitter { await this.waitForDropCampaignUpdateOrTimeout(this.sleepTimeMilliseconds); continue; } - logger.info("stream: " + streamUrl) + logger.info("stream: " + streamUrl); const dropProgressComponent = new DropProgressComponent({requireProgress: false, exitOnClaim: false}); @@ -886,7 +886,7 @@ export class TwitchDropsBot extends EventEmitter { } timeout = setTimeout(a, 1000 * 60 * 5); - } + }; timeout = setTimeout(a, 0); // Watch stream @@ -914,7 +914,7 @@ export class TwitchDropsBot extends EventEmitter { const sleepTime = Math.max(0, this.sleepTimeMilliseconds - (new Date().getTime() - minLastDropCampaignCheckTime)); // Sleep - logger.info('No campaigns active/streams online. Checking again in ' + (sleepTime / 1000 / 60).toFixed(1) + ' min.'); + logger.info("No campaigns active/streams online. Checking again in " + (sleepTime / 1000 / 60).toFixed(1) + " min."); await this.waitForDropCampaignUpdateOrTimeout(sleepTime); } @@ -925,7 +925,7 @@ export class TwitchDropsBot extends EventEmitter { //logger.info('campaign completed'); } catch (error) { if (error instanceof NoStreamsError) { - logger.info('No streams!'); + logger.info("No streams!"); } else if (error instanceof HighPriorityError) { // Ignore } else { @@ -953,7 +953,7 @@ export class TwitchDropsBot extends EventEmitter { async #processDropCampaign(dropCampaignId: string) { // Attempt to make progress towards the current drop campaign - logger.info('Processing campaign: ' + this.#getDropCampaignFullName(dropCampaignId)); + logger.info("Processing campaign: " + this.#getDropCampaignFullName(dropCampaignId)); this.#lastDropCampaignAttemptTimes[dropCampaignId] = new Date().getTime(); const campaign = this.#database.getDropCampaignById(dropCampaignId); @@ -966,7 +966,7 @@ export class TwitchDropsBot extends EventEmitter { // in the twitch inventory), but they will not show up in-game until their account is linked. if (!campaign.self.isAccountConnected) { if (this.#showAccountNotLinkedWarning) { - logger.warn('Twitch account not linked for this campaign!'); + logger.warn("Twitch account not linked for this campaign!"); } } @@ -974,7 +974,7 @@ export class TwitchDropsBot extends EventEmitter { this.#currentDropCampaignDetails = details; this.#database.addOrUpdateDropCampaign(details); - logger.debug('working on campaign ' + JSON.stringify(campaign, null, 4)); + logger.debug("working on campaign " + JSON.stringify(campaign, null, 4)); // Reset failed stream counts for (const streamUrl of Object.getOwnPropertyNames(this.#failedStreamUrlCounts)) { @@ -987,10 +987,10 @@ export class TwitchDropsBot extends EventEmitter { const drop = this.#getFirstUnclaimedDrop(details, inventory); if (drop === null) { - logger.info('no active drops'); + logger.info("no active drops"); break; } - logger.debug('working on drop ' + JSON.stringify(drop, null, 4)); + logger.debug("working on drop " + JSON.stringify(drop, null, 4)); let currentMinutesWatched = 0; @@ -1010,7 +1010,7 @@ export class TwitchDropsBot extends EventEmitter { const remainingWatchTimeMinutes = drop.requiredMinutesWatched - currentMinutesWatched; const remainingDropActiveTimeMinutes = (new Date(Date.parse(drop.endAt)).getTime() - new Date().getTime()) / 1000 / 60; if (remainingDropActiveTimeMinutes < remainingWatchTimeMinutes) { - logger.warn('impossible drop! remaining campaign time (minutes): ' + remainingDropActiveTimeMinutes + ' required watch time (minutes): ' + remainingWatchTimeMinutes); + logger.warn("impossible drop! remaining campaign time (minutes): " + remainingDropActiveTimeMinutes + " required watch time (minutes): " + remainingWatchTimeMinutes); break; } } @@ -1072,20 +1072,20 @@ export class TwitchDropsBot extends EventEmitter { // Get a list of active streams that have drops enabled let streams = await this.#getActiveStreams(dropCampaignId, details); - logger.debug('Found ' + streams.length + ' active streams'); + logger.debug("Found " + streams.length + " active streams"); // Filter out streams that failed too many times streams = streams.filter(stream => { return !this.#streamUrlTemporaryBlacklist.has(getStreamUrl(stream.broadcaster.login)); }); - logger.info('Found ' + streams.length + ' good streams'); + logger.info("Found " + streams.length + " good streams"); if (streams.length === 0) { return null; } return getStreamUrl(streams[0].broadcaster.login); - } + }; const streamUrl = await getStreamToWatch(); logger.debug("want to watch: " + streamUrl); @@ -1103,10 +1103,10 @@ export class TwitchDropsBot extends EventEmitter { const components: Component[] = [ dropProgressComponent, new CommunityPointsComponent() - ] + ]; // Watch first stream - logger.info('Watching stream: ' + streamUrl); + logger.info("Watching stream: " + streamUrl); try { await this.#watchStreamWrapper(streamUrl, components); //todo: update campaign info when we claim a drop @@ -1166,9 +1166,9 @@ export class TwitchDropsBot extends EventEmitter { const minStableSizeIterations = 3; while (checkCounts++ <= maxChecks) { - let html: string | undefined = await (await element.getProperty('outerHTML'))?.jsonValue(); + let html: string | undefined = await (await element.getProperty("outerHTML"))?.jsonValue(); if (!html) { - throw new Error('HTML was undefined!'); + throw new Error("HTML was undefined!"); } let currentHTMLSize = html.length; @@ -1196,15 +1196,15 @@ export class TwitchDropsBot extends EventEmitter { } } return null; - } + }; // Set up components const dropProgressComponent = getComponent(DropProgressComponent); if (dropProgressComponent !== null) { - dropProgressComponent.on('drop-claimed', drop => { + dropProgressComponent.on("drop-claimed", drop => { this.#onDropRewardClaimed(drop); }); - dropProgressComponent.on('drop-data-changed', () => { + dropProgressComponent.on("drop-data-changed", () => { this.emit("drop_progress_updated", dropProgressComponent.currentDrop); }); } @@ -1217,9 +1217,9 @@ export class TwitchDropsBot extends EventEmitter { if (error instanceof HighPriorityError) { // Ignore } else if (error instanceof StreamLoadFailedError) { - logger.warn('Stream failed to load!'); + logger.warn("Stream failed to load!"); } else if (error instanceof StreamDownError) { - logger.info('Stream went down'); + logger.info("Stream went down"); // If the stream goes down, add it to the failed stream urls immediately so we don't try it again. // This is needed because getActiveStreams() can return streams that are down if they went down // very recently. @@ -1230,8 +1230,8 @@ export class TwitchDropsBot extends EventEmitter { } else { logger.error("Error watching stream"); logger.debug(error); - if (process.env.SAVE_ERROR_SCREENSHOTS?.toLowerCase() === 'true') { - await utils.saveScreenshotAndHtml(this.#page, 'error'); + if (process.env.SAVE_ERROR_SCREENSHOTS?.toLowerCase() === "true") { + await utils.saveScreenshotAndHtml(this.#page, "error"); } } @@ -1241,7 +1241,7 @@ export class TwitchDropsBot extends EventEmitter { } this.#failedStreamUrlCounts[streamUrl]++; if (this.#failedStreamUrlCounts[streamUrl] >= this.#failedStreamRetryCount) { - logger.error('Stream failed too many times. Giving up for ' + this.#failedStreamBlacklistTimeout + ' minutes...'); + logger.error("Stream failed too many times. Giving up for " + this.#failedStreamBlacklistTimeout + " minutes..."); this.#streamUrlTemporaryBlacklist.add(streamUrl); this.#failedStreamUrlCounts[streamUrl] = 0; } @@ -1263,7 +1263,7 @@ export class TwitchDropsBot extends EventEmitter { let channelId: string | null = null; // Set up web socket listener - webSocketListener.on('stream-down', message => { + webSocketListener.on("stream-down", message => { this.#isStreamDown = true; }); webSocketListener.on("points-earned", data => { @@ -1274,7 +1274,7 @@ export class TwitchDropsBot extends EventEmitter { this.emit("community_points_earned", data); }); webSocketListener.on("drop-progress", data => { - const drop = this.#database.getDropById(data['drop_id']); + const drop = this.#database.getDropById(data["drop_id"]); if (drop) { this.emit("drop_progress_updated", { ...drop, @@ -1300,8 +1300,8 @@ export class TwitchDropsBot extends EventEmitter { channelId = (await channelShellOperationResult.data())["userOrError"]["channel"]["id"]; // Wait for the page to load completely (hopefully). This checks the video player container for any DOM changes and waits until there haven't been any changes for a few seconds. - logger.info('Waiting for page to load...'); - const element = (await this.#page.waitForXPath('//div[@data-a-player-state]')) as puppeteer.ElementHandle; + logger.info("Waiting for page to load..."); + const element = (await this.#page.waitForXPath("//div[@data-a-player-state]")) as puppeteer.ElementHandle; await this.#waitUntilElementRendered(this.#page, element); const streamPage = new StreamPage(this.#page); @@ -1319,16 +1319,16 @@ export class TwitchDropsBot extends EventEmitter { try { // Click "Accept mature content" button await streamPage.acceptMatureContent(); - logger.info('Accepted mature content'); + logger.info("Accepted mature content"); } catch (error) { // Ignore errors, the button is probably not there } try { await streamPage.setLowestStreamQuality(); - logger.info('Set stream to lowest quality'); + logger.info("Set stream to lowest quality"); } catch (error) { - logger.error('Failed to set stream to lowest quality!'); + logger.error("Failed to set stream to lowest quality!"); throw error; } @@ -1336,9 +1336,9 @@ export class TwitchDropsBot extends EventEmitter { if (this.#hideVideoElements) { try { await streamPage.hideVideoElements(); - logger.info('Set stream visibility to hidden'); + logger.info("Set stream visibility to hidden"); } catch (error) { - logger.error('Failed to set stream visibility to hidden!'); + logger.error("Failed to set stream visibility to hidden!"); throw error; } } @@ -1350,7 +1350,7 @@ export class TwitchDropsBot extends EventEmitter { } } return null; - } + }; // Wrap everything in a try/finally block so that we can stop the progress bar at the end try { @@ -1370,7 +1370,7 @@ export class TwitchDropsBot extends EventEmitter { // Check if there is a higher priority stream we should be watching if (this.#pendingHighPriority) { this.#pendingHighPriority = false; - logger.info('Switching to higher priority stream'); + logger.info("Switching to higher priority stream"); throw new HighPriorityError(); } @@ -1389,8 +1389,8 @@ export class TwitchDropsBot extends EventEmitter { this.#viewerCount = await streamPage.getViewersCount(); this.emit("watch_status_updated", { - 'viewers': this.#viewerCount, - 'uptime': await streamPage.getUptime(), + "viewers": this.#viewerCount, + "uptime": await streamPage.getUptime(), stream_url: streamUrl, watch_time: new Date().getTime() - startWatchTime }); @@ -1403,7 +1403,7 @@ export class TwitchDropsBot extends EventEmitter { self: { currentMinutesWatched: getComponent(DropProgressComponent)?.currentMinutesWatched } - } + }; this.emit("drop_progress_updated", fd); } else { this.emit("drop_progress_updated", null); @@ -1451,7 +1451,7 @@ export class TwitchDropsBot extends EventEmitter { if (!campaign) { return "null"; } - return campaign.game.displayName + ' ' + campaign.name; + return campaign.game.displayName + " " + campaign.name; } getDatabase(): Database { diff --git a/src/update_games.ts b/src/update_games.ts index 13d22fa..4904184 100644 --- a/src/update_games.ts +++ b/src/update_games.ts @@ -1,32 +1,32 @@ -'use strict'; +"use strict"; -import fs from 'fs'; -import path from 'path'; +import fs from "fs"; +import path from "path"; -import logger from './logger.js'; -import {Client} from './twitch.js'; -import {StringOption, BooleanOption, StringListOption} from './options.js'; -import {ConfigurationParser} from './configuration_parser.js'; +import logger from "./logger.js"; +import {Client} from "./twitch.js"; +import {StringOption, BooleanOption, StringListOption} from "./options.js"; +import {ConfigurationParser} from "./configuration_parser.js"; import {LoginPage} from "./pages/login.js"; import {updateGames} from "./utils.js"; // Using puppeteer-extra to add plugins -import puppeteer from 'puppeteer-extra'; +import puppeteer from "puppeteer-extra"; // Add stealth plugin -import StealthPlugin from 'puppeteer-extra-plugin-stealth'; +import StealthPlugin from "puppeteer-extra-plugin-stealth"; puppeteer.use(StealthPlugin()); function onBrowserOrPageClosed() { - logger.info('Browser was disconnected or tab was closed! Exiting...'); + logger.info("Browser was disconnected or tab was closed! Exiting..."); process.exit(1); } function getUsernameFromCookies(cookies: any) { for (const cookie of cookies) { - if (cookie['name'] === 'name' || cookie['name'] === 'login') { - return cookie['value']; + if (cookie["name"] === "name" || cookie["name"] === "login") { + return cookie["value"]; } } } @@ -35,7 +35,7 @@ function areCookiesValid(cookies: any) { let isOauthTokenFound = false; for (const cookie of cookies) { // Check if we have an OAuth token - if (cookie['name'] === 'auth-token') { + if (cookie["name"] === "auth-token") { isOauthTokenFound = true; } } @@ -44,10 +44,10 @@ function areCookiesValid(cookies: any) { // Options defined here can be configured in either the config file or as command-line arguments const options = [ - new StringOption('--username', {alias: '-u'}), - new StringOption('--password', {alias: '-p'}), - new StringOption('--browser', { - alias: '-b', + new StringOption("--username", {alias: "-u"}), + new StringOption("--password", {alias: "-p"}), + new StringOption("--browser", { + alias: "-b", defaultValue: () => { switch (process.platform) { case "win32": @@ -57,15 +57,15 @@ const options = [ return path.join("google-chrome"); default: - return ''; + return ""; } } }), - new BooleanOption('--headless', false, {defaultValue: true}), - new BooleanOption('--headless-login'), - new StringListOption('--browser-args'), - new StringOption('--cookies-path'), - new StringOption('--log-level') + new BooleanOption("--headless", false, {defaultValue: true}), + new BooleanOption("--headless-login"), + new StringListOption("--browser-args"), + new StringOption("--cookies-path"), + new StringOption("--log-level") ]; // Parse arguments @@ -73,55 +73,55 @@ const configurationParser = new ConfigurationParser(options); let config: any = configurationParser.parse(); // Set logging level -if (config['log_level']) { +if (config["log_level"]) { // TODO: validate input - logger.level = config['log_level']; + logger.level = config["log_level"]; } // Make username lowercase -if (config['username']) { - config['username'] = config['username'].toLowerCase(); +if (config["username"]) { + config["username"] = config["username"].toLowerCase(); } (async () => { // Start browser and open a new tab. const browser = await puppeteer.launch({ - headless: config['headless'], - executablePath: config['browser'], - args: config['browser_args'] + headless: config["headless"], + executablePath: config["browser"], + args: config["browser_args"] }); const page = await browser.newPage(); // Automatically stop this program if the browser or page is closed - browser.on('disconnected', onBrowserOrPageClosed); - page.on('close', onBrowserOrPageClosed); + browser.on("disconnected", onBrowserOrPageClosed); + page.on("close", onBrowserOrPageClosed); // Check if we have saved cookies - let cookiesPath = config['cookies_path'] || (config['username'] ? `./cookies-${config['username']}.json` : null); + let cookiesPath = config["cookies_path"] || (config["username"] ? `./cookies-${config["username"]}.json` : null); let requireLogin = false; if (fs.existsSync(cookiesPath)) { // Load cookies - const cookies = JSON.parse(fs.readFileSync(cookiesPath, 'utf-8')); + const cookies = JSON.parse(fs.readFileSync(cookiesPath, "utf-8")); // Make sure these cookies are valid if (areCookiesValid(cookies)) { // If both cookies and a username are provided and the provided username does not match the username stored in the cookies, warn the user and prefer to use the one from the cookies. - const username = config['username']; + const username = config["username"]; if (username && (username !== getUsernameFromCookies(cookies))) { - logger.warn('Provided username does not match the one found in the cookies! Using the cookies to login...'); + logger.warn("Provided username does not match the one found in the cookies! Using the cookies to login..."); } // Restore cookies from previous session - logger.info('Restoring cookies from last session.'); + logger.info("Restoring cookies from last session."); await page.setCookie(...cookies); } else { // Saved cookies are invalid, let's delete them - logger.info('Saved cookies are invalid.') + logger.info("Saved cookies are invalid."); fs.unlinkSync(cookiesPath); // We need to login again @@ -135,27 +135,27 @@ if (config['username']) { let cookies = null; if (requireLogin) { - logger.info('Logging in...'); + logger.info("Logging in..."); // Validate options - if (config['headless_login'] && (config['username'] === undefined || config['password'] === undefined)) { + if (config["headless_login"] && (config["username"] === undefined || config["password"] === undefined)) { console.error("You must provide a username and password to use headless login!"); process.exit(1); } // Check if we need to create a new headful browser for the login - const needNewBrowser = config['headless'] && !config['headless_login']; + const needNewBrowser = config["headless"] && !config["headless_login"]; let loginBrowser = browser; if (needNewBrowser) { loginBrowser = await puppeteer.launch({ headless: false, - executablePath: config['browser'], - args: config['browser_args'] + executablePath: config["browser"], + args: config["browser_args"] }); } const loginPage = new LoginPage(await loginBrowser.newPage()); - cookies = await loginPage.login(config['username'], config['password'], config['headless_login'], config['load_timeout_secs']); + cookies = await loginPage.login(config["username"], config["password"], config["headless_login"], config["load_timeout_secs"]); await page.setCookie(...cookies); if (needNewBrowser) { @@ -166,43 +166,43 @@ if (config['username']) { // Get some data from the cookies let oauthToken: string | undefined = undefined; let channelLogin: string | undefined = undefined; - for (const cookie of await page.cookies('https://www.twitch.tv')) { - switch (cookie['name']) { - case 'auth-token': // OAuth token - oauthToken = cookie['value']; + for (const cookie of await page.cookies("https://www.twitch.tv")) { + switch (cookie["name"]) { + case "auth-token": // OAuth token + oauthToken = cookie["value"]; break; - case 'persistent': // "channelLogin" Used for "DropCampaignDetails" operation - channelLogin = cookie['value'].split('%3A')[0]; + case "persistent": // "channelLogin" Used for "DropCampaignDetails" operation + channelLogin = cookie["value"].split("%3A")[0]; break; - case 'login': - config['username'] = cookie['value']; - logger.info('Logged in as ' + cookie['value']); + case "login": + config["username"] = cookie["value"]; + logger.info("Logged in as " + cookie["value"]); break; } } if (!oauthToken || !channelLogin) { - logger.error('Invalid cookies!'); + logger.error("Invalid cookies!"); process.exit(1); } // Save cookies if (requireLogin) { - cookiesPath = `./cookies-${config['username']}.json`; + cookiesPath = `./cookies-${config["username"]}.json`; fs.writeFileSync(cookiesPath, JSON.stringify(cookies)); - logger.info('Saved cookies to ' + cookiesPath); + logger.info("Saved cookies to " + cookiesPath); } // Seems to be the default hard-coded client ID // Found in sources / static.twitchcdn.net / assets / minimal-cc607a041bc4ae8d6723.js - const twitchClient = new Client('kimne78kx3ncx6brgo4mv6wki5h1ko', oauthToken, channelLogin); + const twitchClient = new Client("kimne78kx3ncx6brgo4mv6wki5h1ko", oauthToken, channelLogin); updateGames(await twitchClient.getDropCampaigns()); - browser.off('disconnected', onBrowserOrPageClosed); - page.off('close', onBrowserOrPageClosed); + browser.off("disconnected", onBrowserOrPageClosed); + page.off("close", onBrowserOrPageClosed); await browser.close(); })().catch(error => { diff --git a/src/utils.ts b/src/utils.ts index f868fe7..8e8f333 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import fs from "node:fs" +import fs from "node:fs"; import path from "node:path"; import {HTTPResponse, Page} from "puppeteer"; @@ -11,8 +11,8 @@ import logger from "./logger.js"; export async function saveScreenshotAndHtml(page: Page, pathPrefix: string) { const time = new Date().getTime(); - const screenshotPath = pathPrefix + '-screenshot-' + time + '.png'; - const htmlPath = pathPrefix + '-page-' + time + '.html'; + const screenshotPath = pathPrefix + "-screenshot-" + time + ".png"; + const htmlPath = pathPrefix + "-page-" + time + ".html"; await page.screenshot({ fullPage: true, path: screenshotPath @@ -75,7 +75,7 @@ export class TimedSet extends Set { } export function updateGames(campaigns: DropCampaign[], sourcePath: string = "./games.csv", destinationPath: string = sourcePath) { - logger.info('Parsing games...'); + logger.info("Parsing games..."); // Read games from source file let oldGames = []; @@ -84,9 +84,9 @@ export function updateGames(campaigns: DropCampaign[], sourcePath: string = "./g let oldGamesRaw = fs.readFileSync(sourcePath, {encoding: "utf-8"}); // Detect and replace line endings - if (oldGamesRaw.includes('\r\n')) { - logger.info('File games.csv contains CRLF line endings. Will replace with LF.'); - oldGamesRaw = oldGamesRaw.replace(/\r\n/g, '\n'); + if (oldGamesRaw.includes("\r\n")) { + logger.info("File games.csv contains CRLF line endings. Will replace with LF."); + oldGamesRaw = oldGamesRaw.replace(/\r\n/g, "\n"); } // Parse string into list of columns @@ -102,7 +102,7 @@ export function updateGames(campaigns: DropCampaign[], sourcePath: string = "./g const oldIdToNameMap = new Map(oldGames.map(game => game.reverse())); // Create list of [name, id] for new games - const newGames = campaigns.map(campaign => [campaign['game']['displayName'], campaign['game']['id']]); + const newGames = campaigns.map(campaign => [campaign["game"]["displayName"], campaign["game"]["id"]]); const newIdToNameMap = new Map(); @@ -132,7 +132,7 @@ export function updateGames(campaigns: DropCampaign[], sourcePath: string = "./g } else if (oldName === newName && oldId == newId) { // same data } else { - logger.info("interesting: " + oldName + " vs " + newName + " " + oldId + " vs " + newId) + logger.info("interesting: " + oldName + " vs " + newName + " " + oldId + " vs " + newId); } newIdToNameMap.set(newId, newName); @@ -152,8 +152,8 @@ export function updateGames(campaigns: DropCampaign[], sourcePath: string = "./g fs.writeFileSync( destinationPath, - 'Name,ID\n' + toWrite); - logger.info('Games list updated'); + "Name,ID\n" + toWrite); + logger.info("Games list updated"); } /** @@ -173,7 +173,7 @@ export function waitForResponseWithOperationName(page: Page, operationName: stri const response = await this.promise; return (await response?.json())[this.operationIndex]["data"]; } - } + }; result.promise = page.waitForResponse((response: HTTPResponse) => { if (response.url().startsWith("https://gql.twitch.tv/gql")) { const postData = response.request().postData(); diff --git a/src/watchdog.ts b/src/watchdog.ts index 092795b..cc5b9c2 100644 --- a/src/watchdog.ts +++ b/src/watchdog.ts @@ -22,11 +22,11 @@ export class TwitchDropsWatchdog extends EventEmitter { if (!this.#isRunning) { this.#isRunning = true; const run = () => { - this.emit('before_update'); + this.emit("before_update"); this.#client.getDropCampaigns().then((campaigns: DropCampaign[]) => { - this.emit('update', campaigns); + this.emit("update", campaigns); }).catch((error) => { - this.emit('error', error); + this.emit("error", error); }).finally(() => { this.#timeoutId = setTimeout(run, 1000 * 60 * this.#pollingIntervalMinutes); }); diff --git a/src/web_socket_listener.ts b/src/web_socket_listener.ts index 438743c..1d6066c 100644 --- a/src/web_socket_listener.ts +++ b/src/web_socket_listener.ts @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -import EventEmitter from 'events'; +import EventEmitter from "events"; -import logger from './logger.js'; +import logger from "./logger.js"; import {CDPSession, Page} from "puppeteer"; export interface UserDropEvents_DropProgress { @@ -17,106 +17,106 @@ class WebSocketListener extends EventEmitter { #ignoreTopicHandler = (message: any) => { return true; - } + }; #topicHandlers: { [key: string]: (message: any) => boolean } = { - 'user-drop-events': message => { - const messageType = message['type']; + "user-drop-events": message => { + const messageType = message["type"]; switch (messageType) { - case 'drop-progress': - logger.debug('Drop progress: ' + message['data']['drop_id'] + ' ' + message['data']['current_progress_min'] + ' / ' + message['data']['required_progress_min']); - this.emit(messageType, message['data']); + case "drop-progress": + logger.debug("Drop progress: " + message["data"]["drop_id"] + " " + message["data"]["current_progress_min"] + " / " + message["data"]["required_progress_min"]); + this.emit(messageType, message["data"]); return true; - case 'drop-claim': - logger.debug('DROP READY TO CLAIM: ' + JSON.stringify(message['data'], null, 4)); - this.emit(messageType, message['data']); + case "drop-claim": + logger.debug("DROP READY TO CLAIM: " + JSON.stringify(message["data"], null, 4)); + this.emit(messageType, message["data"]); return true; } return false; }, - 'video-playback-by-id': message => { - const messageType = message['type']; + "video-playback-by-id": message => { + const messageType = message["type"]; switch (messageType) { - case 'viewcount': - this.emit(messageType, parseInt(message['viewers'])); + case "viewcount": + this.emit(messageType, parseInt(message["viewers"])); return true; - case 'stream-down': + case "stream-down": this.emit(messageType, message); return true; - case 'stream-up': + case "stream-up": this.emit(messageType, message); return true; - case 'commercial': + case "commercial": return true; } return false; }, - 'community-points-user-v1': message => { - const messageType = message['type']; + "community-points-user-v1": message => { + const messageType = message["type"]; switch (messageType) { - case 'claim-available': + case "claim-available": this.emit(messageType, message); return true; - case 'points-earned': + case "points-earned": this.emit(messageType, message["data"]); return true; - case 'reward-redeemed': - case 'claim-claimed': + case "reward-redeemed": + case "claim-claimed": case "active-multipliers-updated": return true; } return false; }, - 'presence': this.#ignoreTopicHandler, - 'leaderboard-events-v1': this.#ignoreTopicHandler, - 'predictions-channel-v1': this.#ignoreTopicHandler, - 'broadcast-settings-update': this.#ignoreTopicHandler, - 'ads': this.#ignoreTopicHandler, - 'stream-chat-room-v1': this.#ignoreTopicHandler, - 'raid': this.#ignoreTopicHandler, - 'community-boost-events-v1': this.#ignoreTopicHandler, - 'creator-goals-events-v1': this.#ignoreTopicHandler, - 'channel-sub-gifts-v1': this.#ignoreTopicHandler, - 'channel-ext-v1': this.#ignoreTopicHandler, - 'community-points-channel-v1': this.#ignoreTopicHandler, - 'hype-train-events-v1': this.#ignoreTopicHandler, - 'polls': this.#ignoreTopicHandler, - 'channel-ad-poll-update-events': this.#ignoreTopicHandler, - 'channel-bounty-board-events.cta': this.#ignoreTopicHandler, - 'crowd-chant-channel-v1': this.#ignoreTopicHandler, - 'radio-events-v1': this.#ignoreTopicHandler, - 'stream-change-v1': this.#ignoreTopicHandler, - 'user-subscribe-events-v1': this.#ignoreTopicHandler, - 'onsite-notifications-v1': this.#ignoreTopicHandler, + "presence": this.#ignoreTopicHandler, + "leaderboard-events-v1": this.#ignoreTopicHandler, + "predictions-channel-v1": this.#ignoreTopicHandler, + "broadcast-settings-update": this.#ignoreTopicHandler, + "ads": this.#ignoreTopicHandler, + "stream-chat-room-v1": this.#ignoreTopicHandler, + "raid": this.#ignoreTopicHandler, + "community-boost-events-v1": this.#ignoreTopicHandler, + "creator-goals-events-v1": this.#ignoreTopicHandler, + "channel-sub-gifts-v1": this.#ignoreTopicHandler, + "channel-ext-v1": this.#ignoreTopicHandler, + "community-points-channel-v1": this.#ignoreTopicHandler, + "hype-train-events-v1": this.#ignoreTopicHandler, + "polls": this.#ignoreTopicHandler, + "channel-ad-poll-update-events": this.#ignoreTopicHandler, + "channel-bounty-board-events.cta": this.#ignoreTopicHandler, + "crowd-chant-channel-v1": this.#ignoreTopicHandler, + "radio-events-v1": this.#ignoreTopicHandler, + "stream-change-v1": this.#ignoreTopicHandler, + "user-subscribe-events-v1": this.#ignoreTopicHandler, + "onsite-notifications-v1": this.#ignoreTopicHandler, }; async attach(page: Page) { this.#cdp = await page.target().createCDPSession(); - await this.#cdp.send('Network.enable'); - await this.#cdp.send('Page.enable'); + await this.#cdp.send("Network.enable"); + await this.#cdp.send("Page.enable"); let pubSubWebSocketRequestId: string; - this.#cdp.on('Network.webSocketCreated', socket => { - if (socket['url'] === 'wss://pubsub-edge.twitch.tv/v1') { - pubSubWebSocketRequestId = socket['requestId']; + this.#cdp.on("Network.webSocketCreated", socket => { + if (socket["url"] === "wss://pubsub-edge.twitch.tv/v1") { + pubSubWebSocketRequestId = socket["requestId"]; } }); - this.#cdp.on('Network.webSocketFrameReceived', frame => { - if (frame['requestId'] === pubSubWebSocketRequestId) { - const payload = JSON.parse(frame['response']['payloadData']); - const payloadType = payload['type']; - if (payloadType === 'PONG') { - logger.debug('PONG'); - } else if (payloadType === 'RESPONSE') { - if (payload['error']) { - logger.debug('Error in payload: ' + JSON.stringify(payload, null, 4)); + this.#cdp.on("Network.webSocketFrameReceived", frame => { + if (frame["requestId"] === pubSubWebSocketRequestId) { + const payload = JSON.parse(frame["response"]["payloadData"]); + const payloadType = payload["type"]; + if (payloadType === "PONG") { + logger.debug("PONG"); + } else if (payloadType === "RESPONSE") { + if (payload["error"]) { + logger.debug("Error in payload: " + JSON.stringify(payload, null, 4)); } - } else if (payloadType === 'MESSAGE') { + } else if (payloadType === "MESSAGE") { // TODO: Some topics contain more than one period! /* Example: { @@ -127,30 +127,30 @@ class WebSocketListener extends EventEmitter { } } */ - const topic = payload['data']['topic'].split('.')[0]; - const message = JSON.parse(payload['data']['message']); + const topic = payload["data"]["topic"].split(".")[0]; + const message = JSON.parse(payload["data"]["message"]); - this.emit('message', message); + this.emit("message", message); // Call topic handler const topicHandler = this.#topicHandlers[topic]; if (topicHandler) { if (!topicHandler(message)) { - logger.debug('Unhandled socket message: ' + JSON.stringify(payload, null, 4)); + logger.debug("Unhandled socket message: " + JSON.stringify(payload, null, 4)); } } else { - logger.debug('No topic handler for socket message: ' + JSON.stringify(payload, null, 4)); + logger.debug("No topic handler for socket message: " + JSON.stringify(payload, null, 4)); } } else { - logger.debug('Unknown payload type: ' + JSON.stringify(payload, null, 4)); + logger.debug("Unknown payload type: " + JSON.stringify(payload, null, 4)); } } }); - this.#cdp.on('Network.webSocketFrameError', data => { - logger.error('Web socket frame error:' + JSON.stringify(data, null, 4)); + this.#cdp.on("Network.webSocketFrameError", data => { + logger.error("Web socket frame error:" + JSON.stringify(data, null, 4)); }); - this.#cdp.on('Network.webSocketClosed ', data => { - logger.error('Web socket closed:' + JSON.stringify(data, null, 4)); + this.#cdp.on("Network.webSocketClosed ", data => { + logger.error("Web socket closed:" + JSON.stringify(data, null, 4)); }); } diff --git a/test/options.test.ts b/test/options.test.ts index 57c83df..529bb3c 100644 --- a/test/options.test.ts +++ b/test/options.test.ts @@ -6,7 +6,7 @@ test("StringListOption", () => { const test = (input: string, expected: string[]) => { expect(option.parse(input)).toStrictEqual(expected); - } + }; test("", []); test("a", ["a"]); diff --git a/test/update_games.test.ts b/test/update_games.test.ts index b5fe3c1..a367b59 100644 --- a/test/update_games.test.ts +++ b/test/update_games.test.ts @@ -13,11 +13,11 @@ function cleanup() { beforeAll(() => { cleanup(); // todo: disable logger console output -}) +}); afterAll(() => { cleanup(); -}) +}); test("update_games", () => { @@ -27,19 +27,19 @@ test("update_games", () => { // Normal update updateGames(campaigns, "test/data/games-0-a.csv", DESTINATION_PATH); const gamesRawActual0 = fs.readFileSync(DESTINATION_PATH).toString(); - const gamesRawExpected0 = fs.readFileSync("test/data/games-0-b.csv").toString().replace(/\r\n/g, '\n'); + const gamesRawExpected0 = fs.readFileSync("test/data/games-0-b.csv").toString().replace(/\r\n/g, "\n"); expect(gamesRawActual0).toStrictEqual(gamesRawExpected0); // No changes updateGames(campaigns, "test/data/games-1-a.csv", DESTINATION_PATH); const gamesRawActual1 = fs.readFileSync(DESTINATION_PATH).toString(); - const gamesRawExpected1 = fs.readFileSync("test/data/games-1-b.csv").toString().replace(/\r\n/g, '\n'); + const gamesRawExpected1 = fs.readFileSync("test/data/games-1-b.csv").toString().replace(/\r\n/g, "\n"); expect(gamesRawActual1).toStrictEqual(gamesRawExpected1); // Missing original games.csv updateGames(campaigns, "does/not/exist/games.csv", DESTINATION_PATH); const gamesRawActual2 = fs.readFileSync(DESTINATION_PATH).toString(); - const gamesRawExpected2 = fs.readFileSync("test/data/games-2-b.csv").toString().replace(/\r\n/g, '\n'); + const gamesRawExpected2 = fs.readFileSync("test/data/games-2-b.csv").toString().replace(/\r\n/g, "\n"); expect(gamesRawActual2).toStrictEqual(gamesRawExpected2); });