mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-18 14:03:58 +00:00
v3.1.0 initial
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user