mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-10 18:36:17 +00:00
v3.1.0 initial
This commit is contained in:
71
.github/workflows/auto-release.yml
vendored
Normal file
71
.github/workflows/auto-release.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Auto Release on package.json version bump
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [v3]
|
||||
paths:
|
||||
- package.json
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read current version
|
||||
id: current
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Read previous version (from previous commit)
|
||||
id: previous
|
||||
run: |
|
||||
PREV_VERSION=$(git show HEAD~1:package.json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" || echo "")
|
||||
echo "version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check if version increased
|
||||
id: check
|
||||
run: |
|
||||
CUR="${{ steps.current.outputs.version }}"
|
||||
PREV="${{ steps.previous.outputs.version }}"
|
||||
|
||||
echo "Current: $CUR"
|
||||
echo "Previous: $PREV"
|
||||
|
||||
# if previous doesn't exist (first commit containing package.json), skip
|
||||
if [ -z "$PREV" ]; then
|
||||
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Compare semver using node's semver package if available; fallback to simple inequality
|
||||
node -e "
|
||||
const cur='${CUR}';
|
||||
const prev='${PREV}';
|
||||
let should = cur !== prev;
|
||||
try {
|
||||
const semver = require('semver');
|
||||
should = semver.gt(cur, prev);
|
||||
} catch (_) {}
|
||||
console.log('should_release=' + should);
|
||||
" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Stop if no bump
|
||||
if: steps.check.outputs.should_release != 'true'
|
||||
run: echo "No version increase detected."
|
||||
|
||||
- name: Create tag + GitHub Release
|
||||
if: steps.check.outputs.should_release == 'true'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.current.outputs.version }}
|
||||
name: v${{ steps.current.outputs.version }}
|
||||
generate_release_notes: true
|
||||
13
dockerignore
Normal file
13
dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
.vscode
|
||||
.DS_Store
|
||||
sessions/
|
||||
.dev/
|
||||
diagnostics/
|
||||
note
|
||||
accounts.dev.json
|
||||
accounts.main.json
|
||||
.playwright-chromium-installed
|
||||
419
package-lock.json
generated
419
package-lock.json
generated
@@ -7,14 +7,14 @@
|
||||
"": {
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "3.0.0",
|
||||
"license": "ISC",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"axios-retry": "^4.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"fingerprint-generator": "^2.1.77",
|
||||
"fingerprint-injector": "^2.1.77",
|
||||
"fingerprint-generator": "^2.1.79",
|
||||
"fingerprint-injector": "^2.1.79",
|
||||
"ghost-cursor-playwright-port": "^1.4.3",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
@@ -22,11 +22,15 @@
|
||||
"otpauth": "^9.4.1",
|
||||
"p-queue": "^9.0.1",
|
||||
"patchright": "^1.57.0",
|
||||
"ts-node": "^10.9.2"
|
||||
"semver": "^7.7.3",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-modules-newline": "^0.0.6",
|
||||
@@ -35,7 +39,7 @@
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -51,9 +55,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -145,9 +149,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
||||
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -157,7 +161,7 @@
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
@@ -179,6 +183,16 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -193,9 +207,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
||||
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -342,13 +356,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
||||
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
|
||||
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.56.1"
|
||||
"playwright": "1.57.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -357,25 +371,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test/node_modules/playwright": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
||||
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.56.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
@@ -389,9 +384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
@@ -440,30 +435,36 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"version": "24.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
||||
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
||||
"integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz",
|
||||
"integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.48.0",
|
||||
"@typescript-eslint/type-utils": "8.48.0",
|
||||
"@typescript-eslint/utils": "8.48.0",
|
||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.51.0",
|
||||
"@typescript-eslint/type-utils": "8.51.0",
|
||||
"@typescript-eslint/utils": "8.51.0",
|
||||
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
"ts-api-utils": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -473,33 +474,23 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.48.0",
|
||||
"@typescript-eslint/parser": "^8.51.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz",
|
||||
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz",
|
||||
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.48.0",
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/typescript-estree": "8.48.0",
|
||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
||||
"@typescript-eslint/scope-manager": "8.51.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -515,14 +506,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz",
|
||||
"integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz",
|
||||
"integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.48.0",
|
||||
"@typescript-eslint/types": "^8.48.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.51.0",
|
||||
"@typescript-eslint/types": "^8.51.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -537,14 +528,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz",
|
||||
"integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz",
|
||||
"integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/visitor-keys": "8.48.0"
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/visitor-keys": "8.51.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -555,9 +546,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz",
|
||||
"integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz",
|
||||
"integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -572,17 +563,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz",
|
||||
"integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz",
|
||||
"integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/typescript-estree": "8.48.0",
|
||||
"@typescript-eslint/utils": "8.48.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||
"@typescript-eslint/utils": "8.51.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
"ts-api-utils": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -597,9 +588,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz",
|
||||
"integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz",
|
||||
"integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -611,21 +602,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz",
|
||||
"integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz",
|
||||
"integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.48.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.48.0",
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/visitor-keys": "8.48.0",
|
||||
"@typescript-eslint/project-service": "8.51.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.51.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||
"debug": "^4.3.4",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
"ts-api-utils": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -639,16 +630,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz",
|
||||
"integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz",
|
||||
"integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.48.0",
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/typescript-estree": "8.48.0"
|
||||
"@typescript-eslint/scope-manager": "8.51.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/typescript-estree": "8.51.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -663,13 +654,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz",
|
||||
"integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz",
|
||||
"integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.48.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -827,9 +818,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.31",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
|
||||
"integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@@ -862,9 +853,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
|
||||
"integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -881,11 +872,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.25",
|
||||
"caniuse-lite": "^1.0.30001754",
|
||||
"electron-to-chromium": "^1.5.249",
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.1.4"
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -917,9 +908,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001757",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
|
||||
"integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
|
||||
"version": "1.0.30001762",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
||||
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1207,9 +1198,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.262",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
|
||||
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
|
||||
"version": "1.5.267",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/encoding-sniffer": {
|
||||
@@ -1305,9 +1296,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1317,7 +1308,7 @@
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -1431,6 +1422,16 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -1476,9 +1477,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -1597,13 +1598,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fingerprint-generator": {
|
||||
"version": "2.1.77",
|
||||
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.77.tgz",
|
||||
"integrity": "sha512-wR15VUEZnwozFiSDRV+40zxlEt3ZV3JNYvLx0CSF9D9smov4pUC6MJZJnlxtDr+Ir4oppU8vn1JXApLk/Qr5Uw==",
|
||||
"version": "2.1.79",
|
||||
"resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.79.tgz",
|
||||
"integrity": "sha512-0dr3kTgvRYHleRPp6OBDcPb8amJmOyFr9aOuwnpN6ooWJ5XyT+/aL/SZ6CU4ZrEtzV26EyJ2Lg7PT32a0NdrRA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"generative-bayesian-network": "^2.1.77",
|
||||
"header-generator": "^2.1.77",
|
||||
"generative-bayesian-network": "^2.1.79",
|
||||
"header-generator": "^2.1.79",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1611,12 +1612,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fingerprint-injector": {
|
||||
"version": "2.1.77",
|
||||
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.77.tgz",
|
||||
"integrity": "sha512-R778SIyrqgWO0P+UWKzIFWUWZz13EGu6UmV7CX3vuFDbsYIL1xiH+s+/nzPSOqFdhXyLo7d8aTOjbGbRLULoQQ==",
|
||||
"version": "2.1.79",
|
||||
"resolved": "https://registry.npmjs.org/fingerprint-injector/-/fingerprint-injector-2.1.79.tgz",
|
||||
"integrity": "sha512-0tKb3wCQ92ZlLLbxRfhoduCI+rIgpsUdb3Jp66TrwoMlSeVDlmoBZfkQW4FheQFau1kg0lIg4Zk4xetBubetQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"fingerprint-generator": "^2.1.77",
|
||||
"fingerprint-generator": "^2.1.79",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1677,9 +1678,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
@@ -1716,9 +1717,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/generative-bayesian-network": {
|
||||
"version": "2.1.77",
|
||||
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.77.tgz",
|
||||
"integrity": "sha512-viU4CRPsmgiklR94LhvdMndaY73BkCH1pGjmOjWbLR/ZwcUd06gKF3TCcsS3npRl74o33YSInSixxm16wIukcA==",
|
||||
"version": "2.1.79",
|
||||
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.79.tgz",
|
||||
"integrity": "sha512-aPH+V2wO+HE0BUX1LbsM8Ak99gmV43lgh+D7GDteM0zgnPqiAwcK9JZPxMPZa3aJUleFtFaL1lAei8g9zNrDIA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.9",
|
||||
@@ -1850,13 +1851,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -1906,13 +1900,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/header-generator": {
|
||||
"version": "2.1.77",
|
||||
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.77.tgz",
|
||||
"integrity": "sha512-ggSG/mfkFMu8CO7xP591G8kp1IJCBvgXu7M8oxTjC9u914JsIzE6zIfoFsXzA+pf0utWJhUsdqU0oV/DtQ4DFQ==",
|
||||
"version": "2.1.79",
|
||||
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.79.tgz",
|
||||
"integrity": "sha512-YvHx8teq4QmV5mz7wdPMsj9n1OZBPnZxA4QE+EOrtx7xbmGvd1gBvDNKCb5XqS4GR/TL75MU5hqMqqqANdILRg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.21.1",
|
||||
"generative-bayesian-network": "^2.1.77",
|
||||
"generative-bayesian-network": "^2.1.79",
|
||||
"ow": "^0.28.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
@@ -1990,9 +1984,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2026,6 +2020,15 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -2166,11 +2169,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
|
||||
"integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
@@ -2552,19 +2555,6 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/playwright-core": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||
@@ -2588,9 +2578,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.1.tgz",
|
||||
"integrity": "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==",
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -2669,7 +2659,6 @@
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -2701,6 +2690,44 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.0.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -2744,9 +2771,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2847,9 +2874,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -2905,6 +2932,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
@@ -2969,6 +2997,15 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
package.json
23
package.json
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "microsoft-rewards-script",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"description": "Automatically do tasks for Microsoft Rewards but in TS!",
|
||||
"author": "Netsky",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"main": "dist/index.js",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"pre-build": "npm i && rimraf dist && npx patchright install chromium",
|
||||
@@ -18,8 +17,10 @@
|
||||
"create-docker": "docker build -t microsoft-rewards-script-docker .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"clear-sessions": "node ./scripts/clearSessions.js",
|
||||
"clear-diagnostics": "rimraf diagnostics"
|
||||
"clear-diagnostics": "rimraf diagnostics",
|
||||
"clear-sessions": "node ./scripts/main/clearSessions.js",
|
||||
"open-session": "node ./scripts/main/browserSession.js -email",
|
||||
"open-session:dev": "node ./scripts/main/browserSession.js -dev -email ItsNetsky@protonmail.com"
|
||||
},
|
||||
"keywords": [
|
||||
"Bing Rewards",
|
||||
@@ -33,6 +34,7 @@
|
||||
"devDependencies": {
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-modules-newline": "^0.0.6",
|
||||
@@ -45,8 +47,8 @@
|
||||
"axios-retry": "^4.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"fingerprint-generator": "^2.1.77",
|
||||
"fingerprint-injector": "^2.1.77",
|
||||
"fingerprint-generator": "^2.1.79",
|
||||
"fingerprint-injector": "^2.1.79",
|
||||
"ghost-cursor-playwright-port": "^1.4.3",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
@@ -54,6 +56,9 @@
|
||||
"otpauth": "^9.4.1",
|
||||
"p-queue": "^9.0.1",
|
||||
"patchright": "^1.57.0",
|
||||
"ts-node": "^10.9.2"
|
||||
"semver": "^7.7.3",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^4.3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const projectRoot = path.resolve(__dirname, '..')
|
||||
|
||||
const possibleConfigPaths = [
|
||||
path.join(projectRoot, 'config.json'),
|
||||
path.join(projectRoot, 'src', 'config.json'),
|
||||
path.join(projectRoot, 'dist', 'config.json')
|
||||
]
|
||||
|
||||
console.log('[DEBUG] Project root:', projectRoot)
|
||||
console.log('[DEBUG] Searching for config.json...')
|
||||
|
||||
let configPath = null
|
||||
for (const p of possibleConfigPaths) {
|
||||
console.log('[DEBUG] Checking:', p)
|
||||
if (fs.existsSync(p)) {
|
||||
configPath = p
|
||||
console.log('[DEBUG] Found config at:', p)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!configPath) {
|
||||
console.error('[ERROR] config.json not found in any expected location!')
|
||||
console.error('[ERROR] Searched:', possibleConfigPaths)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('[INFO] Using config:', configPath)
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
||||
|
||||
if (!config.sessionPath) {
|
||||
console.error("[ERROR] config.json missing 'sessionPath' key!")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('[INFO] Session path from config:', config.sessionPath)
|
||||
|
||||
const configDir = path.dirname(configPath)
|
||||
const possibleSessionDirs = [
|
||||
path.resolve(configDir, config.sessionPath),
|
||||
path.join(projectRoot, 'src/browser', config.sessionPath),
|
||||
path.join(projectRoot, 'dist/browser', config.sessionPath)
|
||||
]
|
||||
|
||||
console.log('[DEBUG] Searching for session directory...')
|
||||
|
||||
let sessionDir = null
|
||||
for (const p of possibleSessionDirs) {
|
||||
console.log('[DEBUG] Checking:', p)
|
||||
if (fs.existsSync(p)) {
|
||||
sessionDir = p
|
||||
console.log('[DEBUG] Found session directory at:', p)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionDir) {
|
||||
sessionDir = path.resolve(configDir, config.sessionPath)
|
||||
console.log('[DEBUG] Using fallback session directory:', sessionDir)
|
||||
}
|
||||
|
||||
const normalizedSessionDir = path.normalize(sessionDir)
|
||||
const normalizedProjectRoot = path.normalize(projectRoot)
|
||||
|
||||
if (!normalizedSessionDir.startsWith(normalizedProjectRoot)) {
|
||||
console.error('[ERROR] Session directory is outside project root!')
|
||||
console.error('[ERROR] Project root:', normalizedProjectRoot)
|
||||
console.error('[ERROR] Session directory:', normalizedSessionDir)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (normalizedSessionDir === normalizedProjectRoot) {
|
||||
console.error('[ERROR] Session directory cannot be the project root!')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const pathSegments = normalizedSessionDir.split(path.sep)
|
||||
if (pathSegments.length < 3) {
|
||||
console.error('[ERROR] Session path is too shallow (safety check failed)!')
|
||||
console.error('[ERROR] Path:', normalizedSessionDir)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (fs.existsSync(sessionDir)) {
|
||||
console.log('[INFO] Removing session folder:', sessionDir)
|
||||
try {
|
||||
fs.rmSync(sessionDir, { recursive: true, force: true })
|
||||
console.log('[SUCCESS] Session folder removed successfully')
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Failed to remove session folder:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
console.log('[INFO] Session folder does not exist:', sessionDir)
|
||||
}
|
||||
|
||||
console.log('[INFO] Done.')
|
||||
168
scripts/main/browserSession.js
Normal file
168
scripts/main/browserSession.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import fs from 'fs'
|
||||
import { chromium } from 'patchright'
|
||||
import { newInjectedContext } from 'fingerprint-injector'
|
||||
import {
|
||||
getDirname,
|
||||
getProjectRoot,
|
||||
log,
|
||||
parseArgs,
|
||||
validateEmail,
|
||||
loadConfig,
|
||||
loadAccounts,
|
||||
findAccountByEmail,
|
||||
getRuntimeBase,
|
||||
getSessionPath,
|
||||
loadCookies,
|
||||
loadFingerprint,
|
||||
buildProxyConfig,
|
||||
setupCleanupHandlers
|
||||
} from '../utils.js'
|
||||
|
||||
const __dirname = getDirname(import.meta.url)
|
||||
const projectRoot = getProjectRoot(__dirname)
|
||||
|
||||
const args = parseArgs()
|
||||
args.dev = args.dev || false
|
||||
|
||||
validateEmail(args.email)
|
||||
|
||||
const { data: config } = loadConfig(projectRoot, args.dev)
|
||||
const { data: accounts } = loadAccounts(projectRoot, args.dev)
|
||||
|
||||
const account = findAccountByEmail(accounts, args.email)
|
||||
if (!account) {
|
||||
log('ERROR', `Account not found: ${args.email}`)
|
||||
log('ERROR', 'Available accounts:')
|
||||
accounts.forEach(acc => {
|
||||
if (acc?.email) log('ERROR', ` - ${acc.email}`)
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const runtimeBase = getRuntimeBase(projectRoot, args.dev)
|
||||
const sessionBase = getSessionPath(runtimeBase, config.sessionPath, args.email)
|
||||
|
||||
log('INFO', 'Validating session data...')
|
||||
|
||||
if (!fs.existsSync(sessionBase)) {
|
||||
log('ERROR', `Session directory does not exist: ${sessionBase}`)
|
||||
log('ERROR', 'Please ensure the session has been created for this account')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!config.baseURL) {
|
||||
log('ERROR', 'baseURL is not set in config.json')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let cookies = await loadCookies(sessionBase, 'desktop')
|
||||
let sessionType = 'desktop'
|
||||
|
||||
if (cookies.length === 0) {
|
||||
log('WARN', 'No desktop session cookies found, trying mobile session...')
|
||||
cookies = await loadCookies(sessionBase, 'mobile')
|
||||
sessionType = 'mobile'
|
||||
|
||||
if (cookies.length === 0) {
|
||||
log('ERROR', 'No cookies found in desktop or mobile session')
|
||||
log('ERROR', `Session directory: ${sessionBase}`)
|
||||
log('ERROR', 'Please ensure a valid session exists for this account')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
log('INFO', `Using mobile session (${cookies.length} cookies)`)
|
||||
}
|
||||
|
||||
const isMobile = sessionType === 'mobile'
|
||||
const fingerprintEnabled = isMobile ? account.saveFingerprint?.mobile : account.saveFingerprint?.desktop
|
||||
|
||||
let fingerprint = null
|
||||
if (fingerprintEnabled) {
|
||||
fingerprint = await loadFingerprint(sessionBase, sessionType)
|
||||
if (!fingerprint) {
|
||||
log('ERROR', `Fingerprint is enabled for ${sessionType} but fingerprint file not found`)
|
||||
log('ERROR', `Expected file: ${sessionBase}/session_fingerprint_${sessionType}.json`)
|
||||
log('ERROR', 'Cannot start browser without fingerprint when it is explicitly enabled')
|
||||
process.exit(1)
|
||||
}
|
||||
log('INFO', `Loaded ${sessionType} fingerprint`)
|
||||
}
|
||||
|
||||
const proxy = buildProxyConfig(account)
|
||||
|
||||
if (account.proxy && account.proxy.url && (!proxy || !proxy.server)) {
|
||||
log('ERROR', 'Proxy is configured in account but proxy data is invalid or incomplete')
|
||||
log('ERROR', 'Account proxy config:', JSON.stringify(account.proxy, null, 2))
|
||||
log('ERROR', 'Required fields: proxy.url, proxy.port')
|
||||
log('ERROR', 'Cannot start browser without proxy when it is explicitly configured')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const userAgent = fingerprint?.fingerprint?.navigator?.userAgent || fingerprint?.fingerprint?.userAgent || null
|
||||
|
||||
log('INFO', `Session: ${args.email} (${sessionType})`)
|
||||
log('INFO', ` Cookies: ${cookies.length}`)
|
||||
log('INFO', ` Fingerprint: ${fingerprint ? 'Yes' : 'No'}`)
|
||||
log('INFO', ` User-Agent: ${userAgent || 'Default'}`)
|
||||
log('INFO', ` Proxy: ${proxy ? 'Yes' : 'No'}`)
|
||||
log('INFO', 'Launching browser...')
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: false,
|
||||
...(proxy ? { proxy } : {}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--mute-audio',
|
||||
'--disable-setuid-sandbox',
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-certificate-errors-spki-list',
|
||||
'--ignore-ssl-errors',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-user-media-security=true',
|
||||
'--disable-blink-features=Attestation',
|
||||
'--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys',
|
||||
'--disable-save-password-bubble'
|
||||
]
|
||||
})
|
||||
|
||||
let context
|
||||
if (fingerprint) {
|
||||
context = await newInjectedContext(browser, { fingerprint })
|
||||
|
||||
await context.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'credentials', {
|
||||
value: {
|
||||
create: () => Promise.reject(new Error('WebAuthn disabled')),
|
||||
get: () => Promise.reject(new Error('WebAuthn disabled'))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
log('SUCCESS', 'Fingerprint injected into browser context')
|
||||
} else {
|
||||
context = await browser.newContext({
|
||||
viewport: isMobile ? { width: 375, height: 667 } : { width: 1366, height: 768 }
|
||||
})
|
||||
}
|
||||
|
||||
if (cookies.length) {
|
||||
await context.addCookies(cookies)
|
||||
log('INFO', `Added ${cookies.length} cookies to context`)
|
||||
}
|
||||
|
||||
const page = await context.newPage()
|
||||
await page.goto(config.baseURL, { waitUntil: 'domcontentloaded' })
|
||||
|
||||
log('SUCCESS', 'Browser opened with session loaded')
|
||||
log('INFO', `Navigated to: ${config.baseURL}`)
|
||||
|
||||
setupCleanupHandlers(async () => {
|
||||
if (browser?.isConnected?.()) {
|
||||
await browser.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
67
scripts/main/clearSessions.js
Normal file
67
scripts/main/clearSessions.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import {
|
||||
getDirname,
|
||||
getProjectRoot,
|
||||
log,
|
||||
loadJsonFile,
|
||||
safeRemoveDirectory
|
||||
} from '../utils.js'
|
||||
|
||||
const __dirname = getDirname(import.meta.url)
|
||||
const projectRoot = getProjectRoot(__dirname)
|
||||
|
||||
const possibleConfigPaths = [
|
||||
path.join(projectRoot, 'config.json'),
|
||||
path.join(projectRoot, 'src', 'config.json'),
|
||||
path.join(projectRoot, 'dist', 'config.json')
|
||||
]
|
||||
|
||||
log('DEBUG', 'Project root:', projectRoot)
|
||||
log('DEBUG', 'Searching for config.json...')
|
||||
|
||||
const configResult = loadJsonFile(possibleConfigPaths, true)
|
||||
const config = configResult.data
|
||||
const configPath = configResult.path
|
||||
|
||||
log('INFO', 'Using config:', configPath)
|
||||
|
||||
if (!config.sessionPath) {
|
||||
log('ERROR', 'Invalid config.json - missing required field: sessionPath')
|
||||
log('ERROR', `Config file: ${configPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
log('INFO', 'Session path from config:', config.sessionPath)
|
||||
|
||||
const configDir = path.dirname(configPath)
|
||||
const possibleSessionDirs = [
|
||||
path.resolve(configDir, config.sessionPath),
|
||||
path.join(projectRoot, 'src/browser', config.sessionPath),
|
||||
path.join(projectRoot, 'dist/browser', config.sessionPath)
|
||||
]
|
||||
|
||||
log('DEBUG', 'Searching for session directory...')
|
||||
|
||||
let sessionDir = null
|
||||
for (const p of possibleSessionDirs) {
|
||||
log('DEBUG', 'Checking:', p)
|
||||
if (fs.existsSync(p)) {
|
||||
sessionDir = p
|
||||
log('DEBUG', 'Found session directory at:', p)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionDir) {
|
||||
sessionDir = path.resolve(configDir, config.sessionPath)
|
||||
log('DEBUG', 'Using fallback session directory:', sessionDir)
|
||||
}
|
||||
|
||||
const success = safeRemoveDirectory(sessionDir, projectRoot)
|
||||
|
||||
if (!success) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
log('INFO', 'Done.')
|
||||
269
scripts/utils.js
Normal file
269
scripts/utils.js
Normal file
@@ -0,0 +1,269 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
export function getDirname(importMetaUrl) {
|
||||
const __filename = fileURLToPath(importMetaUrl)
|
||||
return path.dirname(__filename)
|
||||
}
|
||||
|
||||
export function getProjectRoot(currentDir) {
|
||||
let dir = currentDir
|
||||
while (dir !== path.parse(dir).root) {
|
||||
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
||||
return dir
|
||||
}
|
||||
dir = path.dirname(dir)
|
||||
}
|
||||
throw new Error('Could not find project root (package.json not found)')
|
||||
}
|
||||
|
||||
export function log(level, ...args) {
|
||||
console.log(`[${level}]`, ...args)
|
||||
}
|
||||
|
||||
export function parseArgs(argv = process.argv.slice(2)) {
|
||||
const args = {}
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i]
|
||||
|
||||
if (arg.startsWith('-')) {
|
||||
const key = arg.substring(1)
|
||||
|
||||
if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
|
||||
args[key] = argv[i + 1]
|
||||
i++
|
||||
} else {
|
||||
args[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
export function validateEmail(email) {
|
||||
if (!email) {
|
||||
log('ERROR', 'Missing -email argument')
|
||||
log('ERROR', 'Usage: node script.js -email you@example.com')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (typeof email !== 'string') {
|
||||
log('ERROR', `Invalid email type: expected string, got ${typeof email}`)
|
||||
log('ERROR', 'Usage: node script.js -email you@example.com')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!email.includes('@')) {
|
||||
log('ERROR', `Invalid email format: "${email}"`)
|
||||
log('ERROR', 'Email must contain "@" symbol')
|
||||
log('ERROR', 'Example: you@example.com')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return email
|
||||
}
|
||||
|
||||
export function loadJsonFile(possiblePaths, required = true) {
|
||||
for (const filePath of possiblePaths) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8')
|
||||
return { data: JSON.parse(content), path: filePath }
|
||||
} catch (error) {
|
||||
log('ERROR', `Failed to parse JSON file: ${filePath}`)
|
||||
log('ERROR', `Parse error: ${error.message}`)
|
||||
if (required) process.exit(1)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (required) {
|
||||
log('ERROR', 'Required file not found')
|
||||
log('ERROR', 'Searched in the following locations:')
|
||||
possiblePaths.forEach(p => log('ERROR', ` - ${p}`))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function loadConfig(projectRoot, isDev = false) {
|
||||
const possiblePaths = isDev
|
||||
? [path.join(projectRoot, 'src', 'config.json')]
|
||||
: [
|
||||
path.join(projectRoot, 'dist', 'config.json'),
|
||||
path.join(projectRoot, 'config.json')
|
||||
]
|
||||
|
||||
const result = loadJsonFile(possiblePaths, true)
|
||||
|
||||
const missingFields = []
|
||||
if (!result.data.baseURL) missingFields.push('baseURL')
|
||||
if (!result.data.sessionPath) missingFields.push('sessionPath')
|
||||
if (result.data.headless === undefined) missingFields.push('headless')
|
||||
if (!result.data.workers) missingFields.push('workers')
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
log('ERROR', 'Invalid config.json - missing required fields:')
|
||||
missingFields.forEach(field => log('ERROR', ` - ${field}`))
|
||||
log('ERROR', `Config file: ${result.path}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function loadAccounts(projectRoot, isDev = false) {
|
||||
const possiblePaths = isDev
|
||||
? [path.join(projectRoot, 'src', 'accounts.dev.json')]
|
||||
: [
|
||||
path.join(projectRoot, 'dist', 'accounts.json'),
|
||||
path.join(projectRoot, 'accounts.json'),
|
||||
path.join(projectRoot, 'accounts.example.json')
|
||||
]
|
||||
|
||||
return loadJsonFile(possiblePaths, true)
|
||||
}
|
||||
|
||||
export function findAccountByEmail(accounts, email) {
|
||||
if (!email || typeof email !== 'string') return null
|
||||
return accounts.find(a => a?.email && typeof a.email === 'string' && a.email.toLowerCase() === email.toLowerCase()) || null
|
||||
}
|
||||
|
||||
export function getRuntimeBase(projectRoot, isDev = false) {
|
||||
return path.join(projectRoot, isDev ? 'src' : 'dist')
|
||||
}
|
||||
|
||||
export function getSessionPath(runtimeBase, sessionPath, email) {
|
||||
return path.join(runtimeBase, 'browser', sessionPath, email)
|
||||
}
|
||||
|
||||
export async function loadCookies(sessionBase, type = 'desktop') {
|
||||
const cookiesFile = path.join(sessionBase, `session_${type}.json`)
|
||||
|
||||
if (!fs.existsSync(cookiesFile)) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.promises.readFile(cookiesFile, 'utf8')
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
log('WARN', `Failed to load cookies from: ${cookiesFile}`)
|
||||
log('WARN', `Error: ${error.message}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadFingerprint(sessionBase, type = 'desktop') {
|
||||
const fpFile = path.join(sessionBase, `session_fingerprint_${type}.json`)
|
||||
|
||||
if (!fs.existsSync(fpFile)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.promises.readFile(fpFile, 'utf8')
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
log('WARN', `Failed to load fingerprint from: ${fpFile}`)
|
||||
log('WARN', `Error: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserAgent(fingerprint) {
|
||||
if (!fingerprint) return null
|
||||
return fingerprint?.fingerprint?.userAgent || fingerprint?.userAgent || null
|
||||
}
|
||||
|
||||
export function buildProxyConfig(account) {
|
||||
if (!account.proxy || !account.proxy.url || !account.proxy.port) {
|
||||
return null
|
||||
}
|
||||
|
||||
const proxy = {
|
||||
server: `${account.proxy.url}:${account.proxy.port}`
|
||||
}
|
||||
|
||||
if (account.proxy.username && account.proxy.password) {
|
||||
proxy.username = account.proxy.username
|
||||
proxy.password = account.proxy.password
|
||||
}
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
export function setupCleanupHandlers(cleanupFn) {
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
await cleanupFn()
|
||||
} catch (error) {
|
||||
log('ERROR', 'Cleanup failed:', error.message)
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.on('SIGINT', cleanup)
|
||||
process.on('SIGTERM', cleanup)
|
||||
}
|
||||
|
||||
export function validateDeletionPath(targetPath, projectRoot) {
|
||||
const normalizedTarget = path.normalize(targetPath)
|
||||
const normalizedRoot = path.normalize(projectRoot)
|
||||
|
||||
if (!normalizedTarget.startsWith(normalizedRoot)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Path is outside project root'
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedTarget === normalizedRoot) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Cannot delete project root'
|
||||
}
|
||||
}
|
||||
|
||||
const pathSegments = normalizedTarget.split(path.sep)
|
||||
if (pathSegments.length < 3) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Path is too shallow (safety check failed)'
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, error: null }
|
||||
}
|
||||
|
||||
export function safeRemoveDirectory(dirPath, projectRoot) {
|
||||
const validation = validateDeletionPath(dirPath, projectRoot)
|
||||
|
||||
if (!validation.valid) {
|
||||
log('ERROR', 'Directory deletion failed - safety check:')
|
||||
log('ERROR', ` Reason: ${validation.error}`)
|
||||
log('ERROR', ` Target: ${dirPath}`)
|
||||
log('ERROR', ` Project root: ${projectRoot}`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
log('INFO', `Directory does not exist: ${dirPath}`)
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true })
|
||||
log('SUCCESS', `Directory removed: ${dirPath}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
log('ERROR', `Failed to remove directory: ${dirPath}`)
|
||||
log('ERROR', `Error: ${error.message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,39 @@
|
||||
{
|
||||
"email": "email_1",
|
||||
"password": "password_1",
|
||||
"totp": "",
|
||||
"totpSecret": "",
|
||||
"recoveryEmail": "",
|
||||
"geoLocale": "auto",
|
||||
"langCode": "en",
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"proxyAxios": false,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
"saveFingerprint": {
|
||||
"mobile": false,
|
||||
"desktop": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"email": "email_2",
|
||||
"password": "password_2",
|
||||
"totp": "",
|
||||
"totpSecret": "",
|
||||
"recoveryEmail": "",
|
||||
"geoLocale": "auto",
|
||||
"langCode": "en",
|
||||
"proxy": {
|
||||
"proxyAxios": true,
|
||||
"proxyAxios": false,
|
||||
"url": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
"saveFingerprint": {
|
||||
"mobile": false,
|
||||
"desktop": false
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -1,5 +1,4 @@
|
||||
import rebrowser, { BrowserContext } from 'patchright'
|
||||
|
||||
import { newInjectedContext } from 'fingerprint-injector'
|
||||
import { BrowserFingerprintWithHeaders, FingerprintGenerator } from 'fingerprint-generator'
|
||||
|
||||
@@ -7,7 +6,7 @@ import type { MicrosoftRewardsBot } from '../index'
|
||||
import { loadSessionData, saveFingerprintData } from '../util/Load'
|
||||
import { UserAgentManager } from './UserAgent'
|
||||
|
||||
import type { AccountProxy } from '../interface/Account'
|
||||
import type { Account, AccountProxy } from '../interface/Account'
|
||||
|
||||
/* Test Stuff
|
||||
https://abrahamjuliot.github.io/creepjs/
|
||||
@@ -17,92 +16,110 @@ https://pixelscan.net/
|
||||
https://www.browserscan.net/
|
||||
*/
|
||||
|
||||
interface BrowserCreationResult {
|
||||
context: BrowserContext
|
||||
fingerprint: BrowserFingerprintWithHeaders
|
||||
}
|
||||
|
||||
class Browser {
|
||||
private bot: MicrosoftRewardsBot
|
||||
private readonly bot: MicrosoftRewardsBot
|
||||
private static readonly BROWSER_ARGS = [
|
||||
'--no-sandbox',
|
||||
'--mute-audio',
|
||||
'--disable-setuid-sandbox',
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-certificate-errors-spki-list',
|
||||
'--ignore-ssl-errors',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-user-media-security=true',
|
||||
'--disable-blink-features=Attestation',
|
||||
'--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys',
|
||||
'--disable-save-password-bubble'
|
||||
] as const
|
||||
|
||||
constructor(bot: MicrosoftRewardsBot) {
|
||||
this.bot = bot
|
||||
}
|
||||
|
||||
async createBrowser(
|
||||
proxy: AccountProxy,
|
||||
email: string
|
||||
): Promise<{
|
||||
context: BrowserContext
|
||||
fingerprint: BrowserFingerprintWithHeaders
|
||||
}> {
|
||||
async createBrowser(account: Account): Promise<BrowserCreationResult> {
|
||||
let browser: rebrowser.Browser
|
||||
try {
|
||||
const proxyConfig = account.proxy.url
|
||||
? {
|
||||
server: this.formatProxyServer(account.proxy),
|
||||
...(account.proxy.username &&
|
||||
account.proxy.password && {
|
||||
username: account.proxy.username,
|
||||
password: account.proxy.password
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
|
||||
browser = await rebrowser.chromium.launch({
|
||||
headless: this.bot.config.headless,
|
||||
...(proxy.url && {
|
||||
proxy: { username: proxy.username, password: proxy.password, server: `${proxy.url}:${proxy.port}` }
|
||||
}),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--mute-audio',
|
||||
'--disable-setuid-sandbox',
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-certificate-errors-spki-list',
|
||||
'--ignore-ssl-errors',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-user-media-security=true',
|
||||
'--disable-blink-features=Attestation',
|
||||
'--disable-features=WebAuthentication,PasswordManagerOnboarding,PasswordManager,EnablePasswordsAccountStorage,Passkeys',
|
||||
'--disable-save-password-bubble'
|
||||
]
|
||||
...(proxyConfig && { proxy: proxyConfig }),
|
||||
args: [...Browser.BROWSER_ARGS]
|
||||
})
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'BROWSER',
|
||||
`Launch failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
this.bot.logger.error(this.bot.isMobile, 'BROWSER', `Launch failed: ${errorMessage}`)
|
||||
throw error
|
||||
}
|
||||
|
||||
const sessionData = await loadSessionData(
|
||||
this.bot.config.sessionPath,
|
||||
email,
|
||||
this.bot.config.saveFingerprint,
|
||||
this.bot.isMobile
|
||||
)
|
||||
try {
|
||||
const sessionData = await loadSessionData(
|
||||
this.bot.config.sessionPath,
|
||||
account.email,
|
||||
account.saveFingerprint,
|
||||
this.bot.isMobile
|
||||
)
|
||||
|
||||
const fingerprint = sessionData.fingerprint
|
||||
? sessionData.fingerprint
|
||||
: await this.generateFingerprint(this.bot.isMobile)
|
||||
const fingerprint = sessionData.fingerprint ?? (await this.generateFingerprint(this.bot.isMobile))
|
||||
|
||||
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
|
||||
const context = await newInjectedContext(browser as any, { fingerprint })
|
||||
|
||||
await context.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'credentials', {
|
||||
value: {
|
||||
create: () => Promise.reject(new Error('WebAuthn disabled')),
|
||||
get: () => Promise.reject(new Error('WebAuthn disabled'))
|
||||
}
|
||||
await context.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'credentials', {
|
||||
value: {
|
||||
create: () => Promise.reject(new Error('WebAuthn disabled')),
|
||||
get: () => Promise.reject(new Error('WebAuthn disabled'))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context.setDefaultTimeout(this.bot.utils.stringToNumber(this.bot.config?.globalTimeout ?? 30000))
|
||||
context.setDefaultTimeout(this.bot.utils.stringToNumber(this.bot.config?.globalTimeout ?? 30000))
|
||||
|
||||
await context.addCookies(sessionData.cookies)
|
||||
await context.addCookies(sessionData.cookies)
|
||||
|
||||
if (this.bot.config.saveFingerprint) {
|
||||
await saveFingerprintData(this.bot.config.sessionPath, email, this.bot.isMobile, fingerprint)
|
||||
if (
|
||||
(account.saveFingerprint.mobile && this.bot.isMobile) ||
|
||||
(account.saveFingerprint.desktop && !this.bot.isMobile)
|
||||
) {
|
||||
await saveFingerprintData(this.bot.config.sessionPath, account.email, this.bot.isMobile, fingerprint)
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'BROWSER',
|
||||
`Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`
|
||||
)
|
||||
this.bot.logger.debug(this.bot.isMobile, 'BROWSER-FINGERPRINT', JSON.stringify(fingerprint))
|
||||
|
||||
return { context: context as unknown as BrowserContext, fingerprint }
|
||||
} catch (error) {
|
||||
await browser.close().catch(() => {})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'BROWSER',
|
||||
`Created browser with User-Agent: "${fingerprint.fingerprint.navigator.userAgent}"`
|
||||
)
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'BROWSER-FINGERPRINT', JSON.stringify(fingerprint))
|
||||
|
||||
return {
|
||||
context: context as unknown as BrowserContext,
|
||||
fingerprint: fingerprint
|
||||
private formatProxyServer(proxy: AccountProxy): string {
|
||||
try {
|
||||
const urlObj = new URL(proxy.url)
|
||||
const protocol = urlObj.protocol.replace(':', '')
|
||||
return `${protocol}://${urlObj.hostname}:${proxy.port}`
|
||||
} catch {
|
||||
return `${proxy.url}:${proxy.port}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export default class BrowserFunc {
|
||||
.join('; ')
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
url: `https://rewards.bing.com/api/getuserinfo?type=1&X-Requested-With=XMLHttpRequest&_=${Date.now()}`,
|
||||
url: 'https://rewards.bing.com/api/getuserinfo?type=1',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...(this.bot.fingerprint?.headers ?? {}),
|
||||
|
||||
@@ -2,25 +2,31 @@ import type { Page } from 'patchright'
|
||||
import type { MicrosoftRewardsBot } from '../../index'
|
||||
import { saveSessionData } from '../../util/Load'
|
||||
|
||||
// Methods
|
||||
import { MobileAccessLogin } from './methods/MobileAccessLogin'
|
||||
import { EmailLogin } from './methods/EmailLogin'
|
||||
import { PasswordlessLogin } from './methods/PasswordlessLogin'
|
||||
import { TotpLogin } from './methods/Totp2FALogin'
|
||||
import { CodeLogin } from './methods/GetACodeLogin'
|
||||
import { RecoveryLogin } from './methods/RecoveryEmailLogin'
|
||||
|
||||
import type { Account } from '../../interface/Account'
|
||||
|
||||
type LoginState =
|
||||
| 'EMAIL_INPUT'
|
||||
| 'PASSWORD_INPUT'
|
||||
| 'SIGN_IN_ANOTHER_WAY'
|
||||
| 'SIGN_IN_ANOTHER_WAY_EMAIL'
|
||||
| 'PASSKEY_ERROR'
|
||||
| 'PASSKEY_VIDEO'
|
||||
| 'KMSI_PROMPT'
|
||||
| 'LOGGED_IN'
|
||||
| 'RECOVERY_EMAIL_INPUT'
|
||||
| 'ACCOUNT_LOCKED'
|
||||
| 'ERROR_ALERT'
|
||||
| '2FA_TOTP'
|
||||
| 'LOGIN_PASSWORDLESS'
|
||||
| 'GET_A_CODE'
|
||||
| 'GET_A_CODE_2'
|
||||
| 'UNKNOWN'
|
||||
| 'CHROMEWEBDATA_ERROR'
|
||||
|
||||
@@ -28,28 +34,52 @@ export class Login {
|
||||
emailLogin: EmailLogin
|
||||
passwordlessLogin: PasswordlessLogin
|
||||
totp2FALogin: TotpLogin
|
||||
codeLogin: CodeLogin
|
||||
recoveryLogin: RecoveryLogin
|
||||
|
||||
private readonly selectors = {
|
||||
primaryButton: 'button[data-testid="primaryButton"]',
|
||||
secondaryButton: 'button[data-testid="secondaryButton"]',
|
||||
emailIcon: '[data-testid="tile"]:has(svg path[d*="M5.25 4h13.5a3.25"])',
|
||||
emailIconOld: 'img[data-testid="accessibleImg"][src*="picker_verify_email"]',
|
||||
recoveryEmail: '[data-testid="proof-confirmation"]',
|
||||
passwordIcon: '[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])',
|
||||
accountLocked: '#serviceAbuseLandingTitle',
|
||||
errorAlert: 'div[role="alert"]',
|
||||
passwordEntry: '[data-testid="passwordEntry"]',
|
||||
emailEntry: 'input#usernameEntry',
|
||||
kmsiVideo: '[data-testid="kmsiVideo"]',
|
||||
passKeyVideo: '[data-testid="biometricVideo"]',
|
||||
passKeyError: '[data-testid="registrationImg"]',
|
||||
passwordlessCheck: '[data-testid="deviceShieldCheckmarkVideo"]',
|
||||
totpInput: 'input[name="otc"]',
|
||||
totpInputOld: 'form[name="OneTimeCodeViewForm"]',
|
||||
identityBanner: '[data-testid="identityBanner"]',
|
||||
viewFooter: '[data-testid="viewFooter"] span[role="button"]',
|
||||
bingProfile: '#id_n',
|
||||
requestToken: 'input[name="__RequestVerificationToken"]',
|
||||
requestTokenMeta: 'meta[name="__RequestVerificationToken"]'
|
||||
} as const
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {
|
||||
this.emailLogin = new EmailLogin(this.bot)
|
||||
this.passwordlessLogin = new PasswordlessLogin(this.bot)
|
||||
this.totp2FALogin = new TotpLogin(this.bot)
|
||||
this.codeLogin = new CodeLogin(this.bot)
|
||||
this.recoveryLogin = new RecoveryLogin(this.bot)
|
||||
}
|
||||
|
||||
private readonly primaryButtonSelector = 'button[data-testid="primaryButton"]'
|
||||
private readonly secondaryButtonSelector = 'button[data-testid="secondaryButton"]'
|
||||
|
||||
async login(page: Page, email: string, password: string, totpSecret?: string) {
|
||||
async login(page: Page, account: Account) {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting login process')
|
||||
|
||||
await page.goto('https://www.bing.com/rewards/dashboard', { waitUntil: 'domcontentloaded' }).catch(() => {})
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.bot.browser.utils.reloadBadPage(page)
|
||||
|
||||
await this.bot.browser.utils.disableFido(page)
|
||||
|
||||
const maxIterations = 25
|
||||
let iteration = 0
|
||||
|
||||
let previousState: LoginState = 'UNKNOWN'
|
||||
let sameStateCount = 0
|
||||
|
||||
@@ -59,7 +89,7 @@ export class Login {
|
||||
iteration++
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `State check iteration ${iteration}/${maxIterations}`)
|
||||
|
||||
const state = await this.detectCurrentState(page)
|
||||
const state = await this.detectCurrentState(page, account)
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `Current state: ${state}`)
|
||||
|
||||
if (state !== previousState && previousState !== 'UNKNOWN') {
|
||||
@@ -68,11 +98,16 @@ export class Login {
|
||||
|
||||
if (state === previousState && state !== 'LOGGED_IN' && state !== 'UNKNOWN') {
|
||||
sameStateCount++
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
`Same state count: ${sameStateCount}/4 for state "${state}"`
|
||||
)
|
||||
if (sameStateCount >= 4) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
`Stuck in state "${state}" for 4 loops. Refreshing page...`
|
||||
`Stuck in state "${state}" for 4 loops, refreshing page`
|
||||
)
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await this.bot.utils.wait(3000)
|
||||
@@ -90,8 +125,7 @@ export class Login {
|
||||
break
|
||||
}
|
||||
|
||||
const shouldContinue = await this.handleState(state, page, email, password, totpSecret)
|
||||
|
||||
const shouldContinue = await this.handleState(state, page, account)
|
||||
if (!shouldContinue) {
|
||||
throw new Error(`Login failed or aborted at state: ${state}`)
|
||||
}
|
||||
@@ -103,142 +137,151 @@ export class Login {
|
||||
throw new Error('Login timeout: exceeded maximum iterations')
|
||||
}
|
||||
|
||||
await this.finalizeLogin(page, email)
|
||||
await this.finalizeLogin(page, account.email)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
`Fatal error: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async detectCurrentState(page: Page): Promise<LoginState> {
|
||||
// Make sure we settled before getting a URL
|
||||
private async detectCurrentState(page: Page, account?: Account): Promise<LoginState> {
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
const url = new URL(page.url())
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-CURRENT-STATE', `Current URL: ${url}`)
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Current URL: ${url.hostname}${url.pathname}`)
|
||||
|
||||
if (url.hostname === 'chromewebdata') {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'DETECT-CURRENT-STATE', 'Detected chromewebdata error page')
|
||||
this.bot.logger.warn(this.bot.isMobile, 'DETECT-STATE', 'Detected chromewebdata error page')
|
||||
return 'CHROMEWEBDATA_ERROR'
|
||||
}
|
||||
|
||||
const isLocked = await page
|
||||
.waitForSelector('#serviceAbuseLandingTitle', { state: 'visible', timeout: 200 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
const isLocked = await this.checkSelector(page, this.selectors.accountLocked)
|
||||
if (isLocked) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'Account locked selector found')
|
||||
return 'ACCOUNT_LOCKED'
|
||||
}
|
||||
|
||||
// If instantly loading rewards dash, logged in
|
||||
if (url.hostname === 'rewards.bing.com') {
|
||||
if (url.hostname === 'rewards.bing.com' || url.hostname === 'account.microsoft.com') {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'On rewards/account page, assuming logged in')
|
||||
return 'LOGGED_IN'
|
||||
}
|
||||
|
||||
// If account dash, logged in
|
||||
if (url.hostname === 'account.microsoft.com') {
|
||||
return 'LOGGED_IN'
|
||||
const stateChecks: Array<[string, LoginState]> = [
|
||||
[this.selectors.errorAlert, 'ERROR_ALERT'],
|
||||
[this.selectors.passwordEntry, 'PASSWORD_INPUT'],
|
||||
[this.selectors.emailEntry, 'EMAIL_INPUT'],
|
||||
[this.selectors.recoveryEmail, 'RECOVERY_EMAIL_INPUT'],
|
||||
[this.selectors.kmsiVideo, 'KMSI_PROMPT'],
|
||||
[this.selectors.passKeyVideo, 'PASSKEY_VIDEO'],
|
||||
[this.selectors.passKeyError, 'PASSKEY_ERROR'],
|
||||
[this.selectors.passwordIcon, 'SIGN_IN_ANOTHER_WAY'],
|
||||
[this.selectors.emailIcon, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
|
||||
[this.selectors.emailIconOld, 'SIGN_IN_ANOTHER_WAY_EMAIL'],
|
||||
[this.selectors.passwordlessCheck, 'LOGIN_PASSWORDLESS'],
|
||||
[this.selectors.totpInput, '2FA_TOTP'],
|
||||
[this.selectors.totpInputOld, '2FA_TOTP']
|
||||
]
|
||||
|
||||
const results = await Promise.all(
|
||||
stateChecks.map(async ([sel, state]) => {
|
||||
const visible = await this.checkSelector(page, sel)
|
||||
return visible ? state : null
|
||||
})
|
||||
)
|
||||
|
||||
const visibleStates = results.filter((s): s is LoginState => s !== null)
|
||||
if (visibleStates.length > 0) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Visible states: [${visibleStates.join(', ')}]`)
|
||||
}
|
||||
|
||||
const check = async (selector: string, state: LoginState): Promise<LoginState | null> => {
|
||||
return page
|
||||
.waitForSelector(selector, { state: 'visible', timeout: 200 })
|
||||
.then(visible => (visible ? state : null))
|
||||
.catch(() => null)
|
||||
}
|
||||
|
||||
const results = await Promise.all([
|
||||
check('div[role="alert"]', 'ERROR_ALERT'),
|
||||
check('[data-testid="passwordEntry"]', 'PASSWORD_INPUT'),
|
||||
check('input#usernameEntry', 'EMAIL_INPUT'),
|
||||
check('[data-testid="kmsiVideo"]', 'KMSI_PROMPT'),
|
||||
check('[data-testid="biometricVideo"]', 'PASSKEY_VIDEO'),
|
||||
check('[data-testid="registrationImg"]', 'PASSKEY_ERROR'),
|
||||
check('[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])', 'SIGN_IN_ANOTHER_WAY'),
|
||||
check('[data-testid="deviceShieldCheckmarkVideo"]', 'LOGIN_PASSWORDLESS'),
|
||||
check('input[name="otc"]', '2FA_TOTP'),
|
||||
check('form[name="OneTimeCodeViewForm"]', '2FA_TOTP')
|
||||
const [identityBanner, primaryButton, passwordEntry] = await Promise.all([
|
||||
this.checkSelector(page, this.selectors.identityBanner),
|
||||
this.checkSelector(page, this.selectors.primaryButton),
|
||||
this.checkSelector(page, this.selectors.passwordEntry)
|
||||
])
|
||||
|
||||
// Get a code
|
||||
const identityBanner = await page
|
||||
.waitForSelector('[data-testid="identityBanner"]', { state: 'visible', timeout: 200 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
const primaryButton = await page
|
||||
.waitForSelector(this.primaryButtonSelector, { state: 'visible', timeout: 200 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
const passwordEntry = await page
|
||||
.waitForSelector('[data-testid="passwordEntry"]', { state: 'visible', timeout: 200 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (identityBanner && primaryButton && !passwordEntry && !results.includes('2FA_TOTP')) {
|
||||
results.push('GET_A_CODE') // Lower prio
|
||||
const codeState = account?.password ? 'GET_A_CODE' : 'GET_A_CODE_2'
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DETECT-STATE',
|
||||
`Get code state detected: ${codeState} (has password: ${!!account?.password})`
|
||||
)
|
||||
results.push(codeState)
|
||||
}
|
||||
|
||||
// Final
|
||||
let foundStates = results.filter((s): s is LoginState => s !== null)
|
||||
|
||||
if (foundStates.length === 0) return 'UNKNOWN'
|
||||
if (foundStates.length === 0) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', 'No matching states found')
|
||||
return 'UNKNOWN'
|
||||
}
|
||||
|
||||
if (foundStates.includes('ERROR_ALERT')) {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DETECT-STATE',
|
||||
`ERROR_ALERT found - hostname: ${url.hostname}, has 2FA: ${foundStates.includes('2FA_TOTP')}`
|
||||
)
|
||||
if (url.hostname !== 'login.live.com') {
|
||||
// Remove ERROR_ALERT if not on login.live.com
|
||||
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
|
||||
}
|
||||
if (foundStates.includes('2FA_TOTP')) {
|
||||
// Don't throw on TOTP if expired code is entered
|
||||
foundStates = foundStates.filter(s => s !== 'ERROR_ALERT')
|
||||
}
|
||||
|
||||
// On login.live.com, keep it
|
||||
return 'ERROR_ALERT'
|
||||
if (foundStates.includes('ERROR_ALERT')) return 'ERROR_ALERT'
|
||||
}
|
||||
|
||||
if (foundStates.includes('ERROR_ALERT')) return 'ERROR_ALERT'
|
||||
if (foundStates.includes('ACCOUNT_LOCKED')) return 'ACCOUNT_LOCKED'
|
||||
if (foundStates.includes('PASSKEY_VIDEO')) return 'PASSKEY_VIDEO'
|
||||
if (foundStates.includes('PASSKEY_ERROR')) return 'PASSKEY_ERROR'
|
||||
if (foundStates.includes('KMSI_PROMPT')) return 'KMSI_PROMPT'
|
||||
if (foundStates.includes('PASSWORD_INPUT')) return 'PASSWORD_INPUT'
|
||||
if (foundStates.includes('EMAIL_INPUT')) return 'EMAIL_INPUT'
|
||||
if (foundStates.includes('SIGN_IN_ANOTHER_WAY')) return 'SIGN_IN_ANOTHER_WAY'
|
||||
if (foundStates.includes('LOGIN_PASSWORDLESS')) return 'LOGIN_PASSWORDLESS'
|
||||
if (foundStates.includes('2FA_TOTP')) return '2FA_TOTP'
|
||||
const priorities: LoginState[] = [
|
||||
'ACCOUNT_LOCKED',
|
||||
'PASSKEY_VIDEO',
|
||||
'PASSKEY_ERROR',
|
||||
'KMSI_PROMPT',
|
||||
'PASSWORD_INPUT',
|
||||
'EMAIL_INPUT',
|
||||
'SIGN_IN_ANOTHER_WAY_EMAIL',
|
||||
'SIGN_IN_ANOTHER_WAY',
|
||||
'LOGIN_PASSWORDLESS',
|
||||
'2FA_TOTP'
|
||||
]
|
||||
|
||||
const mainState = foundStates[0] as LoginState
|
||||
for (const priority of priorities) {
|
||||
if (foundStates.includes(priority)) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Selected state by priority: ${priority}`)
|
||||
return priority
|
||||
}
|
||||
}
|
||||
|
||||
return mainState
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DETECT-STATE', `Returning first found state: ${foundStates[0]}`)
|
||||
return foundStates[0] as LoginState
|
||||
}
|
||||
|
||||
private async handleState(
|
||||
state: LoginState,
|
||||
page: Page,
|
||||
email: string,
|
||||
password: string,
|
||||
totpSecret?: string
|
||||
): Promise<boolean> {
|
||||
private async checkSelector(page: Page, selector: string): Promise<boolean> {
|
||||
return page
|
||||
.waitForSelector(selector, { state: 'visible', timeout: 200 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
private async handleState(state: LoginState, page: Page, account: Account): Promise<boolean> {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'HANDLE-STATE', `Processing state: ${state}`)
|
||||
|
||||
switch (state) {
|
||||
case 'ACCOUNT_LOCKED': {
|
||||
const msg = 'This account has been locked! Remove from config and restart!'
|
||||
this.bot.logger.error(this.bot.isMobile, 'CHECK-LOCKED', msg)
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN', msg)
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
case 'ERROR_ALERT': {
|
||||
const alertEl = page.locator('div[role="alert"]')
|
||||
const alertEl = page.locator(this.selectors.errorAlert)
|
||||
const errorMsg = await alertEl.innerText().catch(() => 'Unknown Error')
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN', `Account error: ${errorMsg}`)
|
||||
throw new Error(`Microsoft login error message: ${errorMsg}`)
|
||||
throw new Error(`Microsoft login error: ${errorMsg}`)
|
||||
}
|
||||
|
||||
case 'LOGGED_IN':
|
||||
@@ -246,96 +289,161 @@ export class Login {
|
||||
|
||||
case 'EMAIL_INPUT': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering email')
|
||||
await this.emailLogin.enterEmail(page, email)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
await this.emailLogin.enterEmail(page, account.email)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after email entry')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'PASSWORD_INPUT': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Entering password')
|
||||
await this.emailLogin.enterPassword(page, password)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
await this.emailLogin.enterPassword(page, account.password)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after password entry')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'GET_A_CODE': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code"')
|
||||
// Select sign in other way
|
||||
await this.bot.browser.utils.ghostClick(page, '[data-testid="viewFooter"] span[role="button"]')
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Attempting to bypass "Get code" via footer')
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.viewFooter)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after footer click')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Footer clicked, proceeding')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'GET_A_CODE_2': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling "Get a code" flow')
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.primaryButton)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after primary button click')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating code login handler')
|
||||
await this.codeLogin.handle(page)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Code login handler completed successfully')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'SIGN_IN_ANOTHER_WAY_EMAIL': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Selecting "Send a code to email"')
|
||||
|
||||
const emailSelector = await Promise.race([
|
||||
this.checkSelector(page, this.selectors.emailIcon).then(found =>
|
||||
found ? this.selectors.emailIcon : null
|
||||
),
|
||||
this.checkSelector(page, this.selectors.emailIconOld).then(found =>
|
||||
found ? this.selectors.emailIconOld : null
|
||||
)
|
||||
])
|
||||
|
||||
if (!emailSelector) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Email icon not found')
|
||||
return false
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
`Using ${emailSelector === this.selectors.emailIcon ? 'new' : 'old'} email icon selector`
|
||||
)
|
||||
await this.bot.browser.utils.ghostClick(page, emailSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after email icon click')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating code login handler')
|
||||
await this.codeLogin.handle(page)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Code login handler completed successfully')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'RECOVERY_EMAIL_INPUT': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery email input detected')
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout on recovery page')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Initiating recovery email handler')
|
||||
await this.recoveryLogin.handle(page, account?.recoveryEmail)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery email handler completed successfully')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'CHROMEWEBDATA_ERROR': {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
'chromewebdata error page detected, attempting to recover to Rewards home'
|
||||
)
|
||||
// Try go to Rewards dashboard
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'chromewebdata error detected, attempting recovery')
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', `Navigating to ${this.bot.config.baseURL}`)
|
||||
await page
|
||||
.goto(this.bot.config.baseURL, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 10000
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
await this.bot.utils.wait(3000)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Recovery navigation successful')
|
||||
return true
|
||||
} catch {
|
||||
// If even that fails, fall back to login.live.com
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
'Failed to navigate to baseURL from chromewebdata, retrying login.live.com'
|
||||
)
|
||||
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Fallback to login.live.com')
|
||||
await page
|
||||
.goto('https://login.live.com/', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 10000
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
await this.bot.utils.wait(3000)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Fallback navigation successful')
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
case '2FA_TOTP': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA required')
|
||||
await this.totp2FALogin.handle(page, totpSecret)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA authentication required')
|
||||
await this.totp2FALogin.handle(page, account.totpSecret)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'TOTP 2FA handler completed successfully')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'SIGN_IN_ANOTHER_WAY': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Selecting "Use my password"')
|
||||
const passwordOption = '[data-testid="tile"]:has(svg path[d*="M11.78 10.22a.75.75"])'
|
||||
await this.bot.browser.utils.ghostClick(page, passwordOption)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.passwordIcon)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after password icon click')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Password option selected')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'KMSI_PROMPT': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Accepting KMSI prompt')
|
||||
await this.bot.browser.utils.ghostClick(page, this.primaryButtonSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.primaryButton)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after KMSI acceptance')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'KMSI prompt accepted')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'PASSKEY_VIDEO':
|
||||
case 'PASSKEY_ERROR': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Skipping Passkey prompt')
|
||||
await this.bot.browser.utils.ghostClick(page, this.secondaryButtonSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.secondaryButton)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after Passkey skip')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Passkey prompt skipped')
|
||||
return true
|
||||
}
|
||||
|
||||
case 'LOGIN_PASSWORDLESS': {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Handling passwordless authentication')
|
||||
await this.passwordlessLogin.handle(page)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', 'Network idle timeout after passwordless auth')
|
||||
})
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Passwordless authentication completed successfully')
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -344,12 +452,13 @@ export class Login {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
`Unknown state at host:${url.hostname} path:${url.pathname}. Waiting...`
|
||||
`Unknown state at ${url.hostname}${url.pathname}, waiting`
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
default:
|
||||
this.bot.logger.debug(this.bot.isMobile, 'HANDLE-STATE', `Unhandled state: ${state}, continuing`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -363,21 +472,21 @@ export class Login {
|
||||
if (loginRewardsSuccess) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Logged into Microsoft Rewards successfully')
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN',
|
||||
'Could not verify Rewards Dashboard. Assuming login valid anyway.'
|
||||
)
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN', 'Could not verify Rewards Dashboard, assuming login valid')
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting Bing session verification')
|
||||
await this.verifyBingSession(page)
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Starting rewards session verification')
|
||||
await this.getRewardsSession(page)
|
||||
|
||||
const browser = page.context()
|
||||
const cookies = await browser.cookies()
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN', `Retrieved ${cookies.length} cookies`)
|
||||
await saveSessionData(this.bot.config.sessionPath, cookies, email, this.bot.isMobile)
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Login completed! Session saved!')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN', 'Login completed, session saved')
|
||||
}
|
||||
|
||||
async verifyBingSession(page: Page) {
|
||||
@@ -393,30 +502,34 @@ export class Login {
|
||||
for (let i = 0; i < loopMax; i++) {
|
||||
if (page.isClosed()) break
|
||||
|
||||
// Rare error state
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-BING', `Verification loop ${i + 1}/${loopMax}`)
|
||||
|
||||
const state = await this.detectCurrentState(page)
|
||||
if (state === 'PASSKEY_ERROR') {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-BING',
|
||||
'Verification landed on Passkey error state! Trying to dismiss.'
|
||||
)
|
||||
await this.bot.browser.utils.ghostClick(page, this.secondaryButtonSelector)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Dismissing Passkey error state')
|
||||
await this.bot.browser.utils.ghostClick(page, this.selectors.secondaryButton)
|
||||
}
|
||||
|
||||
const u = new URL(page.url())
|
||||
const atBingHome = u.hostname === 'www.bing.com' && u.pathname === '/'
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-BING',
|
||||
`At Bing home: ${atBingHome} (${u.hostname}${u.pathname})`
|
||||
)
|
||||
|
||||
if (atBingHome) {
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page).catch(() => {})
|
||||
|
||||
const signedIn = await page
|
||||
.waitForSelector('#id_n', { timeout: 3000 })
|
||||
.waitForSelector(this.selectors.bingProfile, { timeout: 3000 })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-BING', `Profile element found: ${signedIn}`)
|
||||
|
||||
if (signedIn || this.bot.isMobile) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Bing session established')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-BING', 'Bing session verified successfully')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -424,16 +537,12 @@ export class Login {
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-BING',
|
||||
'Could not confirm Bing session after retries; continuing'
|
||||
)
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-BING', 'Could not verify Bing session, continuing anyway')
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-BING',
|
||||
`Bing verification error: ${error instanceof Error ? error.message : String(error)}`
|
||||
`Verification error: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -441,7 +550,7 @@ export class Login {
|
||||
private async getRewardsSession(page: Page) {
|
||||
const loopMax = 5
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'GET-REQUEST-TOKEN', 'Fetching request token')
|
||||
this.bot.logger.info(this.bot.isMobile, 'GET-REWARD-SESSION', 'Fetching request token')
|
||||
|
||||
try {
|
||||
await page
|
||||
@@ -451,11 +560,7 @@ export class Login {
|
||||
for (let i = 0; i < loopMax; i++) {
|
||||
if (page.isClosed()) break
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'GET-REWARD-SESSION',
|
||||
`Loop ${i + 1}/${loopMax} | URL=${page.url()}`
|
||||
)
|
||||
this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', `Token fetch loop ${i + 1}/${loopMax}`)
|
||||
|
||||
const u = new URL(page.url())
|
||||
const atRewardHome = u.hostname === 'rewards.bing.com' && u.pathname === '/'
|
||||
@@ -467,23 +572,27 @@ export class Login {
|
||||
const $ = await this.bot.browser.utils.loadInCheerio(html)
|
||||
|
||||
const token =
|
||||
$('input[name="__RequestVerificationToken"]').attr('value') ??
|
||||
$('meta[name="__RequestVerificationToken"]').attr('content') ??
|
||||
$(this.selectors.requestToken).attr('value') ??
|
||||
$(this.selectors.requestTokenMeta).attr('content') ??
|
||||
null
|
||||
|
||||
if (token) {
|
||||
this.bot.requestToken = token
|
||||
this.bot.logger.info(this.bot.isMobile, 'GET-REQUEST-TOKEN', 'Request token has been set!')
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'GET-REWARD-SESSION',
|
||||
`Token extracted: ${token.substring(0, 10)}...`
|
||||
`Request token retrieved: ${token.substring(0, 10)}...`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', 'Token NOT found on page')
|
||||
this.bot.logger.debug(this.bot.isMobile, 'GET-REWARD-SESSION', 'Token not found on page')
|
||||
} else {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'GET-REWARD-SESSION',
|
||||
`Not at reward home: ${u.hostname}${u.pathname}`
|
||||
)
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
@@ -491,19 +600,20 @@ export class Login {
|
||||
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'GET-REQUEST-TOKEN',
|
||||
'No RequestVerificationToken found — some activities may not work'
|
||||
'GET-REWARD-SESSION',
|
||||
'No RequestVerificationToken found, some activities may not work'
|
||||
)
|
||||
} catch (error) {
|
||||
throw this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'GET-REQUEST-TOKEN',
|
||||
`Reward session error: ${error instanceof Error ? error.message : String(error)}`
|
||||
'GET-REWARD-SESSION',
|
||||
`Fatal error: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async getAppAccessToken(page: Page, email: string) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'GET-APP-TOKEN', 'Requesting mobile access token')
|
||||
return await new MobileAccessLogin(this.bot, page).get(email)
|
||||
}
|
||||
}
|
||||
|
||||
129
src/browser/auth/methods/GetACodeLogin.ts
Normal file
129
src/browser/auth/methods/GetACodeLogin.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { Page } from 'patchright'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
import { getErrorMessage, getSubtitleMessage, promptInput } from './LoginUtils'
|
||||
|
||||
export class CodeLogin {
|
||||
private readonly textInputSelector = '[data-testid="codeInputWrapper"]'
|
||||
private readonly secondairyInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
||||
private readonly maxManualSeconds = 60
|
||||
private readonly maxManualAttempts = 5
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
|
||||
private async fillCode(page: Page, code: string): Promise<boolean> {
|
||||
try {
|
||||
const visibleInput = await page
|
||||
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
|
||||
.catch(() => null)
|
||||
|
||||
if (visibleInput) {
|
||||
await page.keyboard.type(code, { delay: 50 })
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Filled code input')
|
||||
return true
|
||||
}
|
||||
|
||||
const secondairyInput = await page.$(this.secondairyInputSelector)
|
||||
if (secondairyInput) {
|
||||
await page.keyboard.type(code, { delay: 50 })
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Filled code input')
|
||||
return true
|
||||
}
|
||||
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-CODE', 'No code input field found')
|
||||
return false
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Failed to fill code input: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async handle(page: Page): Promise<void> {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Code login authentication requested')
|
||||
|
||||
const emailMessage = await getSubtitleMessage(page)
|
||||
if (emailMessage) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', `Page message: "${emailMessage}"`)
|
||||
} else {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-CODE', 'Unable to retrieve email code destination')
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||
const code = await promptInput({
|
||||
question: `Enter the 6-digit code (waiting ${this.maxManualSeconds}s): `,
|
||||
timeoutSeconds: this.maxManualSeconds,
|
||||
validate: code => /^\d{6}$/.test(code)
|
||||
})
|
||||
|
||||
if (!code || !/^\d{6}$/.test(code)) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Invalid or missing code (attempt ${attempt}/${this.maxManualAttempts}) | input length=${code?.length}`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Manual code input failed or timed out')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const filled = await this.fillCode(page, code)
|
||||
if (!filled) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Unable to fill code input (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Code input field not found')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(500)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
// Check if wrong code was entered
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Incorrect code: ${errorMessage} (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error(`Maximum attempts reached: ${errorMessage}`)
|
||||
}
|
||||
|
||||
// Clear the input field before retrying
|
||||
const inputToClear = await page.$(this.textInputSelector).catch(() => null)
|
||||
if (inputToClear) {
|
||||
await inputToClear.click()
|
||||
await page.keyboard.press('Control+A')
|
||||
await page.keyboard.press('Backspace')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-CODE', 'Code authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Code input failed after ${this.maxManualAttempts} attempts`)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-CODE',
|
||||
`Error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/browser/auth/methods/LoginUtils.ts
Normal file
66
src/browser/auth/methods/LoginUtils.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { Page } from 'patchright'
|
||||
import readline from 'readline'
|
||||
|
||||
export interface PromptOptions {
|
||||
question: string
|
||||
timeoutSeconds?: number
|
||||
validate?: (input: string) => boolean
|
||||
transform?: (input: string) => string
|
||||
}
|
||||
|
||||
export function promptInput(options: PromptOptions): Promise<string | null> {
|
||||
const { question, timeoutSeconds = 60, validate, transform } = options
|
||||
|
||||
return new Promise(resolve => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
let resolved = false
|
||||
|
||||
const cleanup = (result: string | null) => {
|
||||
if (resolved) return
|
||||
resolved = true
|
||||
clearTimeout(timer)
|
||||
rl.close()
|
||||
resolve(result)
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => cleanup(null), timeoutSeconds * 1000)
|
||||
|
||||
rl.question(question, answer => {
|
||||
let value = answer.trim()
|
||||
if (transform) value = transform(value)
|
||||
|
||||
if (validate && !validate(value)) {
|
||||
cleanup(null)
|
||||
return
|
||||
}
|
||||
|
||||
cleanup(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function getSubtitleMessage(page: Page): Promise<string | null> {
|
||||
const message = await page
|
||||
.waitForSelector('[data-testid="subtitle"]', { state: 'visible', timeout: 1000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (!message) return null
|
||||
|
||||
const text = await message.innerText()
|
||||
return text.trim()
|
||||
}
|
||||
|
||||
export async function getErrorMessage(page: Page): Promise<string | null> {
|
||||
const errorAlert = await page
|
||||
.waitForSelector('div[role="alert"]', { state: 'visible', timeout: 1000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (!errorAlert) return null
|
||||
|
||||
const text = await errorAlert.innerText()
|
||||
return text.trim()
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Page } from 'patchright'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { URLSearchParams } from 'url'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
|
||||
@@ -29,25 +28,70 @@ export class MobileAccessLogin {
|
||||
authorizeUrl.searchParams.append('access_type', 'offline_access')
|
||||
authorizeUrl.searchParams.append('login_hint', email)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`Auth URL constructed: ${authorizeUrl.origin}${authorizeUrl.pathname}`
|
||||
)
|
||||
|
||||
await this.bot.browser.utils.disableFido(this.page)
|
||||
|
||||
await this.page.goto(authorizeUrl.href).catch(() => {})
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Navigating to OAuth authorize URL')
|
||||
|
||||
await this.page.goto(authorizeUrl.href).catch(err => {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`page.goto() failed: ${err instanceof Error ? err.message : String(err)}`
|
||||
)
|
||||
})
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Waiting for mobile OAuth code...')
|
||||
|
||||
const start = Date.now()
|
||||
let code = ''
|
||||
let lastUrl = ''
|
||||
|
||||
while (Date.now() - start < this.maxTimeout) {
|
||||
const url = new URL(this.page.url())
|
||||
if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') {
|
||||
code = url.searchParams.get('code') || ''
|
||||
if (code) break
|
||||
const currentUrl = this.page.url()
|
||||
|
||||
// Log only when URL changes (high signal, no spam)
|
||||
if (currentUrl !== lastUrl) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `OAuth poll URL changed → ${currentUrl}`)
|
||||
lastUrl = currentUrl
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(currentUrl)
|
||||
|
||||
if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') {
|
||||
code = url.searchParams.get('code') || ''
|
||||
|
||||
if (code) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'OAuth code detected in redirect URL')
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`Invalid URL while polling: ${String(currentUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'Timed out waiting for OAuth code')
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`Timed out waiting for OAuth code after ${Math.round((Date.now() - start) / 1000)}s`
|
||||
)
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `Final page URL: ${this.page.url()}`)
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -57,18 +101,24 @@ export class MobileAccessLogin {
|
||||
data.append('code', code)
|
||||
data.append('redirect_uri', this.redirectUrl)
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Exchanging OAuth code for access token')
|
||||
|
||||
const response = await this.bot.axios.request({
|
||||
url: this.tokenUrl,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: data.toString()
|
||||
}
|
||||
})
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
const token = (response?.data?.access_token as string) ?? ''
|
||||
|
||||
if (!token) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'No access_token in token response')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`Token response payload: ${JSON.stringify(response?.data)}`
|
||||
)
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -78,10 +128,11 @@ export class MobileAccessLogin {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`MobileAccess error: ${error instanceof Error ? error.message : String(error)}`
|
||||
`MobileAccess error: ${error instanceof Error ? error.stack || error.message : String(error)}`
|
||||
)
|
||||
return ''
|
||||
} finally {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Returning to base URL')
|
||||
await this.page.goto(this.bot.config.baseURL, { timeout: 10000 }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
187
src/browser/auth/methods/RecoveryEmailLogin.ts
Normal file
187
src/browser/auth/methods/RecoveryEmailLogin.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { Page } from 'patchright'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
import { getErrorMessage, promptInput } from './LoginUtils'
|
||||
|
||||
export class RecoveryLogin {
|
||||
private readonly textInputSelector = '[data-testid="proof-confirmation"]'
|
||||
private readonly maxManualSeconds = 60
|
||||
private readonly maxManualAttempts = 5
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
|
||||
private async fillEmail(page: Page, email: string): Promise<boolean> {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Attempting to fill email: ${email}`)
|
||||
|
||||
const visibleInput = await page
|
||||
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
|
||||
.catch(() => null)
|
||||
|
||||
if (visibleInput) {
|
||||
await page.keyboard.type(email, { delay: 50 })
|
||||
await page.keyboard.press('Enter')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Successfully filled email input field')
|
||||
return true
|
||||
}
|
||||
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Email input field not found with selector: ${this.textInputSelector}`
|
||||
)
|
||||
return false
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Failed to fill email input: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async handle(page: Page, recoveryEmail: string): Promise<void> {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email recovery authentication flow initiated')
|
||||
|
||||
if (recoveryEmail) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Using provided recovery email: ${recoveryEmail}`
|
||||
)
|
||||
|
||||
const filled = await this.fillEmail(page, recoveryEmail)
|
||||
if (!filled) {
|
||||
throw new Error('Email input field not found')
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response')
|
||||
await this.bot.utils.wait(500)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached')
|
||||
})
|
||||
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
throw new Error(`Email verification failed: ${errorMessage}`)
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
'No recovery email provided, will prompt user for input'
|
||||
)
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Starting attempt ${attempt}/${this.maxManualAttempts}`
|
||||
)
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Prompting user for email input (timeout: ${this.maxManualSeconds}s)`
|
||||
)
|
||||
|
||||
const email = await promptInput({
|
||||
question: `Recovery email (waiting ${this.maxManualSeconds}s): `,
|
||||
timeoutSeconds: this.maxManualSeconds,
|
||||
validate: email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
})
|
||||
|
||||
if (!email) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`No or invalid email input received (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Manual email input failed: no input received')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Invalid email format received (attempt ${attempt}/${this.maxManualAttempts}) | length=${email.length}`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Manual email input failed: invalid format')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', `Valid email received from user: ${email}`)
|
||||
|
||||
const filled = await this.fillEmail(page, email)
|
||||
if (!filled) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Failed to fill email input field (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Email input field not found after maximum attempts')
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Waiting for page response')
|
||||
await this.bot.utils.wait(500)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'LOGIN-RECOVERY', 'Network idle timeout reached')
|
||||
})
|
||||
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-RECOVERY',
|
||||
`Error from page: "${errorMessage}" (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error(`Maximum attempts reached. Last error: ${errorMessage}`)
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Clearing input field for retry')
|
||||
const inputToClear = await page.$(this.textInputSelector).catch(() => null)
|
||||
if (inputToClear) {
|
||||
await inputToClear.click()
|
||||
await page.keyboard.press('Control+A')
|
||||
await page.keyboard.press('Backspace')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Input field cleared')
|
||||
} else {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-RECOVERY', 'Could not find input field to clear')
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-RECOVERY', 'Email authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Email input failed after ${this.maxManualAttempts} attempts`)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-RECOVERY', `Fatal error: ${errorMsg}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Page } from 'patchright'
|
||||
import * as OTPAuth from 'otpauth'
|
||||
import readline from 'readline'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
import { getErrorMessage, promptInput } from './LoginUtils'
|
||||
|
||||
export class TotpLogin {
|
||||
private readonly textInputSelector =
|
||||
'form[name="OneTimeCodeViewForm"] input[type="text"], input#floatingLabelInput5'
|
||||
private readonly hiddenInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
||||
private readonly secondairyInputSelector = 'input[id="otc-confirmation-input"], input[name="otc"]'
|
||||
private readonly submitButtonSelector = 'button[type="submit"]'
|
||||
private readonly maxManualSeconds = 60
|
||||
private readonly maxManualAttempts = 5
|
||||
@@ -17,31 +17,6 @@ export class TotpLogin {
|
||||
return new OTPAuth.TOTP({ secret, digits: 6 }).generate()
|
||||
}
|
||||
|
||||
private async promptManualCode(): Promise<string | null> {
|
||||
return await new Promise(resolve => {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
let resolved = false
|
||||
|
||||
const cleanup = (result: string | null) => {
|
||||
if (resolved) return
|
||||
resolved = true
|
||||
clearTimeout(timer)
|
||||
rl.close()
|
||||
resolve(result)
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => cleanup(null), this.maxManualSeconds * 1000)
|
||||
|
||||
rl.question(`Enter the 6-digit TOTP code (waiting ${this.maxManualSeconds}s): `, answer => {
|
||||
cleanup(answer.trim())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private async fillCode(page: Page, code: string): Promise<boolean> {
|
||||
try {
|
||||
const visibleInput = await page
|
||||
@@ -50,19 +25,18 @@ export class TotpLogin {
|
||||
|
||||
if (visibleInput) {
|
||||
await visibleInput.fill(code)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled visible TOTP text input')
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
|
||||
return true
|
||||
}
|
||||
|
||||
const hiddenInput = await page.$(this.hiddenInputSelector)
|
||||
|
||||
if (hiddenInput) {
|
||||
await hiddenInput.fill(code)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled hidden TOTP input')
|
||||
const secondairyInput = await page.$(this.secondairyInputSelector)
|
||||
if (secondairyInput) {
|
||||
await secondairyInput.fill(code)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled TOTP input')
|
||||
return true
|
||||
}
|
||||
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found (visible or hidden)')
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found')
|
||||
return false
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(
|
||||
@@ -83,9 +57,8 @@ export class TotpLogin {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Generated TOTP code from secret')
|
||||
|
||||
const filled = await this.fillCode(page, code)
|
||||
|
||||
if (!filled) {
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', 'Unable to locate or fill TOTP input field')
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', 'Unable to fill TOTP input field')
|
||||
throw new Error('TOTP input field not found')
|
||||
}
|
||||
|
||||
@@ -93,6 +66,12 @@ export class TotpLogin {
|
||||
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-TOTP', `TOTP failed: ${errorMessage}`)
|
||||
throw new Error(`TOTP authentication failed: ${errorMessage}`)
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
||||
return
|
||||
}
|
||||
@@ -100,45 +79,36 @@ export class TotpLogin {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP secret provided, awaiting manual input')
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxManualAttempts; attempt++) {
|
||||
const code = await this.promptManualCode()
|
||||
const code = await promptInput({
|
||||
question: `Enter the 6-digit TOTP code (waiting ${this.maxManualSeconds}s): `,
|
||||
timeoutSeconds: this.maxManualSeconds,
|
||||
validate: code => /^\d{6}$/.test(code)
|
||||
})
|
||||
|
||||
if (!code || !/^\d{6}$/.test(code)) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`Invalid or missing TOTP code (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
`Invalid or missing code (attempt ${attempt}/${this.maxManualAttempts}) | input length=${code?.length}`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('Manual TOTP input failed or timed out')
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
'Retrying manual TOTP input due to invalid code'
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const filled = await this.fillCode(page, code)
|
||||
|
||||
if (!filled) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`Unable to locate or fill TOTP input field (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
`Unable to fill TOTP input (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error('TOTP input field not found')
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
'Retrying manual TOTP input due to fill failure'
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -146,16 +116,31 @@ export class TotpLogin {
|
||||
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
// Check if wrong code was entered
|
||||
const errorMessage = await getErrorMessage(page)
|
||||
if (errorMessage) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`Incorrect code: ${errorMessage} (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
if (attempt === this.maxManualAttempts) {
|
||||
throw new Error(`Maximum attempts reached: ${errorMessage}`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error(`Manual TOTP input failed after ${this.maxManualAttempts} attempts`)
|
||||
throw new Error(`TOTP input failed after ${this.maxManualAttempts} attempts`)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
`Error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -4,13 +4,10 @@
|
||||
"headless": false,
|
||||
"runOnZeroPoints": false,
|
||||
"clusters": 1,
|
||||
"errorDiagnostics": true,
|
||||
"saveFingerprint": {
|
||||
"mobile": false,
|
||||
"desktop": false
|
||||
},
|
||||
"errorDiagnostics": false,
|
||||
"workers": {
|
||||
"doDailySet": true,
|
||||
"doSpecialPromotions": true,
|
||||
"doMorePromotions": true,
|
||||
"doPunchCards": true,
|
||||
"doAppPromotions": true,
|
||||
@@ -25,6 +22,12 @@
|
||||
"scrollRandomResults": false,
|
||||
"clickRandomResults": false,
|
||||
"parallelSearching": true,
|
||||
"queryEngines": [
|
||||
"google",
|
||||
"wikipedia",
|
||||
"reddit",
|
||||
"local"
|
||||
],
|
||||
"searchResultVisitTime": "10sec",
|
||||
"searchDelay": {
|
||||
"min": "30sec",
|
||||
@@ -39,8 +42,13 @@
|
||||
"consoleLogFilter": {
|
||||
"enabled": false,
|
||||
"mode": "whitelist",
|
||||
"levels": ["error", "warn"],
|
||||
"keywords": ["starting account"],
|
||||
"levels": [
|
||||
"error",
|
||||
"warn"
|
||||
],
|
||||
"keywords": [
|
||||
"starting account"
|
||||
],
|
||||
"regexPatterns": []
|
||||
},
|
||||
"proxy": {
|
||||
@@ -57,15 +65,24 @@
|
||||
"topic": "",
|
||||
"token": "",
|
||||
"title": "Microsoft-Rewards-Script",
|
||||
"tags": ["bot", "notify"],
|
||||
"tags": [
|
||||
"bot",
|
||||
"notify"
|
||||
],
|
||||
"priority": 3
|
||||
},
|
||||
"webhookLogFilter": {
|
||||
"enabled": false,
|
||||
"mode": "whitelist",
|
||||
"levels": ["error"],
|
||||
"keywords": ["starting account", "select number", "collected"],
|
||||
"levels": [
|
||||
"error"
|
||||
],
|
||||
"keywords": [
|
||||
"starting account",
|
||||
"select number",
|
||||
"collected"
|
||||
],
|
||||
"regexPatterns": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,18 @@ import { AppReward } from './activities/app/AppReward'
|
||||
import { UrlReward } from './activities/api/UrlReward'
|
||||
import { Quiz } from './activities/api/Quiz'
|
||||
import { FindClippy } from './activities/api/FindClippy'
|
||||
import { DoubleSearchPoints } from './activities/api/DoubleSearchPoints'
|
||||
|
||||
// Browser
|
||||
import { SearchOnBing } from './activities/browser/SearchOnBing'
|
||||
import { Search } from './activities/browser/Search'
|
||||
|
||||
import type { BasePromotion, DashboardData, FindClippyPromotion } from '../interface/DashboardData'
|
||||
import type {
|
||||
BasePromotion,
|
||||
DashboardData,
|
||||
FindClippyPromotion,
|
||||
PurplePromotionalItem
|
||||
} from '../interface/DashboardData'
|
||||
import type { Promotion } from '../interface/AppDashBoardData'
|
||||
|
||||
export default class Activities {
|
||||
@@ -68,9 +74,14 @@ export default class Activities {
|
||||
await quiz.doQuiz(promotion)
|
||||
}
|
||||
|
||||
doFindClippy = async (promotions: FindClippyPromotion): Promise<void> => {
|
||||
const urlReward = new FindClippy(this.bot)
|
||||
await urlReward.doFindClippy(promotions)
|
||||
doFindClippy = async (promotion: FindClippyPromotion): Promise<void> => {
|
||||
const findClippy = new FindClippy(this.bot)
|
||||
await findClippy.doFindClippy(promotion)
|
||||
}
|
||||
|
||||
doDoubleSearchPoints = async (promotion: PurplePromotionalItem): Promise<void> => {
|
||||
const doubleSearchPoints = new DoubleSearchPoints(this.bot)
|
||||
await doubleSearchPoints.doDoubleSearchPoints(promotion)
|
||||
}
|
||||
|
||||
// App Activities
|
||||
|
||||
@@ -1,22 +1,207 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type {
|
||||
BingSuggestionResponse,
|
||||
BingTrendingTopicsResponse,
|
||||
GoogleSearch,
|
||||
GoogleTrendsResponse
|
||||
} from '../interface/Search'
|
||||
import * as fs from 'fs'
|
||||
import path from 'path'
|
||||
import type { GoogleSearch, GoogleTrendsResponse, RedditListing, WikipediaTopResponse } from '../interface/Search'
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import { QueryEngine } from '../interface/Config'
|
||||
|
||||
export class QueryCore {
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
|
||||
async queryManager(
|
||||
options: {
|
||||
shuffle?: boolean
|
||||
sourceOrder?: QueryEngine[]
|
||||
related?: boolean
|
||||
langCode?: string
|
||||
geoLocale?: string
|
||||
} = {}
|
||||
): Promise<string[]> {
|
||||
const {
|
||||
shuffle = false,
|
||||
sourceOrder = ['google', 'wikipedia', 'reddit', 'local'],
|
||||
related = true,
|
||||
langCode = 'en',
|
||||
geoLocale = 'US'
|
||||
} = options
|
||||
|
||||
try {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`start | shuffle=${shuffle}, related=${related}, lang=${langCode}, geo=${geoLocale}, sources=${sourceOrder.join(',')}`
|
||||
)
|
||||
|
||||
const topicLists: string[][] = []
|
||||
|
||||
const sourceHandlers: Record<
|
||||
'google' | 'wikipedia' | 'reddit' | 'local',
|
||||
(() => Promise<string[]>) | (() => string[])
|
||||
> = {
|
||||
google: async () => {
|
||||
const topics = await this.getGoogleTrends(geoLocale.toUpperCase()).catch(() => [])
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `google: ${topics.length}`)
|
||||
return topics
|
||||
},
|
||||
wikipedia: async () => {
|
||||
const topics = await this.getWikipediaTrending(langCode).catch(() => [])
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `wikipedia: ${topics.length}`)
|
||||
return topics
|
||||
},
|
||||
reddit: async () => {
|
||||
const topics = await this.getRedditTopics().catch(() => [])
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `reddit: ${topics.length}`)
|
||||
return topics
|
||||
},
|
||||
local: () => {
|
||||
const topics = this.getLocalQueryList()
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `local: ${topics.length}`)
|
||||
return topics
|
||||
}
|
||||
}
|
||||
|
||||
for (const source of sourceOrder) {
|
||||
const handler = sourceHandlers[source]
|
||||
if (!handler) continue
|
||||
|
||||
const topics = await Promise.resolve(handler())
|
||||
if (topics.length) topicLists.push(topics)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`sources combined | rawTotal=${topicLists.flat().length}`
|
||||
)
|
||||
|
||||
const baseTopics = this.normalizeAndDedupe(topicLists.flat())
|
||||
|
||||
if (!baseTopics.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'No base topics found (all sources empty)')
|
||||
return []
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`baseTopics dedupe | before=${topicLists.flat().length} | after=${baseTopics.length}`
|
||||
)
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `baseTopics: ${baseTopics.length}`)
|
||||
|
||||
const clusters = related ? await this.buildRelatedClusters(baseTopics, langCode) : baseTopics.map(t => [t])
|
||||
|
||||
this.bot.utils.shuffleArray(clusters)
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'clusters shuffled')
|
||||
|
||||
let finalQueries = clusters.flat()
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`clusters flattened | total=${finalQueries.length}`
|
||||
)
|
||||
|
||||
// Do not cluster searches and shuffle
|
||||
if (shuffle) {
|
||||
this.bot.utils.shuffleArray(finalQueries)
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'finalQueries shuffled')
|
||||
}
|
||||
|
||||
finalQueries = this.normalizeAndDedupe(finalQueries)
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`finalQueries dedupe | after=${finalQueries.length}`
|
||||
)
|
||||
|
||||
if (!finalQueries.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', 'finalQueries deduped to 0')
|
||||
return []
|
||||
}
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `final queries: ${finalQueries.length}`)
|
||||
|
||||
return finalQueries
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'QUERY-MANAGER', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`error: ${error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)}`
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async buildRelatedClusters(baseTopics: string[], langCode: string): Promise<string[][]> {
|
||||
const clusters: string[][] = []
|
||||
|
||||
const LIMIT = 50
|
||||
const head = baseTopics.slice(0, LIMIT)
|
||||
const tail = baseTopics.slice(LIMIT)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`related enabled | baseTopics=${baseTopics.length} | expand=${head.length} | passthrough=${tail.length} | lang=${langCode}`
|
||||
)
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`bing expansion enabled | limit=${LIMIT} | totalCalls=${head.length * 2}`
|
||||
)
|
||||
|
||||
for (const topic of head) {
|
||||
const suggestions = await this.getBingSuggestions(topic, langCode).catch(() => [])
|
||||
const relatedTerms = await this.getBingRelatedTerms(topic).catch(() => [])
|
||||
|
||||
const usedSuggestions = suggestions.slice(0, 6)
|
||||
const usedRelated = relatedTerms.slice(0, 3)
|
||||
|
||||
const cluster = this.normalizeAndDedupe([topic, ...usedSuggestions, ...usedRelated])
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUERY-MANAGER',
|
||||
`cluster expanded | topic="${topic}" | suggestions=${suggestions.length}->${usedSuggestions.length} | related=${relatedTerms.length}->${usedRelated.length} | clusterSize=${cluster.length}`
|
||||
)
|
||||
|
||||
clusters.push(cluster)
|
||||
}
|
||||
|
||||
if (tail.length) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'QUERY-MANAGER', `cluster passthrough | topics=${tail.length}`)
|
||||
|
||||
for (const topic of tail) {
|
||||
clusters.push([topic])
|
||||
}
|
||||
}
|
||||
|
||||
return clusters
|
||||
}
|
||||
|
||||
private normalizeAndDedupe(queries: string[]): string[] {
|
||||
const seen = new Set<string>()
|
||||
const out: string[] = []
|
||||
|
||||
for (const q of queries) {
|
||||
if (!q) continue
|
||||
const trimmed = q.trim()
|
||||
if (!trimmed) continue
|
||||
|
||||
const norm = trimmed.replace(/\s+/g, ' ').toLowerCase()
|
||||
if (seen.has(norm)) continue
|
||||
|
||||
seen.add(norm)
|
||||
out.push(trimmed)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
async getGoogleTrends(geoLocale: string): Promise<string[]> {
|
||||
const queryTerms: GoogleSearch[] = []
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-GOOGLE-TRENDS',
|
||||
`Generating search queries, can take a while! | GeoLocale: ${geoLocale}`
|
||||
)
|
||||
|
||||
try {
|
||||
const request: AxiosRequestConfig = {
|
||||
@@ -29,163 +214,287 @@ export class QueryCore {
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||
const rawData = response.data
|
||||
|
||||
const trendsData = this.extractJsonFromResponse(rawData)
|
||||
const trendsData = this.extractJsonFromResponse(response.data)
|
||||
if (!trendsData) {
|
||||
throw this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-GOOGLE-TRENDS',
|
||||
'Failed to parse Google Trends response'
|
||||
)
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No queries')
|
||||
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No trendsData parsed from response')
|
||||
return []
|
||||
}
|
||||
|
||||
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)])
|
||||
if (mappedTrendsData.length < 90) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-GOOGLE-TRENDS',
|
||||
'Insufficient search queries, falling back to US'
|
||||
)
|
||||
const mapped = trendsData.map(q => [q[0], q[9]!.slice(1)])
|
||||
|
||||
if (mapped.length < 90 && geoLocale !== 'US') {
|
||||
return this.getGoogleTrends('US')
|
||||
}
|
||||
|
||||
for (const [topic, relatedQueries] of mappedTrendsData) {
|
||||
for (const [topic, related] of mapped) {
|
||||
queryTerms.push({
|
||||
topic: topic as string,
|
||||
related: relatedQueries as string[]
|
||||
related: related as string[]
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-GOOGLE-TRENDS',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
`request failed: ${
|
||||
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||
}`
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const queries = queryTerms.flatMap(x => [x.topic, ...x.related])
|
||||
|
||||
return queries
|
||||
return queryTerms.flatMap(x => [x.topic, ...x.related])
|
||||
}
|
||||
|
||||
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
|
||||
const lines = text.split('\n')
|
||||
for (const line of lines) {
|
||||
for (const line of text.split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||
try {
|
||||
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (!trimmed.startsWith('[')) continue
|
||||
try {
|
||||
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async getBingSuggestions(query: string = '', langCode: string = 'en'): Promise<string[]> {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-BING-SUGGESTIONS',
|
||||
`Generating bing suggestions! | LangCode: ${langCode}`
|
||||
)
|
||||
|
||||
async getBingSuggestions(query = '', langCode = 'en'): Promise<string[]> {
|
||||
try {
|
||||
const request: AxiosRequestConfig = {
|
||||
url: `https://www.bingapis.com/api/v7/suggestions?q=${encodeURIComponent(query)}&appid=6D0A9B8C5100E9ECC7E11A104ADD76C10219804B&cc=xl&setlang=${langCode}`,
|
||||
url: `https://www.bingapis.com/api/v7/suggestions?q=${encodeURIComponent(
|
||||
query
|
||||
)}&appid=6D0A9B8C5100E9ECC7E11A104ADD76C10219804B&cc=xl&setlang=${langCode}`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(this.bot.fingerprint?.headers ?? {}),
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||
const rawData: BingSuggestionResponse = response.data
|
||||
const suggestions =
|
||||
response.data.suggestionGroups?.[0]?.searchSuggestions?.map((x: { query: any }) => x.query) ?? []
|
||||
|
||||
const searchSuggestions = rawData.suggestionGroups[0]?.searchSuggestions
|
||||
|
||||
if (!searchSuggestions?.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'API returned no results')
|
||||
return []
|
||||
if (!suggestions.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-BING-SUGGESTIONS',
|
||||
`empty suggestions | query="${query}" | lang=${langCode}`
|
||||
)
|
||||
}
|
||||
|
||||
return searchSuggestions.map(x => x.query)
|
||||
return suggestions
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-SUGGESTIONS', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-GOOGLE-TRENDS',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
'SEARCH-BING-SUGGESTIONS',
|
||||
`request failed | query="${query}" | lang=${langCode} | error=${
|
||||
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||
}`
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async getBingRelatedTerms(term: string): Promise<string[]> {
|
||||
async getBingRelatedTerms(query: string): Promise<string[]> {
|
||||
try {
|
||||
const request = {
|
||||
url: `https://api.bing.com/osjson.aspx?query=${term}`,
|
||||
const request: AxiosRequestConfig = {
|
||||
url: `https://api.bing.com/osjson.aspx?query=${encodeURIComponent(query)}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
...(this.bot.fingerprint?.headers ?? {})
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||
const rawData = response.data
|
||||
const related = response.data?.[1]
|
||||
const out = Array.isArray(related) ? related : []
|
||||
|
||||
const relatedTerms = rawData[1]
|
||||
|
||||
if (!relatedTerms?.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'API returned no results')
|
||||
return []
|
||||
if (!out.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-BING-RELATED',
|
||||
`empty related terms | query="${query}"`
|
||||
)
|
||||
}
|
||||
|
||||
return relatedTerms
|
||||
return out
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-RELATED', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-BING-RELATED',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
`request failed | query="${query}" | error=${
|
||||
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||
}`
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async getBingTendingTopics(langCode: string = 'en'): Promise<string[]> {
|
||||
async getBingTrendingTopics(langCode = 'en'): Promise<string[]> {
|
||||
try {
|
||||
const request = {
|
||||
const request: AxiosRequestConfig = {
|
||||
url: `https://www.bing.com/api/v7/news/trendingtopics?appid=91B36E34F9D1B900E54E85A77CF11FB3BE5279E6&cc=xl&setlang=${langCode}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
Authorization: `Bearer ${this.bot.accessToken}`,
|
||||
'User-Agent':
|
||||
'Bing/32.5.431027001 (com.microsoft.bing; build:431027001; iOS 17.6.1) Alamofire/5.10.2',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Rewards-Country': this.bot.userData.geoLocale,
|
||||
'X-Rewards-Language': 'en',
|
||||
'X-Rewards-ismobile': 'true'
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||
const rawData: BingTrendingTopicsResponse = response.data
|
||||
const topics =
|
||||
response.data.value?.map(
|
||||
(x: { query: { text: string }; name: string }) => x.query?.text?.trim() || x.name.trim()
|
||||
) ?? []
|
||||
|
||||
const trendingTopics = rawData.value
|
||||
|
||||
if (!trendingTopics?.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'API returned no results')
|
||||
return []
|
||||
if (!topics.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-BING-TRENDING',
|
||||
`empty trending topics | lang=${langCode}`
|
||||
)
|
||||
}
|
||||
|
||||
const queries = trendingTopics.map(x => x.query?.text?.trim() || x.name.trim())
|
||||
|
||||
return queries
|
||||
return topics
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-BING-TRENDING', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-BING-TRENDING',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
`request failed | lang=${langCode} | error=${
|
||||
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||
}`
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
async getWikipediaTrending(langCode = 'en'): Promise<string[]> {
|
||||
try {
|
||||
const date = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
const yyyy = date.getUTCFullYear()
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, '0')
|
||||
const dd = String(date.getUTCDate()).padStart(2, '0')
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
url: `https://wikimedia.org/api/rest_v1/metrics/pageviews/top/${langCode}.wikipedia/all-access/${yyyy}/${mm}/${dd}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...(this.bot.fingerprint?.headers ?? {})
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||
const articles = (response.data as WikipediaTopResponse).items?.[0]?.articles ?? []
|
||||
|
||||
const out = articles.slice(0, 50).map(a => a.article.replace(/_/g, ' '))
|
||||
|
||||
if (!out.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-WIKIPEDIA-TRENDING', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-WIKIPEDIA-TRENDING',
|
||||
`empty wikipedia top | lang=${langCode}`
|
||||
)
|
||||
}
|
||||
|
||||
return out
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-WIKIPEDIA-TRENDING', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-WIKIPEDIA-TRENDING',
|
||||
`request failed | lang=${langCode} | error=${
|
||||
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||
}`
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async getRedditTopics(subreddit = 'popular'): Promise<string[]> {
|
||||
try {
|
||||
const safe = subreddit.replace(/[^a-zA-Z0-9_+]/g, '')
|
||||
const request: AxiosRequestConfig = {
|
||||
url: `https://www.reddit.com/r/${safe}.json?limit=50`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
...(this.bot.fingerprint?.headers ?? {})
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request, this.bot.config.proxy.queryEngine)
|
||||
const posts = (response.data as RedditListing).data?.children ?? []
|
||||
|
||||
const out = posts.filter(p => !p.data.over_18).map(p => p.data.title)
|
||||
|
||||
if (!out.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-REDDIT-TRENDING', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-REDDIT-TRENDING',
|
||||
`empty reddit listing | subreddit=${safe}`
|
||||
)
|
||||
}
|
||||
|
||||
return out
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-REDDIT', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-REDDIT',
|
||||
`request failed | subreddit=${subreddit} | error=${
|
||||
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||
}`
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
getLocalQueryList(): string[] {
|
||||
try {
|
||||
const file = path.join(__dirname, './search-queries.json')
|
||||
const queries = JSON.parse(fs.readFileSync(file, 'utf8')) as string[]
|
||||
const out = Array.isArray(queries) ? queries : []
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-LOCAL-QUERY-LIST',
|
||||
'local queries loaded | file=search-queries.json'
|
||||
)
|
||||
|
||||
if (!out.length) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-LOCAL-QUERY-LIST', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-LOCAL-QUERY-LIST',
|
||||
'search-queries.json parsed but empty or invalid'
|
||||
)
|
||||
}
|
||||
|
||||
return out
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'SEARCH-LOCAL-QUERY-LIST', 'No queries')
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-LOCAL-QUERY-LIST',
|
||||
`read/parse failed | error=${
|
||||
error instanceof Error ? `${error.name}: ${error.message}\n${error.stack ?? ''}` : String(error)
|
||||
}`
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class SearchManager {
|
||||
|
||||
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Closing mobile session')
|
||||
try {
|
||||
await executionContext.run({ isMobile: true, accountEmail }, async () => {
|
||||
await executionContext.run({ isMobile: true, account }, async () => {
|
||||
await this.bot.browser.func.closeBrowser(mobileSession.context, accountEmail)
|
||||
})
|
||||
this.bot.logger.info('main', 'SEARCH-MANAGER', 'Mobile session closed')
|
||||
@@ -368,7 +368,7 @@ export class SearchManager {
|
||||
`Init | account=${accountEmail} | proxy=${account.proxy ?? 'none'}`
|
||||
)
|
||||
|
||||
const session = await this.bot['browserFactory'].createBrowser(account.proxy, accountEmail)
|
||||
const session = await this.bot['browserFactory'].createBrowser(account)
|
||||
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Browser created, new page')
|
||||
|
||||
this.bot.mainDesktopPage = await session.context.newPage()
|
||||
@@ -377,7 +377,7 @@ export class SearchManager {
|
||||
this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login start')
|
||||
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'Calling login handler')
|
||||
|
||||
await this.bot['login'].login(this.bot.mainDesktopPage, accountEmail, account.password, account.totp)
|
||||
await this.bot['login'].login(this.bot.mainDesktopPage, account)
|
||||
|
||||
this.bot.logger.info('main', 'SEARCH-DESKTOP-LOGIN', 'Login passed, verifying')
|
||||
this.bot.logger.debug('main', 'SEARCH-DESKTOP-LOGIN', 'verifyBingSession')
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { Page } from 'patchright'
|
||||
import type { MicrosoftRewardsBot } from '../index'
|
||||
import type { DashboardData, PunchCard, BasePromotion, FindClippyPromotion } from '../interface/DashboardData'
|
||||
import type {
|
||||
DashboardData,
|
||||
PunchCard,
|
||||
BasePromotion,
|
||||
FindClippyPromotion,
|
||||
PurplePromotionalItem
|
||||
} from '../interface/DashboardData'
|
||||
import type { AppDashboardData } from '../interface/AppDashBoardData'
|
||||
|
||||
export class Workers {
|
||||
@@ -38,13 +44,14 @@ export class Workers {
|
||||
]
|
||||
|
||||
const activitiesUncompleted: BasePromotion[] =
|
||||
morePromotions?.filter(
|
||||
x =>
|
||||
!x.complete &&
|
||||
x.pointProgressMax > 0 &&
|
||||
x.exclusiveLockedFeatureStatus !== 'locked' &&
|
||||
x.promotionType
|
||||
) ?? []
|
||||
morePromotions?.filter(x => {
|
||||
if (x.complete) return false
|
||||
if (x.pointProgressMax <= 0) return false
|
||||
if (x.exclusiveLockedFeatureStatus === 'locked') return false
|
||||
if (!x.promotionType) return false
|
||||
|
||||
return true
|
||||
}) ?? []
|
||||
|
||||
if (!activitiesUncompleted.length) {
|
||||
this.bot.logger.info(
|
||||
@@ -67,13 +74,14 @@ export class Workers {
|
||||
}
|
||||
|
||||
public async doAppPromotions(data: AppDashboardData) {
|
||||
const appRewards = data.response.promotions.filter(
|
||||
x =>
|
||||
x.attributes['complete']?.toLowerCase() === 'false' &&
|
||||
x.attributes['offerid'] &&
|
||||
x.attributes['type'] &&
|
||||
x.attributes['type'] === 'sapphire'
|
||||
)
|
||||
const appRewards = data.response.promotions.filter(x => {
|
||||
if (x.attributes['complete']?.toLowerCase() !== 'false') return false
|
||||
if (!x.attributes['offerid']) return false
|
||||
if (!x.attributes['type']) return false
|
||||
if (x.attributes['type'] !== 'sapphire') return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (!appRewards.length) {
|
||||
this.bot.logger.info(
|
||||
@@ -93,6 +101,77 @@ export class Workers {
|
||||
this.bot.logger.info(this.bot.isMobile, 'APP-PROMOTIONS', 'All "App Promotions" items have been completed')
|
||||
}
|
||||
|
||||
public async doSpecialPromotions(data: DashboardData) {
|
||||
const specialPromotions: PurplePromotionalItem[] = [
|
||||
...new Map(
|
||||
[...(data.promotionalItems ?? [])]
|
||||
.filter(Boolean)
|
||||
.map(p => [p.offerId, p as PurplePromotionalItem] as const)
|
||||
).values()
|
||||
]
|
||||
|
||||
const supportedPromotions = ['ww_banner_optin_2x']
|
||||
|
||||
const specialPromotionsUncompleted: PurplePromotionalItem[] =
|
||||
specialPromotions?.filter(x => {
|
||||
if (x.complete) return false
|
||||
if (x.exclusiveLockedFeatureStatus === 'locked') return false
|
||||
if (!x.promotionType) return false
|
||||
|
||||
const offerId = (x.offerId ?? '').toLowerCase()
|
||||
return supportedPromotions.some(s => offerId.includes(s))
|
||||
}) ?? []
|
||||
|
||||
for (const activity of specialPromotionsUncompleted) {
|
||||
try {
|
||||
const type = activity.promotionType?.toLowerCase() ?? ''
|
||||
const name = activity.name?.toLowerCase() ?? ''
|
||||
const offerId = (activity as PurplePromotionalItem).offerId
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SPECIAL-ACTIVITY',
|
||||
`Processing activity | title="${activity.title}" | offerId=${offerId} | type=${type}"`
|
||||
)
|
||||
|
||||
switch (type) {
|
||||
// UrlReward
|
||||
case 'urlreward': {
|
||||
// Special "Double Search Points" activation
|
||||
if (name.includes('ww_banner_optin_2x')) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'ACTIVITY',
|
||||
`Found activity type "Double Search Points" | title="${activity.title}" | offerId=${offerId}`
|
||||
)
|
||||
|
||||
await this.bot.activities.doDoubleSearchPoints(activity)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Unsupported types
|
||||
default: {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'SPECIAL-ACTIVITY',
|
||||
`Skipped activity "${activity.title}" | offerId=${offerId} | Reason: Unsupported type "${activity.promotionType}"`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'SPECIAL-ACTIVITY',
|
||||
`Error while solving activity "${activity.title}" | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'SPECIAL-ACTIVITY', 'All "Special Activites" items have been completed')
|
||||
}
|
||||
|
||||
private async solveActivities(activities: BasePromotion[], page: Page, punchCard?: PunchCard) {
|
||||
for (const activity of activities) {
|
||||
try {
|
||||
|
||||
124
src/functions/activities/api/DoubleSearchPoints.ts
Normal file
124
src/functions/activities/api/DoubleSearchPoints.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import { Workers } from '../../Workers'
|
||||
import { PromotionalItem } from '../../../interface/DashboardData'
|
||||
|
||||
export class DoubleSearchPoints extends Workers {
|
||||
private cookieHeader: string = ''
|
||||
|
||||
private fingerprintHeader: { [x: string]: string } = {}
|
||||
|
||||
public async doDoubleSearchPoints(promotion: PromotionalItem) {
|
||||
const offerId = promotion.offerId
|
||||
const activityType = promotion.activityType
|
||||
|
||||
try {
|
||||
if (!this.bot.requestToken) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
'Skipping: Request token not available, this activity requires it!'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.cookieHeader = (this.bot.isMobile ? this.bot.cookies.mobile : this.bot.cookies.desktop)
|
||||
.map((c: { name: string; value: string }) => `${c.name}=${c.value}`)
|
||||
.join('; ')
|
||||
|
||||
const fingerprintHeaders = { ...this.bot.fingerprint.headers }
|
||||
delete fingerprintHeaders['Cookie']
|
||||
delete fingerprintHeaders['cookie']
|
||||
this.fingerprintHeader = fingerprintHeaders
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Starting Double Search Points | offerId=${offerId}`
|
||||
)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Prepared headers | cookieLength=${this.cookieHeader.length} | fingerprintHeaderKeys=${Object.keys(this.fingerprintHeader).length}`
|
||||
)
|
||||
|
||||
const formData = new URLSearchParams({
|
||||
id: offerId,
|
||||
hash: promotion.hash,
|
||||
timeZone: '60',
|
||||
activityAmount: '1',
|
||||
dbs: '0',
|
||||
form: '',
|
||||
type: activityType,
|
||||
__RequestVerificationToken: this.bot.requestToken
|
||||
})
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Prepared Double Search Points form data | offerId=${offerId} | hash=${promotion.hash} | timeZone=60 | activityAmount=1 | type=${activityType}`
|
||||
)
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
url: 'https://rewards.bing.com/api/reportactivity?X-Requested-With=XMLHttpRequest',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(this.bot.fingerprint?.headers ?? {}),
|
||||
Cookie: this.cookieHeader,
|
||||
Referer: 'https://rewards.bing.com/',
|
||||
Origin: 'https://rewards.bing.com'
|
||||
},
|
||||
data: formData
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Sending Double Search Points request | offerId=${offerId} | url=${request.url}`
|
||||
)
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Received Double Search Points response | offerId=${offerId} | status=${response.status}`
|
||||
)
|
||||
|
||||
const data = await this.bot.browser.func.getDashboardData()
|
||||
const promotionalItem = data.promotionalItems.find(item =>
|
||||
item.name.toLowerCase().includes('ww_banner_optin_2x')
|
||||
)
|
||||
|
||||
// If OK, should no longer be presernt in promotionalItems
|
||||
if (promotionalItem) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Unable to find or activate Double Search Points | offerId=${offerId} | status=${response.status}`
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Activated Double Search Points | offerId=${offerId} | status=${response.status}`,
|
||||
'green'
|
||||
)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Waiting after Double Search Points | offerId=${offerId}`
|
||||
)
|
||||
|
||||
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 10000))
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'DOUBLE-SEARCH-POINTS',
|
||||
`Error in doDoubleSearchPoints | offerId=${offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,38 +33,34 @@ export class Search extends Workers {
|
||||
`Search points remaining | Edge=${missingPoints.edgePoints} | Desktop=${missingPoints.desktopPoints} | Mobile=${missingPoints.mobilePoints}`
|
||||
)
|
||||
|
||||
let queries: string[] = []
|
||||
|
||||
const queryCore = new QueryCore(this.bot)
|
||||
const locale = (this.bot.userData.geoLocale ?? 'US').toUpperCase()
|
||||
const langCode = (this.bot.userData.langCode ?? 'en').toLowerCase()
|
||||
|
||||
const locale = this.bot.userData.geoLocale.toUpperCase()
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Resolving search queries via QueryCore | locale=${locale} | lang=${langCode} | related=true`
|
||||
)
|
||||
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Resolving search queries | locale=${locale}`)
|
||||
let queries = await queryCore.queryManager({
|
||||
shuffle: true,
|
||||
related: true,
|
||||
langCode,
|
||||
geoLocale: locale,
|
||||
sourceOrder: ['google', 'wikipedia', 'reddit', 'local']
|
||||
})
|
||||
|
||||
// Set Google search queries
|
||||
queries = await queryCore.getGoogleTrends(locale)
|
||||
queries = [...new Set(queries.map(q => q.trim()).filter(Boolean))]
|
||||
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Fetched base queries | count=${queries.length}`)
|
||||
|
||||
// Deduplicate queries
|
||||
queries = [...new Set(queries)]
|
||||
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Deduplicated queries | count=${queries.length}`)
|
||||
|
||||
// Shuffle
|
||||
queries = this.bot.utils.shuffleArray(queries)
|
||||
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Shuffled queries | count=${queries.length}`)
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Query pool ready | count=${queries.length}`)
|
||||
|
||||
// Go to bing
|
||||
const targetUrl = this.searchPageURL ? this.searchPageURL : this.bingHome
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Navigating to search page | url=${targetUrl}`)
|
||||
|
||||
await page.goto(targetUrl)
|
||||
|
||||
// Wait until page loaded
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
||||
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
let stagnantLoop = 0
|
||||
@@ -77,7 +73,6 @@ export class Search extends Workers {
|
||||
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
||||
const newMissingPointsTotal = newMissingPoints.totalPoints
|
||||
|
||||
// Points gained for THIS query only
|
||||
const rawGained = missingPointsTotal - newMissingPointsTotal
|
||||
const gainedPoints = Math.max(0, rawGained)
|
||||
|
||||
@@ -91,12 +86,10 @@ export class Search extends Workers {
|
||||
} else {
|
||||
stagnantLoop = 0
|
||||
|
||||
// Update global user data
|
||||
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
||||
|
||||
// Track for return value
|
||||
totalGainedPoints += gainedPoints
|
||||
|
||||
this.bot.logger.info(
|
||||
@@ -107,10 +100,8 @@ export class Search extends Workers {
|
||||
)
|
||||
}
|
||||
|
||||
// Update loop state
|
||||
missingPointsTotal = newMissingPointsTotal
|
||||
|
||||
// Completed
|
||||
if (missingPointsTotal === 0) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
@@ -120,7 +111,6 @@ export class Search extends Workers {
|
||||
break
|
||||
}
|
||||
|
||||
// Stuck
|
||||
if (stagnantLoop > stagnantLoopMax) {
|
||||
this.bot.logger.warn(
|
||||
isMobile,
|
||||
@@ -130,105 +120,123 @@ export class Search extends Workers {
|
||||
stagnantLoop = 0
|
||||
break
|
||||
}
|
||||
|
||||
const remainingQueries = queries.length - (i + 1)
|
||||
const minBuffer = 20
|
||||
if (missingPointsTotal > 0 && remainingQueries < minBuffer) {
|
||||
this.bot.logger.warn(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Low query buffer while still missing points, regenerating | remainingQueries=${remainingQueries} | missing=${missingPointsTotal}`
|
||||
)
|
||||
|
||||
const extra = await queryCore.queryManager({
|
||||
shuffle: true,
|
||||
related: true,
|
||||
langCode,
|
||||
geoLocale: locale,
|
||||
sourceOrder: this.bot.config.searchSettings.queryEngines
|
||||
})
|
||||
|
||||
const merged = [...queries, ...extra].map(q => q.trim()).filter(Boolean)
|
||||
queries = [...new Set(merged)]
|
||||
queries = this.bot.utils.shuffleArray(queries)
|
||||
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Query pool regenerated | count=${queries.length}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingPointsTotal > 0) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Search completed but still missing points, generating extra searches | remaining=${missingPointsTotal}`
|
||||
`Search completed but still missing points, continuing with regenerated queries | remaining=${missingPointsTotal}`
|
||||
)
|
||||
|
||||
let i = 0
|
||||
let stagnantLoop = 0
|
||||
const stagnantLoopMax = 5
|
||||
|
||||
while (missingPointsTotal > 0) {
|
||||
const query = queries[i++] as string
|
||||
const extra = await queryCore.queryManager({
|
||||
shuffle: true,
|
||||
related: true,
|
||||
langCode,
|
||||
geoLocale: locale,
|
||||
sourceOrder: this.bot.config.searchSettings.queryEngines
|
||||
})
|
||||
|
||||
const merged = [...queries, ...extra].map(q => q.trim()).filter(Boolean)
|
||||
const newPool = [...new Set(merged)]
|
||||
queries = this.bot.utils.shuffleArray(newPool)
|
||||
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Fetching related terms for extra searches | baseQuery="${query}"`
|
||||
`New query pool generated | count=${queries.length}`
|
||||
)
|
||||
|
||||
const relatedTerms = await queryCore.getBingRelatedTerms(query)
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Related terms resolved | baseQuery="${query}" | count=${relatedTerms.length}`
|
||||
)
|
||||
for (const query of queries) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Extra search | remaining=${missingPointsTotal} | query="${query}"`
|
||||
)
|
||||
|
||||
if (relatedTerms.length > 3) {
|
||||
for (const term of relatedTerms.slice(1, 3)) {
|
||||
searchCounters = await this.bingSearch(page, query, isMobile)
|
||||
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
||||
const newMissingPointsTotal = newMissingPoints.totalPoints
|
||||
|
||||
const rawGained = missingPointsTotal - newMissingPointsTotal
|
||||
const gainedPoints = Math.max(0, rawGained)
|
||||
|
||||
if (gainedPoints === 0) {
|
||||
stagnantLoop++
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Extra search | remaining=${missingPointsTotal} | query="${term}"`
|
||||
`No points gained ${stagnantLoop}/${stagnantLoopMax} | query="${query}" | remaining=${newMissingPointsTotal}`
|
||||
)
|
||||
} else {
|
||||
stagnantLoop = 0
|
||||
|
||||
searchCounters = await this.bingSearch(page, term, isMobile)
|
||||
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
||||
const newMissingPointsTotal = newMissingPoints.totalPoints
|
||||
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
||||
|
||||
// Points gained for THIS extra query only
|
||||
const rawGained = missingPointsTotal - newMissingPointsTotal
|
||||
const gainedPoints = Math.max(0, rawGained)
|
||||
totalGainedPoints += gainedPoints
|
||||
|
||||
if (gainedPoints === 0) {
|
||||
stagnantLoop++
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`No points gained for extra query ${stagnantLoop}/${stagnantLoopMax} | query="${term}" | remaining=${newMissingPointsTotal}`
|
||||
)
|
||||
} else {
|
||||
stagnantLoop = 0
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`gainedPoints=${gainedPoints} points | query="${query}" | remaining=${newMissingPointsTotal}`,
|
||||
'green'
|
||||
)
|
||||
}
|
||||
|
||||
// Update global user data
|
||||
const newBalance = Number(this.bot.userData.currentPoints ?? 0) + gainedPoints
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
||||
missingPointsTotal = newMissingPointsTotal
|
||||
|
||||
// Track for return value
|
||||
totalGainedPoints += gainedPoints
|
||||
if (missingPointsTotal === 0) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
'All required search points earned during extra searches'
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`gainedPoints=${gainedPoints} points | query="${term}" | remaining=${newMissingPointsTotal}`,
|
||||
'green'
|
||||
)
|
||||
}
|
||||
|
||||
// Update loop state
|
||||
missingPointsTotal = newMissingPointsTotal
|
||||
|
||||
// Completed
|
||||
if (missingPointsTotal === 0) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
'All required search points earned during extra searches'
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// Stuck again
|
||||
if (stagnantLoop > stagnantLoopMax) {
|
||||
this.bot.logger.warn(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Search did not gain points for ${stagnantLoopMax} extra iterations, aborting extra searches`
|
||||
)
|
||||
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Aborted extra searches | startBalance=${startBalance} | finalBalance=${finalBalance}`
|
||||
)
|
||||
return totalGainedPoints
|
||||
}
|
||||
if (stagnantLoop > stagnantLoopMax) {
|
||||
this.bot.logger.warn(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Search did not gain points for ${stagnantLoopMax} iterations, aborting extra searches`
|
||||
)
|
||||
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Aborted extra searches | startBalance=${startBalance} | finalBalance=${finalBalance}`
|
||||
)
|
||||
return totalGainedPoints
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,7 +267,6 @@ export class Search extends Workers {
|
||||
|
||||
this.searchCount++
|
||||
|
||||
// Page fill seems to get more sluggish over time
|
||||
if (this.searchCount % refreshThreshold === 0) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
@@ -271,7 +278,7 @@ export class Search extends Workers {
|
||||
|
||||
await searchPage.goto(this.bingHome)
|
||||
await searchPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
||||
await this.bot.browser.utils.tryDismissAllMessages(searchPage) // Not always the case but possible for new cookie headers
|
||||
await this.bot.browser.utils.tryDismissAllMessages(searchPage)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
@@ -402,11 +409,9 @@ export class Search extends Workers {
|
||||
await this.bot.utils.wait(this.bot.config.searchSettings.searchResultVisitTime)
|
||||
|
||||
if (isMobile) {
|
||||
// Mobile
|
||||
await page.goto(searchPageUrl)
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', 'Navigated back to search page')
|
||||
} else {
|
||||
// Desktop
|
||||
const newTab = await this.bot.browser.utils.getLatestTab(page)
|
||||
const newTabUrl = newTab.url()
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ export class SearchOnBing extends Workers {
|
||||
if (this.bot.config.searchOnBingLocalQueries) {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', 'Using local queries config file')
|
||||
|
||||
const data = fs.readFileSync(path.join(__dirname, '../queries.json'), 'utf8')
|
||||
const data = fs.readFileSync(path.join(__dirname, '../bing-search-activity-queries.json'), 'utf8')
|
||||
queries = JSON.parse(data)
|
||||
|
||||
this.bot.logger.debug(
|
||||
@@ -250,7 +250,7 @@ export class SearchOnBing extends Workers {
|
||||
// Fetch from the repo directly so the user doesn't need to redownload the script for the new activities
|
||||
const response = await this.bot.axios.request({
|
||||
method: 'GET',
|
||||
url: 'https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/v3/src/functions/queries.json'
|
||||
url: 'https://raw.githubusercontent.com/TheNetsky/Microsoft-Rewards-Script/refs/heads/v3/src/functions/bing-search-activity-queries.json'
|
||||
})
|
||||
queries = response.data
|
||||
|
||||
|
||||
582
src/functions/bing-search-activity-queries.json
Normal file
582
src/functions/bing-search-activity-queries.json
Normal file
@@ -0,0 +1,582 @@
|
||||
[
|
||||
{
|
||||
"title": "Houses near you",
|
||||
"queries": [
|
||||
"Houses near me",
|
||||
"Homes for sale near me",
|
||||
"Apartments near me",
|
||||
"Real estate listings near me",
|
||||
"Zillow homes near me",
|
||||
"houses for rent near me"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Feeling symptoms?",
|
||||
"queries": [
|
||||
"Rash on forearm",
|
||||
"Stuffy nose",
|
||||
"Tickling cough",
|
||||
"sore throat remedies",
|
||||
"headache and nausea causes",
|
||||
"fever symptoms adults"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Get your shopping done faster",
|
||||
"queries": [
|
||||
"Buy PS5",
|
||||
"Buy Xbox",
|
||||
"Chair deals",
|
||||
"wireless mouse deals",
|
||||
"best gaming headset price",
|
||||
"laptop deals",
|
||||
"buy office chair",
|
||||
"SSD deals"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Translate anything",
|
||||
"queries": [
|
||||
"Translate welcome home to Korean",
|
||||
"Translate welcome home to Japanese",
|
||||
"Translate goodbye to Japanese",
|
||||
"Translate good morning to Spanish",
|
||||
"Translate thank you to French",
|
||||
"Translate see you later to Italian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Search the lyrics of a song",
|
||||
"queries": [
|
||||
"Debarge rhythm of the night lyrics",
|
||||
"bohemian rhapsody lyrics",
|
||||
"hotel california lyrics",
|
||||
"blinding lights lyrics",
|
||||
"lose yourself lyrics",
|
||||
"smells like teen spirit lyrics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Let's watch that movie again!",
|
||||
"queries": [
|
||||
"Alien movie",
|
||||
"Aliens movie",
|
||||
"Alien 3 movie",
|
||||
"Predator movie",
|
||||
"Terminator movie",
|
||||
"John Wick movie",
|
||||
"Interstellar movie",
|
||||
"The Matrix movie"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Plan a quick getaway",
|
||||
"queries": [
|
||||
"Flights Amsterdam to Tokyo",
|
||||
"Flights New York to Tokyo",
|
||||
"cheap flights to paris",
|
||||
"flights amsterdam to rome",
|
||||
"last minute flight deals",
|
||||
"direct flights from amsterdam",
|
||||
"weekend getaway europe",
|
||||
"best time to visit tokyo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Discover open job roles",
|
||||
"queries": [
|
||||
"jobs at Microsoft",
|
||||
"Microsoft Job Openings",
|
||||
"Jobs near me",
|
||||
"jobs at Boeing worked",
|
||||
"software engineer jobs near me",
|
||||
"remote developer jobs",
|
||||
"IT jobs netherlands",
|
||||
"customer support jobs near me"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "You can track your package",
|
||||
"queries": [
|
||||
"USPS tracking",
|
||||
"UPS tracking",
|
||||
"DHL tracking",
|
||||
"FedEx tracking",
|
||||
"track my package",
|
||||
"international package tracking"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Find somewhere new to explore",
|
||||
"queries": [
|
||||
"Directions to Berlin",
|
||||
"Directions to Tokyo",
|
||||
"Directions to New York",
|
||||
"things to do in berlin",
|
||||
"tourist attractions tokyo",
|
||||
"best places to visit in new york",
|
||||
"hidden gems near me",
|
||||
"day trips near me"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Too tired to cook tonight?",
|
||||
"queries": [
|
||||
"KFC near me",
|
||||
"Burger King near me",
|
||||
"McDonalds near me",
|
||||
"pizza delivery near me",
|
||||
"restaurants open now",
|
||||
"best takeout near me",
|
||||
"quick dinner ideas",
|
||||
"easy dinner recipes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Quickly convert your money",
|
||||
"queries": [
|
||||
"convert 250 USD to yen",
|
||||
"convert 500 USD to yen",
|
||||
"usd to eur",
|
||||
"gbp to eur",
|
||||
"eur to jpy",
|
||||
"currency converter",
|
||||
"exchange rate today",
|
||||
"1000 yen to euro"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Learn to cook a new recipe",
|
||||
"queries": [
|
||||
"How to cook ratatouille",
|
||||
"How to cook lasagna",
|
||||
"easy pasta recipe",
|
||||
"how to make pancakes",
|
||||
"how to make fried rice",
|
||||
"simple chicken recipe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Find places to stay!",
|
||||
"queries": [
|
||||
"Hotels Berlin Germany",
|
||||
"Hotels Amsterdam Netherlands",
|
||||
"hotels in paris",
|
||||
"best hotels in tokyo",
|
||||
"cheap hotels london",
|
||||
"places to stay in barcelona",
|
||||
"hotel deals",
|
||||
"booking hotels near me"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "How's the economy?",
|
||||
"queries": [
|
||||
"sp 500",
|
||||
"nasdaq",
|
||||
"dow jones today",
|
||||
"inflation rate europe",
|
||||
"interest rates today",
|
||||
"stock market today",
|
||||
"economic news",
|
||||
"recession forecast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Who won?",
|
||||
"queries": [
|
||||
"braves score",
|
||||
"champions league results",
|
||||
"premier league results",
|
||||
"nba score",
|
||||
"formula 1 winner",
|
||||
"latest football scores",
|
||||
"ucl final winner",
|
||||
"world cup final result"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Gaming time",
|
||||
"queries": [
|
||||
"Overwatch video game",
|
||||
"Call of duty video game",
|
||||
"best games 2025",
|
||||
"top xbox games",
|
||||
"popular steam games",
|
||||
"new pc games",
|
||||
"game reviews",
|
||||
"best co-op games"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Expand your vocabulary",
|
||||
"queries": [
|
||||
"definition definition",
|
||||
"meaning of serendipity",
|
||||
"define nostalgia",
|
||||
"synonym for happy",
|
||||
"define eloquent",
|
||||
"what does epiphany mean",
|
||||
"word of the day",
|
||||
"define immaculate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "What time is it?",
|
||||
"queries": [
|
||||
"Japan time",
|
||||
"New York time",
|
||||
"time in london",
|
||||
"time in tokyo",
|
||||
"current time in amsterdam",
|
||||
"time in los angeles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Find deals on Bing",
|
||||
"queries": [
|
||||
"best laptop deals",
|
||||
"tech deals today",
|
||||
"wireless earbuds deals",
|
||||
"gaming chair deals",
|
||||
"discount codes electronics",
|
||||
"best amazon deals today",
|
||||
"smartphone deals",
|
||||
"ssd deals"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Prepare for the weather",
|
||||
"queries": [
|
||||
"weather tomorrow",
|
||||
"weekly weather forecast",
|
||||
"rain forecast today",
|
||||
"weather in amsterdam",
|
||||
"storm forecast europe",
|
||||
"uv index today",
|
||||
"temperature this weekend",
|
||||
"snow forecast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Track your delivery",
|
||||
"queries": [
|
||||
"track my package",
|
||||
"postnl track and trace",
|
||||
"dhl parcel tracking",
|
||||
"ups tracking",
|
||||
"fedex tracking",
|
||||
"usps tracking",
|
||||
"parcel tracking",
|
||||
"international package tracking"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Explore a new spot today",
|
||||
"queries": [
|
||||
"places to visit near me",
|
||||
"things to do near me",
|
||||
"hidden gems netherlands",
|
||||
"best museums near me",
|
||||
"parks near me",
|
||||
"tourist attractions nearby",
|
||||
"best cafes near me",
|
||||
"day trip ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Maisons près de chez vous",
|
||||
"queries": [
|
||||
"Maisons près de chez moi",
|
||||
"Maisons à vendre près de chez moi",
|
||||
"Appartements près de chez moi",
|
||||
"Annonces immobilières près de chez moi",
|
||||
"Maisons à louer près de chez moi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vous ressentez des symptômes ?",
|
||||
"queries": [
|
||||
"Éruption cutanée sur l'avant-bras",
|
||||
"Nez bouché",
|
||||
"Toux chatouilleuse",
|
||||
"mal de gorge remèdes",
|
||||
"maux de tête causes",
|
||||
"symptômes de la grippe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Faites vos achats plus vite",
|
||||
"queries": [
|
||||
"Acheter une PS5",
|
||||
"Acheter une Xbox",
|
||||
"Offres sur les chaises",
|
||||
"offres ordinateur portable",
|
||||
"meilleures offres casque",
|
||||
"acheter souris sans fil",
|
||||
"promotions ssd",
|
||||
"bons plans tech"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Traduisez tout !",
|
||||
"queries": [
|
||||
"Traduction bienvenue à la maison en coréen",
|
||||
"Traduction bienvenue à la maison en japonais",
|
||||
"Traduction au revoir en japonais",
|
||||
"Traduire bonjour en espagnol",
|
||||
"Traduire merci en anglais",
|
||||
"Traduire à plus tard en italien"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Rechercher paroles de chanson",
|
||||
"queries": [
|
||||
"Paroles de Debarge rhythm of the night",
|
||||
"paroles bohemian rhapsody",
|
||||
"paroles hotel california",
|
||||
"paroles blinding lights",
|
||||
"paroles lose yourself",
|
||||
"paroles smells like teen spirit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Et si nous regardions ce film une nouvelle fois?",
|
||||
"queries": [
|
||||
"Alien film",
|
||||
"Film Aliens",
|
||||
"Film Alien 3",
|
||||
"Film Predator",
|
||||
"Film Terminator",
|
||||
"Film John Wick",
|
||||
"Film Interstellar",
|
||||
"Film Matrix"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Planifiez une petite escapade",
|
||||
"queries": [
|
||||
"Vols Amsterdam-Tokyo",
|
||||
"Vols New York-Tokyo",
|
||||
"vols pas chers paris",
|
||||
"vols amsterdam rome",
|
||||
"offres vols dernière minute",
|
||||
"week-end en europe",
|
||||
"vols directs depuis amsterdam"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Consulter postes à pourvoir",
|
||||
"queries": [
|
||||
"emplois chez Microsoft",
|
||||
"Offres d'emploi Microsoft",
|
||||
"Emplois près de chez moi",
|
||||
"emplois chez Boeing",
|
||||
"emplois développeur à distance",
|
||||
"emplois informatique pays-bas",
|
||||
"offres d'emploi près de chez moi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vous pouvez suivre votre colis",
|
||||
"queries": [
|
||||
"Suivi Chronopost",
|
||||
"suivi colis",
|
||||
"suivi DHL",
|
||||
"suivi UPS",
|
||||
"suivi FedEx",
|
||||
"suivi international colis"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trouver un endroit à découvrir",
|
||||
"queries": [
|
||||
"Itinéraire vers Berlin",
|
||||
"Itinéraire vers Tokyo",
|
||||
"Itinéraire vers New York",
|
||||
"que faire à berlin",
|
||||
"attractions tokyo",
|
||||
"meilleurs endroits à visiter à new york",
|
||||
"endroits à visiter près de chez moi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trop fatigué pour cuisiner ce soir ?",
|
||||
"queries": [
|
||||
"KFC près de chez moi",
|
||||
"Burger King près de chez moi",
|
||||
"McDonalds près de chez moi",
|
||||
"livraison pizza près de chez moi",
|
||||
"restaurants ouverts maintenant",
|
||||
"idées dîner rapide",
|
||||
"quoi manger ce soir"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Convertissez rapidement votre argent",
|
||||
"queries": [
|
||||
"convertir 250 EUR en yen",
|
||||
"convertir 500 EUR en yen",
|
||||
"usd en eur",
|
||||
"gbp en eur",
|
||||
"eur en jpy",
|
||||
"convertisseur de devises",
|
||||
"taux de change aujourd'hui",
|
||||
"1000 yen en euro"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Apprenez à cuisiner une nouvelle recette",
|
||||
"queries": [
|
||||
"Comment faire cuire la ratatouille",
|
||||
"Comment faire cuire les lasagnes",
|
||||
"recette pâtes facile",
|
||||
"comment faire des crêpes",
|
||||
"recette riz sauté",
|
||||
"recette poulet simple"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trouvez des emplacements pour rester!",
|
||||
"queries": [
|
||||
"Hôtels Berlin Allemagne",
|
||||
"Hôtels Amsterdam Pays-Bas",
|
||||
"hôtels paris",
|
||||
"meilleurs hôtels tokyo",
|
||||
"hôtels pas chers londres",
|
||||
"hébergement barcelone",
|
||||
"offres hôtels",
|
||||
"hôtels près de chez moi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Comment se porte l'économie ?",
|
||||
"queries": [
|
||||
"CAC 40",
|
||||
"indice dax",
|
||||
"dow jones aujourd'hui",
|
||||
"inflation europe",
|
||||
"taux d'intérêt aujourd'hui",
|
||||
"marché boursier aujourd'hui",
|
||||
"actualités économie",
|
||||
"prévisions récession"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Qui a gagné ?",
|
||||
"queries": [
|
||||
"score du Paris Saint-Germain",
|
||||
"résultats ligue des champions",
|
||||
"résultats premier league",
|
||||
"score nba",
|
||||
"vainqueur formule 1",
|
||||
"derniers scores football",
|
||||
"vainqueur finale ldc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Temps de jeu",
|
||||
"queries": [
|
||||
"Jeu vidéo Overwatch",
|
||||
"Jeu vidéo Call of Duty",
|
||||
"meilleurs jeux 2025",
|
||||
"top jeux xbox",
|
||||
"jeux steam populaires",
|
||||
"nouveaux jeux pc",
|
||||
"avis jeux vidéo",
|
||||
"meilleurs jeux coop"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Enrichissez votre vocabulaire",
|
||||
"queries": [
|
||||
"definition definition",
|
||||
"signification sérendipité",
|
||||
"définir nostalgie",
|
||||
"synonyme heureux",
|
||||
"définir éloquent",
|
||||
"mot du jour",
|
||||
"que veut dire épiphanie"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Quelle heure est-il ?",
|
||||
"queries": [
|
||||
"Heure du Japon",
|
||||
"Heure de New York",
|
||||
"heure de londres",
|
||||
"heure de tokyo",
|
||||
"heure actuelle amsterdam",
|
||||
"heure de los angeles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vérifier la météo",
|
||||
"queries": [
|
||||
"Météo de Paris",
|
||||
"Météo de la France",
|
||||
"météo demain",
|
||||
"prévisions météo semaine",
|
||||
"météo amsterdam",
|
||||
"risque de pluie aujourd'hui"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Tenez-vous informé des sujets d'actualité",
|
||||
"queries": [
|
||||
"Augmentation Impots",
|
||||
"Mort célébrité",
|
||||
"actualités france",
|
||||
"actualité internationale",
|
||||
"dernières nouvelles économie",
|
||||
"news technologie"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Préparez-vous pour la météo",
|
||||
"queries": [
|
||||
"météo demain",
|
||||
"prévisions météo semaine",
|
||||
"météo amsterdam",
|
||||
"risque de pluie aujourd'hui",
|
||||
"indice uv aujourd'hui",
|
||||
"température ce week-end",
|
||||
"alerte tempête"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Suivez votre livraison",
|
||||
"queries": [
|
||||
"suivi colis",
|
||||
"postnl suivi colis",
|
||||
"suivi DHL colis",
|
||||
"suivi UPS",
|
||||
"suivi FedEx",
|
||||
"suivi international colis",
|
||||
"suivre ma livraison"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Trouvez des offres sur Bing",
|
||||
"queries": [
|
||||
"meilleures offres ordinateur portable",
|
||||
"bons plans tech",
|
||||
"promotions écouteurs",
|
||||
"offres chaise gamer",
|
||||
"codes promo électronique",
|
||||
"meilleures offres amazon aujourd'hui"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Explorez un nouvel endroit aujourd'hui",
|
||||
"queries": [
|
||||
"endroits à visiter près de chez moi",
|
||||
"que faire près de chez moi",
|
||||
"endroits insolites pays-bas",
|
||||
"meilleurs musées près de chez moi",
|
||||
"parcs près de chez moi",
|
||||
"attractions touristiques à proximité",
|
||||
"meilleurs cafés près de chez moi"
|
||||
]
|
||||
}
|
||||
]
|
||||
116
src/functions/search-queries.json
Normal file
116
src/functions/search-queries.json
Normal file
@@ -0,0 +1,116 @@
|
||||
[
|
||||
"weather tomorrow",
|
||||
"how to cook pasta",
|
||||
"best movies 2024",
|
||||
"latest tech news",
|
||||
"how tall is the eiffel tower",
|
||||
"easy dinner recipes",
|
||||
"what time is it in japan",
|
||||
"how does photosynthesis work",
|
||||
"best budget smartphones",
|
||||
"coffee vs espresso difference",
|
||||
"how to improve wifi signal",
|
||||
"popular netflix series",
|
||||
"how many calories in an apple",
|
||||
"world population today",
|
||||
"best free pc games",
|
||||
"how to clean a keyboard",
|
||||
"what is artificial intelligence",
|
||||
"simple home workouts",
|
||||
"how long do cats live",
|
||||
"famous paintings in museums",
|
||||
"how to boil eggs",
|
||||
"latest windows updates",
|
||||
"how to screenshot on windows",
|
||||
"best travel destinations europe",
|
||||
"what is cloud computing",
|
||||
"how to save money monthly",
|
||||
"best youtube channels",
|
||||
"how fast is light",
|
||||
"how to learn programming",
|
||||
"popular board games",
|
||||
"how to make pancakes",
|
||||
"capital cities of europe",
|
||||
"how does a vpn work",
|
||||
"best productivity apps",
|
||||
"how to grow plants indoors",
|
||||
"difference between hdd and ssd",
|
||||
"how to fix slow computer",
|
||||
"most streamed songs",
|
||||
"how to tie a tie",
|
||||
"what causes rain",
|
||||
"best laptops for students",
|
||||
"how to reset router",
|
||||
"healthy breakfast ideas",
|
||||
"how many continents are there",
|
||||
"latest smartphone features",
|
||||
"how to meditate beginners",
|
||||
"what is renewable energy",
|
||||
"best pc accessories",
|
||||
"how to clean glasses",
|
||||
"famous landmarks worldwide",
|
||||
"how to make coffee at home",
|
||||
"what is machine learning",
|
||||
"best programming languages",
|
||||
"how to backup files",
|
||||
"how does bluetooth work",
|
||||
"top video games right now",
|
||||
"how to improve sleep quality",
|
||||
"what is cryptocurrency",
|
||||
"easy lunch ideas",
|
||||
"how to check internet speed",
|
||||
"best noise cancelling headphones",
|
||||
"how to take screenshots on mac",
|
||||
"what is the milky way",
|
||||
"how to organize files",
|
||||
"popular mobile apps",
|
||||
"how to learn faster",
|
||||
"how does gps work",
|
||||
"best free antivirus",
|
||||
"how to clean a monitor",
|
||||
"what is an electric car",
|
||||
"simple math tricks",
|
||||
"how to update drivers",
|
||||
"famous scientists",
|
||||
"how to cook rice",
|
||||
"what is the tallest mountain",
|
||||
"best tv shows all time",
|
||||
"how to improve typing speed",
|
||||
"how does solar power work",
|
||||
"easy dessert recipes",
|
||||
"how to fix bluetooth issues",
|
||||
"what is the internet",
|
||||
"best pc keyboards",
|
||||
"how to stay focused",
|
||||
"popular science facts",
|
||||
"how to convert files to pdf",
|
||||
"how long does it take to sleep",
|
||||
"best travel tips",
|
||||
"how to clean headphones",
|
||||
"what is open source software",
|
||||
"how to manage time better",
|
||||
"latest gaming news",
|
||||
"how to check laptop temperature",
|
||||
"what is a firewall",
|
||||
"easy meal prep ideas",
|
||||
"how to reduce eye strain",
|
||||
"best budget headphones",
|
||||
"how does email work",
|
||||
"what is virtual reality",
|
||||
"how to compress files",
|
||||
"popular programming tools",
|
||||
"how to improve concentration",
|
||||
"how to make smoothies",
|
||||
"best desk setup ideas",
|
||||
"how to block ads",
|
||||
"what is 5g technology",
|
||||
"how to clean a mouse",
|
||||
"famous world wonders",
|
||||
"how to improve battery life",
|
||||
"best cloud storage services",
|
||||
"how to learn a new language",
|
||||
"what is dark mode",
|
||||
"how to clear browser cache",
|
||||
"popular tech podcasts",
|
||||
"how to stay motivated"
|
||||
]
|
||||
34
src/index.ts
34
src/index.ts
@@ -12,6 +12,7 @@ import BrowserUtils from './browser/BrowserUtils'
|
||||
import { IpcLog, Logger } from './logging/Logger'
|
||||
import Utils from './util/Utils'
|
||||
import { loadAccounts, loadConfig } from './util/Load'
|
||||
import { checkNodeVersion } from './util/Validator'
|
||||
|
||||
import { Login } from './browser/auth/Login'
|
||||
import { Workers } from './functions/Workers'
|
||||
@@ -27,7 +28,7 @@ import type { AppDashboardData } from './interface/AppDashBoardData'
|
||||
|
||||
interface ExecutionContext {
|
||||
isMobile: boolean
|
||||
accountEmail: string
|
||||
account: Account
|
||||
}
|
||||
|
||||
interface BrowserSession {
|
||||
@@ -50,7 +51,7 @@ const executionContext = new AsyncLocalStorage<ExecutionContext>()
|
||||
export function getCurrentContext(): ExecutionContext {
|
||||
const context = executionContext.getStore()
|
||||
if (!context) {
|
||||
return { isMobile: false, accountEmail: 'unknown' }
|
||||
return { isMobile: false, account: {} as any }
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -62,6 +63,7 @@ async function flushAllWebhooks(timeoutMs = 5000): Promise<void> {
|
||||
interface UserData {
|
||||
userName: string
|
||||
geoLocale: string
|
||||
langCode: string
|
||||
initialPoints: number
|
||||
currentPoints: number
|
||||
gainedPoints: number
|
||||
@@ -99,7 +101,8 @@ export class MicrosoftRewardsBot {
|
||||
constructor() {
|
||||
this.userData = {
|
||||
userName: '',
|
||||
geoLocale: '',
|
||||
geoLocale: 'US',
|
||||
langCode: 'en',
|
||||
initialPoints: 0,
|
||||
currentPoints: 0,
|
||||
gainedPoints: 0
|
||||
@@ -134,7 +137,7 @@ export class MicrosoftRewardsBot {
|
||||
this.logger.info(
|
||||
'main',
|
||||
'RUN-START',
|
||||
`Starting Microsoft Rewards bot| v${pkg.version} | Accounts: ${totalAccounts} | Clusters: ${this.config.clusters}`
|
||||
`Starting Microsoft Rewards Script | v${pkg.version} | Accounts: ${totalAccounts} | Clusters: ${this.config.clusters}`
|
||||
)
|
||||
|
||||
if (this.config.clusters > 1) {
|
||||
@@ -185,11 +188,14 @@ export class MicrosoftRewardsBot {
|
||||
|
||||
const onWorkerDone = async (label: 'exit' | 'disconnect', worker: Worker, code?: number): Promise<void> => {
|
||||
const { pid } = worker.process
|
||||
|
||||
if (!pid || this.exitedWorkers.includes(pid)) return
|
||||
else this.exitedWorkers.push(pid)
|
||||
|
||||
this.activeWorkers -= 1
|
||||
|
||||
if (!pid || this.exitedWorkers.includes(pid)) {
|
||||
return
|
||||
} else {
|
||||
this.exitedWorkers.push(pid)
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
'main',
|
||||
`CLUSTER-WORKER-${label.toUpperCase()}`,
|
||||
@@ -233,6 +239,7 @@ export class MicrosoftRewardsBot {
|
||||
if (process.send) {
|
||||
process.send({ __stats: stats })
|
||||
}
|
||||
|
||||
process.disconnect()
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -355,14 +362,14 @@ export class MicrosoftRewardsBot {
|
||||
let mobileContextClosed = false
|
||||
|
||||
try {
|
||||
return await executionContext.run({ isMobile: true, accountEmail }, async () => {
|
||||
mobileSession = await this.browserFactory.createBrowser(account.proxy, accountEmail)
|
||||
return await executionContext.run({ isMobile: true, account }, async () => {
|
||||
mobileSession = await this.browserFactory.createBrowser(account)
|
||||
const initialContext: BrowserContext = mobileSession.context
|
||||
this.mainMobilePage = await initialContext.newPage()
|
||||
|
||||
this.logger.info('main', 'BROWSER', `Mobile Browser started | ${accountEmail}`)
|
||||
|
||||
await this.login.login(this.mainMobilePage, accountEmail, account.password, account.totp)
|
||||
await this.login.login(this.mainMobilePage, account)
|
||||
|
||||
try {
|
||||
this.accessToken = await this.login.getAppAccessToken(this.mainMobilePage, accountEmail)
|
||||
@@ -410,6 +417,7 @@ export class MicrosoftRewardsBot {
|
||||
|
||||
if (this.config.workers.doAppPromotions) await this.workers.doAppPromotions(appData)
|
||||
if (this.config.workers.doDailySet) await this.workers.doDailySet(data, this.mainMobilePage)
|
||||
if (this.config.workers.doSpecialPromotions) await this.workers.doSpecialPromotions(data)
|
||||
if (this.config.workers.doMorePromotions) await this.workers.doMorePromotions(data, this.mainMobilePage)
|
||||
if (this.config.workers.doDailyCheckIn) await this.activities.doDailyCheckIn()
|
||||
if (this.config.workers.doReadToEarn) await this.activities.doReadToEarn()
|
||||
@@ -448,7 +456,7 @@ export class MicrosoftRewardsBot {
|
||||
} finally {
|
||||
if (mobileSession && !mobileContextClosed) {
|
||||
try {
|
||||
await executionContext.run({ isMobile: true, accountEmail }, async () => {
|
||||
await executionContext.run({ isMobile: true, account }, async () => {
|
||||
await this.browser.func.closeBrowser(mobileSession!.context, accountEmail)
|
||||
})
|
||||
} catch {}
|
||||
@@ -460,6 +468,8 @@ export class MicrosoftRewardsBot {
|
||||
export { executionContext }
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// Check before doing anything
|
||||
checkNodeVersion()
|
||||
const rewardsBot = new MicrosoftRewardsBot()
|
||||
|
||||
process.on('beforeExit', () => {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export interface Account {
|
||||
email: string
|
||||
password: string
|
||||
totp?: string
|
||||
totpSecret?: string
|
||||
recoveryEmail: string
|
||||
geoLocale: 'auto' | string
|
||||
langCode: 'en' | string
|
||||
proxy: AccountProxy
|
||||
saveFingerprint: ConfigSaveFingerprint
|
||||
}
|
||||
|
||||
export interface AccountProxy {
|
||||
@@ -13,3 +16,8 @@ export interface AccountProxy {
|
||||
password: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface ConfigSaveFingerprint {
|
||||
mobile: boolean
|
||||
desktop: boolean
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface Config {
|
||||
runOnZeroPoints: boolean
|
||||
clusters: number
|
||||
errorDiagnostics: boolean
|
||||
saveFingerprint: ConfigSaveFingerprint
|
||||
workers: ConfigWorkers
|
||||
searchOnBingLocalQueries: boolean
|
||||
globalTimeout: number | string
|
||||
@@ -16,15 +15,13 @@ export interface Config {
|
||||
webhook: ConfigWebhook
|
||||
}
|
||||
|
||||
export interface ConfigSaveFingerprint {
|
||||
mobile: boolean
|
||||
desktop: boolean
|
||||
}
|
||||
export type QueryEngine = 'google' | 'wikipedia' | 'reddit' | 'local'
|
||||
|
||||
export interface ConfigSearchSettings {
|
||||
scrollRandomResults: boolean
|
||||
clickRandomResults: boolean
|
||||
parallelSearching: boolean
|
||||
queryEngines: QueryEngine[]
|
||||
searchResultVisitTime: number | string
|
||||
searchDelay: ConfigDelay
|
||||
readDelay: ConfigDelay
|
||||
@@ -41,6 +38,7 @@ export interface ConfigProxy {
|
||||
|
||||
export interface ConfigWorkers {
|
||||
doDailySet: boolean
|
||||
doSpecialPromotions: boolean
|
||||
doMorePromotions: boolean
|
||||
doPunchCards: boolean
|
||||
doAppPromotions: boolean
|
||||
|
||||
@@ -94,3 +94,23 @@ export interface BingTrendingImage {
|
||||
export interface BingTrendingQuery {
|
||||
text: string
|
||||
}
|
||||
|
||||
export interface WikipediaTopResponse {
|
||||
items: Array<{
|
||||
articles: Array<{
|
||||
article: string
|
||||
views: number
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
export interface RedditListing {
|
||||
data: {
|
||||
children: Array<{
|
||||
data: {
|
||||
title: string
|
||||
over_18: boolean
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,10 @@ export class Logger {
|
||||
const now = new Date().toLocaleString()
|
||||
const formatted = formatMessage(message)
|
||||
|
||||
const userName = this.bot.userData.userName ? this.bot.userData.userName : 'MAIN'
|
||||
|
||||
const levelTag = level.toUpperCase()
|
||||
const cleanMsg = `[${now}] [${this.bot.userData.userName}] [${levelTag}] ${platformText(
|
||||
isMobile
|
||||
)} [${title}] ${formatted}`
|
||||
const cleanMsg = `[${now}] [${userName}] [${levelTag}] ${platformText(isMobile)} [${title}] ${formatted}`
|
||||
|
||||
const config = this.bot.config
|
||||
|
||||
@@ -85,7 +85,7 @@ export class Logger {
|
||||
}
|
||||
|
||||
const badge = platformBadge(isMobile)
|
||||
const consoleStr = `[${now}] [${this.bot.userData.userName}] [${levelTag}] ${badge} [${title}] ${formatted}`
|
||||
const consoleStr = `[${now}] [${userName}] [${levelTag}] ${badge} [${title}] ${formatted}`
|
||||
|
||||
let logColor: ColorKey | undefined = color
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import axiosRetry from 'axios-retry'
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
import { URL } from 'url'
|
||||
import type { AccountProxy } from '../interface/Account'
|
||||
|
||||
@@ -36,7 +37,9 @@ class AxiosClient {
|
||||
})
|
||||
}
|
||||
|
||||
private getAgentForProxy(proxyConfig: AccountProxy): HttpProxyAgent<string> | HttpsProxyAgent<string> {
|
||||
private getAgentForProxy(
|
||||
proxyConfig: AccountProxy
|
||||
): HttpProxyAgent<string> | HttpsProxyAgent<string> | SocksProxyAgent {
|
||||
const { url: baseUrl, port, username, password } = proxyConfig
|
||||
|
||||
let urlObj: URL
|
||||
@@ -67,8 +70,11 @@ class AxiosClient {
|
||||
return new HttpProxyAgent(proxyUrl)
|
||||
case 'https:':
|
||||
return new HttpsProxyAgent(proxyUrl)
|
||||
case 'socks4:':
|
||||
case 'socks5:':
|
||||
return new SocksProxyAgent(proxyUrl)
|
||||
default:
|
||||
throw new Error(`Unsupported proxy protocol: ${protocol}. Only HTTP(S) is supported!`)
|
||||
throw new Error(`Unsupported proxy protocol: ${protocol}. Only HTTP(S) and SOCKS4/5 are supported!`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import type { BrowserFingerprintWithHeaders } from 'fingerprint-generator'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import type { Account } from '../interface/Account'
|
||||
import type { Config, ConfigSaveFingerprint } from '../interface/Config'
|
||||
import type { Account, ConfigSaveFingerprint } from '../interface/Account'
|
||||
import type { Config } from '../interface/Config'
|
||||
import { validateAccounts, validateConfig } from './Validator'
|
||||
|
||||
let configCache: Config
|
||||
|
||||
@@ -18,8 +19,11 @@ export function loadAccounts(): Account[] {
|
||||
|
||||
const accountDir = path.join(__dirname, '../', file)
|
||||
const accounts = fs.readFileSync(accountDir, 'utf-8')
|
||||
const accountsData = JSON.parse(accounts)
|
||||
|
||||
return JSON.parse(accounts)
|
||||
validateAccounts(accountsData)
|
||||
|
||||
return accountsData
|
||||
} catch (error) {
|
||||
throw new Error(error as string)
|
||||
}
|
||||
@@ -35,6 +39,8 @@ export function loadConfig(): Config {
|
||||
const config = fs.readFileSync(configDir, 'utf-8')
|
||||
|
||||
const configData = JSON.parse(config)
|
||||
validateConfig(configData)
|
||||
|
||||
configCache = configData
|
||||
|
||||
return configData
|
||||
|
||||
@@ -21,10 +21,19 @@ export default class Util {
|
||||
}
|
||||
|
||||
shuffleArray<T>(array: T[]): T[] {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
|
||||
const a = array[i]
|
||||
const b = array[j]
|
||||
|
||||
if (a === undefined || b === undefined) continue
|
||||
|
||||
array[i] = b
|
||||
array[j] = a
|
||||
}
|
||||
|
||||
return array
|
||||
.map(value => ({ value, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ value }) => value)
|
||||
}
|
||||
|
||||
randomNumber(min: number, max: number): number {
|
||||
|
||||
131
src/util/Validator.ts
Normal file
131
src/util/Validator.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { z } from 'zod'
|
||||
import semver from 'semver'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
import { Config } from '../interface/Config'
|
||||
import { Account } from '../interface/Account'
|
||||
|
||||
const NumberOrString = z.union([z.number(), z.string()])
|
||||
|
||||
const LogFilterSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
mode: z.enum(['whitelist', 'blacklist']),
|
||||
levels: z.array(z.enum(['debug', 'info', 'warn', 'error'])).optional(),
|
||||
keywords: z.array(z.string()).optional(),
|
||||
regexPatterns: z.array(z.string()).optional()
|
||||
})
|
||||
|
||||
const DelaySchema = z.object({
|
||||
min: NumberOrString,
|
||||
max: NumberOrString
|
||||
})
|
||||
|
||||
const QueryEngineSchema = z.enum(['google', 'wikipedia', 'reddit', 'local'])
|
||||
|
||||
// Webhook
|
||||
const WebhookSchema = z.object({
|
||||
discord: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
url: z.string()
|
||||
})
|
||||
.optional(),
|
||||
ntfy: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
url: z.string(),
|
||||
topic: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
priority: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5)]).optional()
|
||||
})
|
||||
.optional(),
|
||||
webhookLogFilter: LogFilterSchema
|
||||
})
|
||||
|
||||
// Config
|
||||
export const ConfigSchema = z.object({
|
||||
baseURL: z.string(),
|
||||
sessionPath: z.string(),
|
||||
headless: z.boolean(),
|
||||
runOnZeroPoints: z.boolean(),
|
||||
clusters: z.number().int().nonnegative(),
|
||||
errorDiagnostics: z.boolean(),
|
||||
workers: z.object({
|
||||
doDailySet: z.boolean(),
|
||||
doSpecialPromotions: z.boolean(),
|
||||
doMorePromotions: z.boolean(),
|
||||
doPunchCards: z.boolean(),
|
||||
doAppPromotions: z.boolean(),
|
||||
doDesktopSearch: z.boolean(),
|
||||
doMobileSearch: z.boolean(),
|
||||
doDailyCheckIn: z.boolean(),
|
||||
doReadToEarn: z.boolean()
|
||||
}),
|
||||
searchOnBingLocalQueries: z.boolean(),
|
||||
globalTimeout: NumberOrString,
|
||||
searchSettings: z.object({
|
||||
scrollRandomResults: z.boolean(),
|
||||
clickRandomResults: z.boolean(),
|
||||
parallelSearching: z.boolean(),
|
||||
queryEngines: z.array(QueryEngineSchema),
|
||||
searchResultVisitTime: NumberOrString,
|
||||
searchDelay: DelaySchema,
|
||||
readDelay: DelaySchema
|
||||
}),
|
||||
debugLogs: z.boolean(),
|
||||
proxy: z.object({
|
||||
queryEngine: z.boolean()
|
||||
}),
|
||||
consoleLogFilter: LogFilterSchema,
|
||||
webhook: WebhookSchema
|
||||
})
|
||||
|
||||
// Account
|
||||
export const AccountSchema = z.object({
|
||||
email: z.string(),
|
||||
password: z.string(),
|
||||
totpSecret: z.string().optional(),
|
||||
recoveryEmail: z.string(),
|
||||
geoLocale: z.string(),
|
||||
langCode: z.string(),
|
||||
proxy: z.object({
|
||||
proxyAxios: z.boolean(),
|
||||
url: z.string(),
|
||||
port: z.number(),
|
||||
password: z.string(),
|
||||
username: z.string()
|
||||
}),
|
||||
saveFingerprint: z.object({
|
||||
mobile: z.boolean(),
|
||||
desktop: z.boolean()
|
||||
})
|
||||
})
|
||||
|
||||
export function validateConfig(data: unknown): Config {
|
||||
return ConfigSchema.parse(data) as Config
|
||||
}
|
||||
|
||||
export function validateAccounts(data: unknown): Account[] {
|
||||
return z.array(AccountSchema).parse(data)
|
||||
}
|
||||
|
||||
export function checkNodeVersion(): void {
|
||||
try {
|
||||
const requiredVersion = pkg.engines?.node
|
||||
|
||||
if (!requiredVersion) {
|
||||
console.warn('No Node.js version requirement found in package.json "engines" field.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!semver.satisfies(process.version, requiredVersion)) {
|
||||
console.error(`Current Node.js version ${process.version} does not satisfy requirement: ${requiredVersion}`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to validate Node.js version:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,12 @@
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
|
||||
"types": ["node"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
// Keep explicit typeRoots to ensure resolution in environments that don't auto-detect before full install.
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
@@ -65,6 +69,14 @@
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/accounts.json", "src/config.json", "src/functions/queries.json"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/accounts.json",
|
||||
"src/config.json",
|
||||
"src/functions/bing-search-activity-queries.json",
|
||||
"src/functions/search-queries.json"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user