diff --git a/.eslintrc.js b/.eslintrc.js index f8b5e61..b221152 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,6 +28,10 @@ module.exports = { 'error', 'never' ], + '@typescript-eslint/no-explicit-any': + ['warn', { + fixToUnknown: true // This line is optional and only relevant if you are using TypeScript + }], 'comma-dangle': 'off', '@typescript-eslint/comma-dangle': 'error', 'prefer-arrow-callback': 'error' diff --git a/package.json b/package.json index e72c5fd..d6e3131 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "microsoft-rewards-script", - "version": "1.4.7", + "version": "1.4.8", "description": "Automatically do tasks for Microsoft Rewards but in TS!", "main": "index.js", "engines": { @@ -26,17 +26,17 @@ "author": "Netsky", "license": "ISC", "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.11.0", + "@typescript-eslint/eslint-plugin": "^7.17.0", "eslint": "^8.57.0", "eslint-plugin-modules-newline": "^0.0.6", - "typescript": "^5.4.5" + "typescript": "^5.5.4" }, "dependencies": { - "axios": "^1.7.2", - "cheerio": "^1.0.0-rc.12", - "fingerprint-generator": "^2.1.51", - "fingerprint-injector": "^2.1.51", - "playwright": "^1.44.1", + "axios": "^1.7.4", + "cheerio": "^1.0.0", + "fingerprint-generator": "^2.1.54", + "fingerprint-injector": "^2.1.54", + "playwright": "^1.46.1", "ts-node": "^10.9.2" } } diff --git a/src/functions/Login.ts b/src/functions/Login.ts index a9142ef..aaab206 100644 --- a/src/functions/Login.ts +++ b/src/functions/Login.ts @@ -56,78 +56,110 @@ export class Login { private async execLogin(page: Page, email: string, password: string) { try { - // Enter email - await page.fill('#i0116', email) - await page.click('#idSIButton9') - - this.bot.log('LOGIN', 'Email entered successfully') - - try { - // Enter password - await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 }) - await this.bot.utils.wait(2000) - - await page.fill('#i0118', password) - await page.click('#idSIButton9') - - this.bot.log('LOGIN', 'Password entered successfully') - - // When erroring at this stage it means a 2FA code is required - } catch (error) { - // this.bot.log('LOGIN', 'App approval required because you have passwordless enabled.'); - - let numberToPress: string | null = await (await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })).textContent(); - - if (!numberToPress) { - await page.click('button[aria-describedby="confirmSendTitle"]'); - await this.bot.utils.wait(2000); - numberToPress = await (await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })).textContent(); - } - - if (numberToPress) { - while (true) { - try { - this.bot.log('LOGIN', 'Press the number below on your Authenticator app to approve the login'); - this.bot.log('LOGIN', 'If you press the wrong number or the "Deny" button, try again in 60 seconds'); - this.bot.log('LOGIN', 'Number to press: ' + numberToPress); - await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 }) - break; - } catch (error) { - this.bot.log('LOGIN', 'The code is expired. Trying to get the new code...'); - (await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 5000 })).click(); - numberToPress = await (await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })).textContent(); - } - } - this.bot.log('LOGIN', 'Login successfully approved!'); - } else { - this.bot.log('LOGIN', '2FA code required') - // Wait for user input - const code = await new Promise((resolve) => { - rl.question('Enter 2FA code:\n', (input) => { - rl.close() - resolve(input) - }) - }) - - await page.fill('input[name="otc"]', code) - await page.keyboard.press('Enter') - - } - } - - } catch (error) { - this.bot.log('LOGIN', 'An error occurred:' + error, 'error') + await this.enterEmail(page, email) + await this.enterPassword(page, password) + await this.checkLoggedIn(page) + } catch (error: any) { + this.bot.log('LOGIN', 'An error occurred: ' + error.message, 'error') } + } - const currentURL = new URL(page.url()) + private async enterEmail(page: Page, email: string) { + await page.fill('#i0116', email) + await page.click('#idSIButton9') + this.bot.log('LOGIN', 'Email entered successfully') + } - while (currentURL.pathname !== '/' || currentURL.hostname !== 'rewards.bing.com') { + private async enterPassword(page: Page, password: string) { + try { + await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 }) + await this.bot.utils.wait(2000) + await page.fill('#i0118', password) + await page.click('#idSIButton9') + this.bot.log('LOGIN', 'Password entered successfully') + } catch { + this.bot.log('LOGIN', 'Password entry failed or 2FA required') + await this.handle2FA(page) + } + } + + private async handle2FA(page: Page) { + try { + const numberToPress = await this.get2FACode(page) + if (numberToPress) { + // Authentictor App verification + await this.authAppVerification(page, numberToPress) + } else { + // SMS verification + await this.authSMSVerification(page) + } + } catch (error: any) { + this.bot.log('LOGIN', `2FA handling failed: ${error.message}`) + } + } + + private async get2FACode(page: Page): Promise { + try { + const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 }) + return await element.textContent() + } catch { + await page.click('button[aria-describedby="confirmSendTitle"]') + await this.bot.utils.wait(2000) + const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 }) + return await element.textContent() + } + } + + private async authAppVerification(page: Page, numberToPress: string | null) { + // eslint-disable-next-line no-constant-condition + while (true) { + try { + this.bot.log('LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`) + this.bot.log('LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds') + + await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 }) + + this.bot.log('LOGIN', 'Login successfully approved!') + break + } catch { + this.bot.log('LOGIN', 'The code is expired. Trying to get a new code...') + await page.click('button[aria-describedby="pushNotificationsTitle errorDescription"]') + numberToPress = await this.get2FACode(page) + } + } + } + + private async authSMSVerification(page: Page) { + this.bot.log('LOGIN', 'SMS 2FA code required. Waiting for user input...') + + const code = await new Promise((resolve) => { + rl.question('Enter 2FA code:\n', (input) => { + rl.close() + resolve(input) + }) + }) + + await page.fill('input[name="otc"]', code) + await page.keyboard.press('Enter') + this.bot.log('LOGIN', '2FA code entered successfully') + } + + private async checkLoggedIn(page: Page) { + const targetHostname = 'rewards.bing.com' + const targetPathname = '/' + + // eslint-disable-next-line no-constant-condition + while (true) { await this.bot.browser.utils.tryDismissAllMessages(page) - currentURL.href = page.url() + const currentURL = new URL(page.url()) + if (currentURL.hostname === targetHostname && currentURL.pathname === targetPathname) { + break + } } // Wait for login to complete await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }) + this.bot.log('LOGIN', 'Successfully logged into the rewards portal') } private async checkBingLogin(page: Page): Promise { diff --git a/src/functions/Workers.ts b/src/functions/Workers.ts index 43643ef..9b5b543 100644 --- a/src/functions/Workers.ts +++ b/src/functions/Workers.ts @@ -87,7 +87,7 @@ export class Workers { morePromotions.push(data.promotionalItem as unknown as MorePromotion) } - const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && !x.attributes.is_unlocked) ?? []; + const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && !x.attributes.is_unlocked) ?? [] if (!activitiesUncompleted.length) { this.bot.log('MORE-PROMOTIONS', 'All "More Promotion" items have already been completed') @@ -132,13 +132,13 @@ export class Workers { } - let selector = `[data-bi-id="${activity.offerId}"]` + let selector = `[data-bi-id^="${activity.offerId}"]` if (punchCard) { selector = await this.bot.browser.func.getPunchCardActivity(activityPage, activity) } else if (activity.name.toLowerCase().includes('membercenter')) { - selector = `[data-bi-id="${activity.name}"]` + selector = `[data-bi-id^="${activity.name}"]` } // Click element, it will be opened in a new tab diff --git a/src/functions/activities/Search.ts b/src/functions/activities/Search.ts index e30e72d..f018115 100644 --- a/src/functions/activities/Search.ts +++ b/src/functions/activities/Search.ts @@ -42,6 +42,8 @@ export class Search extends Workers { // Mobile search doesn't seem to like related queries? googleSearchQueries.forEach(x => { this.bot.isMobile ? queries.push(x.topic) : queries.push(x.topic, ...x.related) }) + await this.bot.browser.utils.tryDismissBingCookieBanner(page) + // Loop over Google search queries for (let i = 0; i < queries.length; i++) { const query = queries[i] as string