mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-23 08:21:04 +00:00
v3 init
Based of v3.0.0b10.
This commit is contained in:
86
src/browser/auth/methods/EmailLogin.ts
Normal file
86
src/browser/auth/methods/EmailLogin.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { Page } from 'patchright'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
|
||||
export class EmailLogin {
|
||||
private submitButton = 'button[type="submit"]'
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
|
||||
async enterEmail(page: Page, email: string): Promise<'ok' | 'error'> {
|
||||
try {
|
||||
const emailInputSelector = 'input[type="email"]'
|
||||
const emailField = await page
|
||||
.waitForSelector(emailInputSelector, { state: 'visible', timeout: 1000 })
|
||||
.catch(() => {})
|
||||
if (!emailField) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-ENTER-EMAIL', 'Email field not found')
|
||||
return 'error'
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
|
||||
const prefilledEmail = await page
|
||||
.waitForSelector('#userDisplayName', { state: 'visible', timeout: 1000 })
|
||||
.catch(() => {})
|
||||
if (!prefilledEmail) {
|
||||
await page.fill(emailInputSelector, '').catch(() => {})
|
||||
await this.bot.utils.wait(500)
|
||||
await page.fill(emailInputSelector, email).catch(() => {})
|
||||
await this.bot.utils.wait(1000)
|
||||
} else {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-ENTER-EMAIL', 'Email prefilled')
|
||||
}
|
||||
|
||||
await page.waitForSelector(this.submitButton, { state: 'visible', timeout: 2000 }).catch(() => {})
|
||||
|
||||
await this.bot.browser.utils.ghostClick(page, this.submitButton)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-ENTER-EMAIL', 'Email submitted')
|
||||
|
||||
return 'ok'
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-ENTER-EMAIL',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
|
||||
async enterPassword(page: Page, password: string): Promise<'ok' | 'needs-2fa' | 'error'> {
|
||||
try {
|
||||
const passwordInputSelector = 'input[type="password"]'
|
||||
const passwordField = await page
|
||||
.waitForSelector(passwordInputSelector, { state: 'visible', timeout: 1000 })
|
||||
.catch(() => {})
|
||||
if (!passwordField) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-ENTER-PASSWORD', 'Password field not found')
|
||||
return 'error'
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
await page.fill(passwordInputSelector, '').catch(() => {})
|
||||
await this.bot.utils.wait(500)
|
||||
await page.fill(passwordInputSelector, password).catch(() => {})
|
||||
await this.bot.utils.wait(1000)
|
||||
|
||||
const submitButton = await page
|
||||
.waitForSelector(this.submitButton, { state: 'visible', timeout: 2000 })
|
||||
.catch(() => null)
|
||||
|
||||
if (submitButton) {
|
||||
await this.bot.browser.utils.ghostClick(page, this.submitButton)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-ENTER-PASSWORD', 'Password submitted')
|
||||
}
|
||||
|
||||
return 'ok'
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-ENTER-PASSWORD',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/browser/auth/methods/MobileAccessLogin.ts
Normal file
88
src/browser/auth/methods/MobileAccessLogin.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { Page } from 'patchright'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { URLSearchParams } from 'url'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
|
||||
export class MobileAccessLogin {
|
||||
private clientId = '0000000040170455'
|
||||
private authUrl = 'https://login.live.com/oauth20_authorize.srf'
|
||||
private redirectUrl = 'https://login.live.com/oauth20_desktop.srf'
|
||||
private tokenUrl = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
|
||||
private scope = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
|
||||
private maxTimeout = 180_000 // 3min
|
||||
|
||||
constructor(
|
||||
private bot: MicrosoftRewardsBot,
|
||||
private page: Page
|
||||
) {}
|
||||
|
||||
async get(email: string): Promise<string> {
|
||||
try {
|
||||
const authorizeUrl = new URL(this.authUrl)
|
||||
authorizeUrl.searchParams.append('response_type', 'code')
|
||||
authorizeUrl.searchParams.append('client_id', this.clientId)
|
||||
authorizeUrl.searchParams.append('redirect_uri', this.redirectUrl)
|
||||
authorizeUrl.searchParams.append('scope', this.scope)
|
||||
authorizeUrl.searchParams.append('state', randomBytes(16).toString('hex'))
|
||||
authorizeUrl.searchParams.append('access_type', 'offline_access')
|
||||
authorizeUrl.searchParams.append('login_hint', email)
|
||||
|
||||
await this.bot.browser.utils.disableFido(this.page)
|
||||
|
||||
await this.page.goto(authorizeUrl.href).catch(() => {})
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Waiting for mobile OAuth code...')
|
||||
const start = Date.now()
|
||||
let code = ''
|
||||
|
||||
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
|
||||
}
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'Timed out waiting for OAuth code')
|
||||
return ''
|
||||
}
|
||||
|
||||
const data = new URLSearchParams()
|
||||
data.append('grant_type', 'authorization_code')
|
||||
data.append('client_id', this.clientId)
|
||||
data.append('code', code)
|
||||
data.append('redirect_uri', this.redirectUrl)
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
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')
|
||||
return ''
|
||||
}
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Mobile access token received')
|
||||
return token
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-APP',
|
||||
`MobileAccess error: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return ''
|
||||
} finally {
|
||||
await this.page.goto(this.bot.config.baseURL, { timeout: 10000 }).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/browser/auth/methods/PasswordlessLogin.ts
Normal file
110
src/browser/auth/methods/PasswordlessLogin.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { Page } from 'patchright'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
|
||||
export class PasswordlessLogin {
|
||||
private readonly maxAttempts = 60
|
||||
private readonly numberDisplaySelector = 'div[data-testid="displaySign"]'
|
||||
private readonly approvalPath = '/ppsecure/post.srf'
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
|
||||
private async getDisplayedNumber(page: Page): Promise<string | null> {
|
||||
try {
|
||||
const numberElement = await page
|
||||
.waitForSelector(this.numberDisplaySelector, {
|
||||
timeout: 5000
|
||||
})
|
||||
.catch(() => null)
|
||||
|
||||
if (numberElement) {
|
||||
const number = await numberElement.textContent()
|
||||
return number?.trim() || null
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-PASSWORDLESS', 'Could not retrieve displayed number')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private async waitForApproval(page: Page): Promise<boolean> {
|
||||
try {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-PASSWORDLESS',
|
||||
`Waiting for approval... (timeout after ${this.maxAttempts} seconds)`
|
||||
)
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
|
||||
const currentUrl = new URL(page.url())
|
||||
if (currentUrl.pathname === this.approvalPath) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-PASSWORDLESS', 'Approval detected')
|
||||
return true
|
||||
}
|
||||
|
||||
// Every 5 seconds to show it's still waiting
|
||||
if (attempt % 5 === 0) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-PASSWORDLESS',
|
||||
`Still waiting... (${attempt}/${this.maxAttempts} seconds elapsed)`
|
||||
)
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
}
|
||||
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-PASSWORDLESS',
|
||||
`Approval timeout after ${this.maxAttempts} seconds!`
|
||||
)
|
||||
return false
|
||||
} catch (error: any) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-PASSWORDLESS',
|
||||
`Approval failed, an error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async handle(page: Page): Promise<void> {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-PASSWORDLESS', 'Passwordless authentication requested')
|
||||
|
||||
const displayedNumber = await this.getDisplayedNumber(page)
|
||||
|
||||
if (displayedNumber) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-PASSWORDLESS',
|
||||
`Please approve login and select number: ${displayedNumber}`
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-PASSWORDLESS',
|
||||
'Please approve login on your authenticator app'
|
||||
)
|
||||
}
|
||||
|
||||
const approved = await this.waitForApproval(page)
|
||||
|
||||
if (approved) {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-PASSWORDLESS', 'Login approved successfully')
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
} else {
|
||||
this.bot.logger.error(this.bot.isMobile, 'LOGIN-PASSWORDLESS', 'Login approval failed or timed out')
|
||||
throw new Error('Passwordless authentication timeout')
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-PASSWORDLESS',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/browser/auth/methods/Totp2FALogin.ts
Normal file
163
src/browser/auth/methods/Totp2FALogin.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { Page } from 'patchright'
|
||||
import * as OTPAuth from 'otpauth'
|
||||
import readline from 'readline'
|
||||
import type { MicrosoftRewardsBot } from '../../../index'
|
||||
|
||||
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 submitButtonSelector = 'button[type="submit"]'
|
||||
private readonly maxManualSeconds = 60
|
||||
private readonly maxManualAttempts = 5
|
||||
|
||||
constructor(private bot: MicrosoftRewardsBot) {}
|
||||
|
||||
private generateTotpCode(secret: string): string {
|
||||
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
|
||||
.waitForSelector(this.textInputSelector, { state: 'visible', timeout: 500 })
|
||||
.catch(() => null)
|
||||
|
||||
if (visibleInput) {
|
||||
await visibleInput.fill(code)
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'Filled visible TOTP text 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')
|
||||
return true
|
||||
}
|
||||
|
||||
this.bot.logger.warn(this.bot.isMobile, 'LOGIN-TOTP', 'No TOTP input field found (visible or hidden)')
|
||||
return false
|
||||
} catch (error) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`Failed to fill TOTP input: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async handle(page: Page, totpSecret?: string): Promise<void> {
|
||||
try {
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP 2FA authentication requested')
|
||||
|
||||
if (totpSecret) {
|
||||
const code = this.generateTotpCode(totpSecret)
|
||||
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')
|
||||
throw new Error('TOTP input field not found')
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(500)
|
||||
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
this.bot.logger.info(this.bot.isMobile, 'LOGIN-TOTP', 'TOTP authentication completed successfully')
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if (!code || !/^\d{6}$/.test(code)) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`Invalid or missing TOTP code (attempt ${attempt}/${this.maxManualAttempts})`
|
||||
)
|
||||
|
||||
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})`
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(500)
|
||||
await this.bot.browser.utils.ghostClick(page, this.submitButtonSelector)
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {})
|
||||
|
||||
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`)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'LOGIN-TOTP',
|
||||
`An error occurred: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user