mirror of
https://github.com/TheNetsky/Microsoft-Rewards-Script.git
synced 2026-01-20 15:03:58 +00:00
v3 init
Based of v3.0.0b10.
This commit is contained in:
130
src/functions/activities/api/FindClippy.ts
Normal file
130
src/functions/activities/api/FindClippy.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { FindClippyPromotion } from '../../../interface/DashboardData'
|
||||
import { Workers } from '../../Workers'
|
||||
|
||||
export class FindClippy extends Workers {
|
||||
private cookieHeader: string = ''
|
||||
|
||||
private fingerprintHeader: { [x: string]: string } = {}
|
||||
|
||||
private gainedPoints: number = 0
|
||||
|
||||
private oldBalance: number = this.bot.userData.currentPoints
|
||||
|
||||
public async doFindClippy(promotion: FindClippyPromotion) {
|
||||
const offerId = promotion.offerId
|
||||
const activityType = promotion.activityType
|
||||
|
||||
try {
|
||||
if (!this.bot.requestToken) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'FIND-CLIPPY',
|
||||
'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,
|
||||
'FIND-CLIPPY',
|
||||
`Starting Find Clippy | offerId=${offerId} | activityType=${activityType} | oldBalance=${this.oldBalance}`
|
||||
)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'FIND-CLIPPY',
|
||||
`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,
|
||||
'FIND-CLIPPY',
|
||||
`Prepared Find Clippy 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,
|
||||
'FIND-CLIPPY',
|
||||
`Sending Find Clippy request | offerId=${offerId} | url=${request.url}`
|
||||
)
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'FIND-CLIPPY',
|
||||
`Received Find Clippy response | offerId=${offerId} | status=${response.status}`
|
||||
)
|
||||
|
||||
const newBalance = await this.bot.browser.func.getCurrentPoints()
|
||||
this.gainedPoints = newBalance - this.oldBalance
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'FIND-CLIPPY',
|
||||
`Balance delta after Find Clippy | offerId=${offerId} | oldBalance=${this.oldBalance} | newBalance=${newBalance} | gainedPoints=${this.gainedPoints}`
|
||||
)
|
||||
|
||||
if (this.gainedPoints > 0) {
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + this.gainedPoints
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'FIND-CLIPPY',
|
||||
`Found Clippy | offerId=${offerId} | status=${response.status} | gainedPoints=${this.gainedPoints} | newBalance=${newBalance}`,
|
||||
'green'
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'FIND-CLIPPY',
|
||||
`Found Clippy but no points were gained | offerId=${offerId} | status=${response.status} | oldBalance=${this.oldBalance} | newBalance=${newBalance}`
|
||||
)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'FIND-CLIPPY', `Waiting after Find Clippy | offerId=${offerId}`)
|
||||
|
||||
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 10000))
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'FIND-CLIPPY',
|
||||
`Error in doFindClippy | offerId=${offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
173
src/functions/activities/api/Quiz.ts
Normal file
173
src/functions/activities/api/Quiz.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { BasePromotion } from '../../../interface/DashboardData'
|
||||
import { Workers } from '../../Workers'
|
||||
|
||||
export class Quiz extends Workers {
|
||||
private cookieHeader: string = ''
|
||||
|
||||
private fingerprintHeader: { [x: string]: string } = {}
|
||||
|
||||
private gainedPoints: number = 0
|
||||
|
||||
private oldBalance: number = this.bot.userData.currentPoints
|
||||
|
||||
async doQuiz(promotion: BasePromotion) {
|
||||
const offerId = promotion.offerId
|
||||
this.oldBalance = Number(this.bot.userData.currentPoints ?? 0)
|
||||
const startBalance = this.oldBalance
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Starting quiz | offerId=${offerId} | pointProgressMax=${promotion.pointProgressMax} | activityProgressMax=${promotion.activityProgressMax} | currentPoints=${startBalance}`
|
||||
)
|
||||
|
||||
try {
|
||||
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.debug(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Prepared quiz headers | offerId=${offerId} | cookieLength=${this.cookieHeader.length} | fingerprintHeaderKeys=${Object.keys(this.fingerprintHeader).length}`
|
||||
)
|
||||
|
||||
// 8-question quiz
|
||||
if (promotion.activityProgressMax === 80) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Detected 8-question quiz (activityProgressMax=80), marking as completed | offerId=${offerId}`
|
||||
)
|
||||
|
||||
// Not implemented
|
||||
return
|
||||
}
|
||||
|
||||
//Standard points quizzes (20/30/40/50 max)
|
||||
if ([20, 30, 40, 50].includes(promotion.pointProgressMax)) {
|
||||
let oldBalance = startBalance
|
||||
let gainedPoints = 0
|
||||
const maxAttempts = 20
|
||||
let totalGained = 0
|
||||
let attempts = 0
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Starting ReportActivity loop | offerId=${offerId} | maxAttempts=${maxAttempts} | startingBalance=${oldBalance}`
|
||||
)
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const jsonData = {
|
||||
UserId: null,
|
||||
TimeZoneOffset: -60,
|
||||
OfferId: offerId,
|
||||
ActivityCount: 1,
|
||||
QuestionIndex: '-1'
|
||||
}
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
url: 'https://www.bing.com/bingqa/ReportActivity?ajaxreq=1',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
cookie: this.cookieHeader,
|
||||
...this.fingerprintHeader
|
||||
},
|
||||
data: JSON.stringify(jsonData)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Sending ReportActivity request | attempt=${i + 1}/${maxAttempts} | offerId=${offerId} | url=${request.url}`
|
||||
)
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Received ReportActivity response | attempt=${i + 1}/${maxAttempts} | offerId=${offerId} | status=${response.status}`
|
||||
)
|
||||
|
||||
const newBalance = await this.bot.browser.func.getCurrentPoints()
|
||||
gainedPoints = newBalance - oldBalance
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Balance delta after ReportActivity | attempt=${i + 1}/${maxAttempts} | offerId=${offerId} | oldBalance=${oldBalance} | newBalance=${newBalance} | gainedPoints=${gainedPoints}`
|
||||
)
|
||||
|
||||
attempts = i + 1
|
||||
|
||||
if (gainedPoints > 0) {
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
||||
|
||||
oldBalance = newBalance
|
||||
totalGained += gainedPoints
|
||||
this.gainedPoints += gainedPoints
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`ReportActivity ${i + 1} → ${response.status} | offerId=${offerId} | gainedPoints=${gainedPoints} | newBalance=${newBalance}`,
|
||||
'green'
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`ReportActivity ${i + 1} | offerId=${offerId} | no more points gained, ending quiz | lastBalance=${newBalance}`
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Waiting between ReportActivity attempts | attempt=${i + 1}/${maxAttempts} | offerId=${offerId}`
|
||||
)
|
||||
|
||||
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 7000))
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Error during ReportActivity | attempt=${i + 1}/${maxAttempts} | offerId=${offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Completed the quiz successfully | offerId=${offerId} | attempts=${attempts} | totalGained=${totalGained} | startBalance=${startBalance} | finalBalance=${this.bot.userData.currentPoints}`
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Unsupported quiz configuration | offerId=${offerId} | pointProgressMax=${promotion.pointProgressMax} | activityProgressMax=${promotion.activityProgressMax}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'QUIZ',
|
||||
`Error in doQuiz | offerId=${promotion.offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
129
src/functions/activities/api/UrlReward.ts
Normal file
129
src/functions/activities/api/UrlReward.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { BasePromotion } from '../../../interface/DashboardData'
|
||||
import { Workers } from '../../Workers'
|
||||
|
||||
export class UrlReward extends Workers {
|
||||
private cookieHeader: string = ''
|
||||
|
||||
private fingerprintHeader: { [x: string]: string } = {}
|
||||
|
||||
private gainedPoints: number = 0
|
||||
|
||||
private oldBalance: number = this.bot.userData.currentPoints
|
||||
|
||||
public async doUrlReward(promotion: BasePromotion) {
|
||||
if (!this.bot.requestToken) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
'Skipping: Request token not available, this activity requires it!'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const offerId = promotion.offerId
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
`Starting UrlReward | offerId=${offerId} | geo=${this.bot.userData.geoLocale} | oldBalance=${this.oldBalance}`
|
||||
)
|
||||
|
||||
try {
|
||||
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.debug(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
`Prepared UrlReward headers | offerId=${offerId} | 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: '',
|
||||
__RequestVerificationToken: this.bot.requestToken
|
||||
})
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
`Prepared UrlReward form data | offerId=${offerId} | hash=${promotion.hash} | timeZone=60 | activityAmount=1`
|
||||
)
|
||||
|
||||
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,
|
||||
'URL-REWARD',
|
||||
`Sending UrlReward request | offerId=${offerId} | url=${request.url}`
|
||||
)
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
`Received UrlReward response | offerId=${offerId} | status=${response.status}`
|
||||
)
|
||||
|
||||
const newBalance = await this.bot.browser.func.getCurrentPoints()
|
||||
this.gainedPoints = newBalance - this.oldBalance
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
`Balance delta after UrlReward | offerId=${offerId} | oldBalance=${this.oldBalance} | newBalance=${newBalance} | gainedPoints=${this.gainedPoints}`
|
||||
)
|
||||
|
||||
if (this.gainedPoints > 0) {
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + this.gainedPoints
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
`Completed UrlReward | offerId=${offerId} | status=${response.status} | gainedPoints=${this.gainedPoints} | newBalance=${newBalance}`,
|
||||
'green'
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
`Failed UrlReward with no points | offerId=${offerId} | status=${response.status} | oldBalance=${this.oldBalance} | newBalance=${newBalance}`
|
||||
)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'URL-REWARD', `Waiting after UrlReward | offerId=${offerId}`)
|
||||
|
||||
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 10000))
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'URL-REWARD',
|
||||
`Error in doUrlReward | offerId=${promotion.offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
119
src/functions/activities/app/AppReward.ts
Normal file
119
src/functions/activities/app/AppReward.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { Promotion } from '../../../interface/AppDashBoardData'
|
||||
import { Workers } from '../../Workers'
|
||||
|
||||
export class AppReward extends Workers {
|
||||
private gainedPoints: number = 0
|
||||
|
||||
private oldBalance: number = this.bot.userData.currentPoints
|
||||
|
||||
public async doAppReward(promotion: Promotion) {
|
||||
if (!this.bot.accessToken) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
'Skipping: App access token not available, this activity requires it!'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const offerId = promotion.attributes['offerid']
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Starting AppReward | offerId=${offerId} | country=${this.bot.userData.geoLocale} | oldBalance=${this.oldBalance}`
|
||||
)
|
||||
|
||||
try {
|
||||
const jsonData = {
|
||||
id: randomUUID(),
|
||||
amount: 1,
|
||||
type: 101,
|
||||
attributes: {
|
||||
offerid: offerId
|
||||
},
|
||||
country: this.bot.userData.geoLocale
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Prepared activity payload | offerId=${offerId} | id=${jsonData.id} | amount=${jsonData.amount} | type=${jsonData.type} | country=${jsonData.country}`
|
||||
)
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
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'
|
||||
},
|
||||
data: JSON.stringify(jsonData)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Sending activity request | offerId=${offerId} | url=${request.url}`
|
||||
)
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Received activity response | offerId=${offerId} | status=${response.status}`
|
||||
)
|
||||
|
||||
const newBalance = Number(response?.data?.response?.balance ?? this.oldBalance)
|
||||
this.gainedPoints = newBalance - this.oldBalance
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Balance delta after AppReward | offerId=${offerId} | oldBalance=${this.oldBalance} | newBalance=${newBalance} | gainedPoints=${this.gainedPoints}`
|
||||
)
|
||||
|
||||
if (this.gainedPoints > 0) {
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + this.gainedPoints
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Completed AppReward | offerId=${offerId} | gainedPoints=${this.gainedPoints} | oldBalance=${this.oldBalance} | newBalance=${newBalance}`,
|
||||
'green'
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Completed AppReward with no points | offerId=${offerId} | oldBalance=${this.oldBalance} | newBalance=${newBalance}`
|
||||
)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'APP-REWARD', `Waiting after AppReward | offerId=${offerId}`)
|
||||
|
||||
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 10000))
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Finished AppReward | offerId=${offerId} | finalBalance=${this.bot.userData.currentPoints}`
|
||||
)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'APP-REWARD',
|
||||
`Error in doAppReward | offerId=${offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/functions/activities/app/DailyCheckIn.ts
Normal file
161
src/functions/activities/app/DailyCheckIn.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { Workers } from '../../Workers'
|
||||
|
||||
export class DailyCheckIn extends Workers {
|
||||
private gainedPoints: number = 0
|
||||
|
||||
private oldBalance: number = this.bot.userData.currentPoints
|
||||
|
||||
public async doDailyCheckIn() {
|
||||
if (!this.bot.accessToken) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
'Skipping: App access token not available, this activity requires it!'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.oldBalance = Number(this.bot.userData.currentPoints ?? 0)
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Starting Daily Check-In | geo=${this.bot.userData.geoLocale} | currentPoints=${this.oldBalance}`
|
||||
)
|
||||
|
||||
try {
|
||||
// Try type 101 first
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DAILY-CHECK-IN', 'Attempting Daily Check-In | type=101')
|
||||
|
||||
let response = await this.submitDaily(101) // Try using 101 (EU Variant?)
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Received Daily Check-In response | type=101 | status=${response?.status ?? 'unknown'}`
|
||||
)
|
||||
|
||||
let newBalance = Number(response?.data?.response?.balance ?? this.oldBalance)
|
||||
this.gainedPoints = newBalance - this.oldBalance
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Balance delta after Daily Check-In | type=101 | oldBalance=${this.oldBalance} | newBalance=${newBalance} | gainedPoints=${this.gainedPoints}`
|
||||
)
|
||||
|
||||
if (this.gainedPoints > 0) {
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + this.gainedPoints
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Completed Daily Check-In | type=101 | gainedPoints=${this.gainedPoints} | oldBalance=${this.oldBalance} | newBalance=${newBalance}`,
|
||||
'green'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`No points gained with type=101 | oldBalance=${this.oldBalance} | newBalance=${newBalance} | retryingWithType=103`
|
||||
)
|
||||
|
||||
// Fallback to type 103
|
||||
this.bot.logger.debug(this.bot.isMobile, 'DAILY-CHECK-IN', 'Attempting Daily Check-In | type=103')
|
||||
|
||||
response = await this.submitDaily(103) // Try using 103 (USA Variant?)
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Received Daily Check-In response | type=103 | status=${response?.status ?? 'unknown'}`
|
||||
)
|
||||
|
||||
newBalance = Number(response?.data?.response?.balance ?? this.oldBalance)
|
||||
this.gainedPoints = newBalance - this.oldBalance
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Balance delta after Daily Check-In | type=103 | oldBalance=${this.oldBalance} | newBalance=${newBalance} | gainedPoints=${this.gainedPoints}`
|
||||
)
|
||||
|
||||
if (this.gainedPoints > 0) {
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + this.gainedPoints
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Completed Daily Check-In | type=103 | gainedPoints=${this.gainedPoints} | oldBalance=${this.oldBalance} | newBalance=${newBalance}`,
|
||||
'green'
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Daily Check-In completed but no points gained | typesTried=101,103 | oldBalance=${this.oldBalance} | finalBalance=${newBalance}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Error during Daily Check-In | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async submitDaily(type: number) {
|
||||
try {
|
||||
const jsonData = {
|
||||
id: randomUUID(),
|
||||
amount: 1,
|
||||
type: type,
|
||||
attributes: {
|
||||
offerid: 'Gamification_Sapphire_DailyCheckIn'
|
||||
},
|
||||
country: this.bot.userData.geoLocale
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Preparing Daily Check-In payload | type=${type} | id=${jsonData.id} | amount=${jsonData.amount} | country=${jsonData.country}`
|
||||
)
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
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'
|
||||
},
|
||||
data: JSON.stringify(jsonData)
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Sending Daily Check-In request | type=${type} | url=${request.url}`
|
||||
)
|
||||
|
||||
return this.bot.axios.request(request)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'DAILY-CHECK-IN',
|
||||
`Error in submitDaily | type=${type} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/functions/activities/app/ReadToEarn.ts
Normal file
131
src/functions/activities/app/ReadToEarn.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { Workers } from '../../Workers'
|
||||
|
||||
export class ReadToEarn extends Workers {
|
||||
public async doReadToEarn() {
|
||||
if (!this.bot.accessToken) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
'Skipping: App access token not available, this activity requires it!'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const delayMin = this.bot.config.searchSettings.readDelay.min
|
||||
const delayMax = this.bot.config.searchSettings.readDelay.max
|
||||
const startBalance = Number(this.bot.userData.currentPoints ?? 0)
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`Starting Read to Earn | geo=${this.bot.userData.geoLocale} | delayRange=${delayMin}-${delayMax} | currentPoints=${startBalance}`
|
||||
)
|
||||
|
||||
try {
|
||||
const jsonData = {
|
||||
amount: 1,
|
||||
id: '1',
|
||||
type: 101,
|
||||
attributes: {
|
||||
offerid: 'ENUS_readarticle3_30points'
|
||||
},
|
||||
country: this.bot.userData.geoLocale
|
||||
}
|
||||
|
||||
const articleCount = 10
|
||||
let totalGained = 0
|
||||
let articlesRead = 0
|
||||
let oldBalance = startBalance
|
||||
|
||||
for (let i = 0; i < articleCount; ++i) {
|
||||
jsonData.id = randomBytes(64).toString('hex')
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`Submitting Read to Earn activity | article=${i + 1}/${articleCount} | id=${jsonData.id} | country=${jsonData.country}`
|
||||
)
|
||||
|
||||
const request: AxiosRequestConfig = {
|
||||
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
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'
|
||||
},
|
||||
data: JSON.stringify(jsonData)
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`Received Read to Earn response | article=${i + 1}/${articleCount} | status=${response?.status ?? 'unknown'}`
|
||||
)
|
||||
|
||||
const newBalance = Number(response?.data?.response?.balance ?? oldBalance)
|
||||
const gainedPoints = newBalance - oldBalance
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`Balance delta after article | article=${i + 1}/${articleCount} | oldBalance=${oldBalance} | newBalance=${newBalance} | gainedPoints=${gainedPoints}`
|
||||
)
|
||||
|
||||
if (gainedPoints <= 0) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`No points gained, stopping Read to Earn | article=${i + 1}/${articleCount} | status=${response.status} | oldBalance=${oldBalance} | newBalance=${newBalance}`
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// Update point tracking
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + gainedPoints
|
||||
totalGained += gainedPoints
|
||||
articlesRead = i + 1
|
||||
oldBalance = newBalance
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`Read article ${i + 1}/${articleCount} | status=${response.status} | gainedPoints=${gainedPoints} | newBalance=${newBalance}`,
|
||||
'green'
|
||||
)
|
||||
|
||||
// Wait random delay between articles
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`Waiting between articles | article=${i + 1}/${articleCount} | delayRange=${delayMin}-${delayMax}`
|
||||
)
|
||||
|
||||
await this.bot.utils.wait(this.bot.utils.randomDelay(delayMin, delayMax))
|
||||
}
|
||||
|
||||
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`Completed Read to Earn | articlesRead=${articlesRead} | totalGained=${totalGained} | startBalance=${startBalance} | finalBalance=${finalBalance}`
|
||||
)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'READ-TO-EARN',
|
||||
`Error during Read to Earn | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
426
src/functions/activities/browser/Search.ts
Normal file
426
src/functions/activities/browser/Search.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import type { Page } from 'patchright'
|
||||
import type { Counters, DashboardData } from '../../../interface/DashboardData'
|
||||
|
||||
import { QueryCore } from '../../QueryEngine'
|
||||
import { Workers } from '../../Workers'
|
||||
|
||||
export class Search extends Workers {
|
||||
private bingHome = 'https://bing.com'
|
||||
private searchPageURL = ''
|
||||
private searchCount = 0
|
||||
|
||||
public async doSearch(data: DashboardData, page: Page, isMobile: boolean): Promise<number> {
|
||||
const startBalance = Number(this.bot.userData.currentPoints ?? 0)
|
||||
|
||||
this.bot.logger.info(isMobile, 'SEARCH-BING', `Starting Bing searches | currentPoints=${startBalance}`)
|
||||
|
||||
let totalGainedPoints = 0
|
||||
|
||||
try {
|
||||
let searchCounters: Counters = await this.bot.browser.func.getSearchPoints()
|
||||
const missingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
||||
let missingPointsTotal = missingPoints.totalPoints
|
||||
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Initial search counters | mobile=${missingPoints.mobilePoints} | desktop=${missingPoints.desktopPoints} | edge=${missingPoints.edgePoints}`
|
||||
)
|
||||
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`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.toUpperCase()
|
||||
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Resolving search queries | locale=${locale}`)
|
||||
|
||||
// Set Google search queries
|
||||
queries = await queryCore.getGoogleTrends(locale)
|
||||
|
||||
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}`)
|
||||
|
||||
// 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
|
||||
const stagnantLoopMax = 10
|
||||
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
const query = queries[i] as string
|
||||
|
||||
searchCounters = await this.bingSearch(page, query, isMobile)
|
||||
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)
|
||||
|
||||
if (gainedPoints === 0) {
|
||||
stagnantLoop++
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`No points gained ${stagnantLoop}/${stagnantLoopMax} | query="${query}" | remaining=${newMissingPointsTotal}`
|
||||
)
|
||||
} 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(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`gainedPoints=${gainedPoints} points | query="${query}" | remaining=${newMissingPointsTotal}`,
|
||||
'green'
|
||||
)
|
||||
}
|
||||
|
||||
// Update loop state
|
||||
missingPointsTotal = newMissingPointsTotal
|
||||
|
||||
// Completed
|
||||
if (missingPointsTotal === 0) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
'All required search points earned, stopping main search loop'
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
// Stuck
|
||||
if (stagnantLoop > stagnantLoopMax) {
|
||||
this.bot.logger.warn(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Search did not gain points for ${stagnantLoopMax} iterations, aborting main search loop`
|
||||
)
|
||||
stagnantLoop = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (missingPointsTotal > 0) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Search completed but still missing points, generating extra searches | remaining=${missingPointsTotal}`
|
||||
)
|
||||
|
||||
let i = 0
|
||||
let stagnantLoop = 0
|
||||
const stagnantLoopMax = 5
|
||||
|
||||
while (missingPointsTotal > 0) {
|
||||
const query = queries[i++] as string
|
||||
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Fetching related terms for extra searches | baseQuery="${query}"`
|
||||
)
|
||||
|
||||
const relatedTerms = await queryCore.getBingRelatedTerms(query)
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Related terms resolved | baseQuery="${query}" | count=${relatedTerms.length}`
|
||||
)
|
||||
|
||||
if (relatedTerms.length > 3) {
|
||||
for (const term of relatedTerms.slice(1, 3)) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING-EXTRA',
|
||||
`Extra search | remaining=${missingPointsTotal} | query="${term}"`
|
||||
)
|
||||
|
||||
searchCounters = await this.bingSearch(page, term, isMobile)
|
||||
const newMissingPoints = this.bot.browser.func.missingSearchPoints(searchCounters, isMobile)
|
||||
const newMissingPointsTotal = newMissingPoints.totalPoints
|
||||
|
||||
// Points gained for THIS extra query only
|
||||
const rawGained = missingPointsTotal - newMissingPointsTotal
|
||||
const gainedPoints = Math.max(0, rawGained)
|
||||
|
||||
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
|
||||
|
||||
// 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(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalBalance = Number(this.bot.userData.currentPoints ?? startBalance)
|
||||
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Completed Bing searches | startBalance=${startBalance} | newBalance=${finalBalance}`
|
||||
)
|
||||
|
||||
return totalGainedPoints
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Error in doSearch | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return totalGainedPoints
|
||||
}
|
||||
}
|
||||
|
||||
private async bingSearch(searchPage: Page, query: string, isMobile: boolean) {
|
||||
const maxAttempts = 5
|
||||
const refreshThreshold = 10 // Page gets sluggish after x searches?
|
||||
|
||||
this.searchCount++
|
||||
|
||||
// Page fill seems to get more sluggish over time
|
||||
if (this.searchCount % refreshThreshold === 0) {
|
||||
this.bot.logger.info(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Returning to home page to clear accumulated page context | count=${this.searchCount} | threshold=${refreshThreshold}`
|
||||
)
|
||||
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-BING', `Returning home to refresh state | url=${this.bingHome}`)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Starting bingSearch | query="${query}" | maxAttempts=${maxAttempts} | searchCount=${this.searchCount} | refreshEvery=${refreshThreshold} | scrollRandomResults=${this.bot.config.searchSettings.scrollRandomResults} | clickRandomResults=${this.bot.config.searchSettings.clickRandomResults}`
|
||||
)
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const searchBar = '#sb_form_q'
|
||||
const searchBox = searchPage.locator(searchBar)
|
||||
|
||||
await searchPage.evaluate(() => {
|
||||
window.scrollTo({ left: 0, top: 0, behavior: 'auto' })
|
||||
})
|
||||
|
||||
await searchPage.keyboard.press('Home')
|
||||
await searchBox.waitFor({ state: 'visible', timeout: 15000 })
|
||||
|
||||
await this.bot.utils.wait(1000)
|
||||
await this.bot.browser.utils.ghostClick(searchPage, searchBar, { clickCount: 3 })
|
||||
await searchBox.fill('')
|
||||
|
||||
await searchPage.keyboard.type(query, { delay: 50 })
|
||||
await searchPage.keyboard.press('Enter')
|
||||
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Submitted query to Bing | attempt=${i + 1}/${maxAttempts} | query="${query}"`
|
||||
)
|
||||
|
||||
await this.bot.utils.wait(3000)
|
||||
|
||||
if (this.bot.config.searchSettings.scrollRandomResults) {
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.randomScroll(searchPage, isMobile)
|
||||
}
|
||||
|
||||
if (this.bot.config.searchSettings.clickRandomResults) {
|
||||
await this.bot.utils.wait(2000)
|
||||
await this.clickRandomLink(searchPage, isMobile)
|
||||
}
|
||||
|
||||
await this.bot.utils.wait(
|
||||
this.bot.utils.randomDelay(
|
||||
this.bot.config.searchSettings.searchDelay.min,
|
||||
this.bot.config.searchSettings.searchDelay.max
|
||||
)
|
||||
)
|
||||
|
||||
const counters = await this.bot.browser.func.getSearchPoints()
|
||||
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Search counters after query | attempt=${i + 1}/${maxAttempts} | query="${query}"`
|
||||
)
|
||||
|
||||
return counters
|
||||
} catch (error) {
|
||||
if (i >= 5) {
|
||||
this.bot.logger.error(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Failed after 5 retries | query="${query}" | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
this.bot.logger.error(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Search attempt failed | attempt=${i + 1}/${maxAttempts} | query="${query}" | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
|
||||
this.bot.logger.warn(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Retrying search | attempt=${i + 1}/${maxAttempts} | query="${query}"`
|
||||
)
|
||||
|
||||
await this.bot.utils.wait(2000)
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-BING',
|
||||
`Returning current search counters after failed retries | query="${query}"`
|
||||
)
|
||||
|
||||
return await this.bot.browser.func.getSearchPoints()
|
||||
}
|
||||
|
||||
private async randomScroll(page: Page, isMobile: boolean) {
|
||||
try {
|
||||
const viewportHeight = await page.evaluate(() => window.innerHeight)
|
||||
const totalHeight = await page.evaluate(() => document.body.scrollHeight)
|
||||
const randomScrollPosition = Math.floor(Math.random() * (totalHeight - viewportHeight))
|
||||
|
||||
this.bot.logger.debug(
|
||||
isMobile,
|
||||
'SEARCH-RANDOM-SCROLL',
|
||||
`Random scroll | viewportHeight=${viewportHeight} | totalHeight=${totalHeight} | scrollPos=${randomScrollPosition}`
|
||||
)
|
||||
|
||||
await page.evaluate((scrollPos: number) => {
|
||||
window.scrollTo({ left: 0, top: scrollPos, behavior: 'auto' })
|
||||
}, randomScrollPosition)
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
isMobile,
|
||||
'SEARCH-RANDOM-SCROLL',
|
||||
`An error occurred during random scroll | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async clickRandomLink(page: Page, isMobile: boolean) {
|
||||
try {
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', 'Attempting to click a random search result link')
|
||||
|
||||
const searchPageUrl = page.url()
|
||||
|
||||
await this.bot.browser.utils.ghostClick(page, '#b_results .b_algo h2')
|
||||
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()
|
||||
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', `Visited result tab | url=${newTabUrl}`)
|
||||
|
||||
await this.bot.browser.utils.closeTabs(newTab)
|
||||
this.bot.logger.debug(isMobile, 'SEARCH-RANDOM-CLICK', 'Closed result tab')
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
isMobile,
|
||||
'SEARCH-RANDOM-CLICK',
|
||||
`An error occurred during random click | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
330
src/functions/activities/browser/SearchOnBing.ts
Normal file
330
src/functions/activities/browser/SearchOnBing.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import type { Page } from 'patchright'
|
||||
import * as fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { Workers } from '../../Workers'
|
||||
import { QueryCore } from '../../QueryEngine'
|
||||
|
||||
import type { BasePromotion } from '../../../interface/DashboardData'
|
||||
|
||||
export class SearchOnBing extends Workers {
|
||||
private bingHome = 'https://bing.com'
|
||||
|
||||
private cookieHeader: string = ''
|
||||
|
||||
private fingerprintHeader: { [x: string]: string } = {}
|
||||
|
||||
private gainedPoints: number = 0
|
||||
|
||||
private success: boolean = false
|
||||
|
||||
private oldBalance: number = this.bot.userData.currentPoints
|
||||
|
||||
public async doSearchOnBing(promotion: BasePromotion, page: Page) {
|
||||
const offerId = promotion.offerId
|
||||
this.oldBalance = Number(this.bot.userData.currentPoints ?? 0)
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING',
|
||||
`Starting SearchOnBing | offerId=${offerId} | title="${promotion.title}" | currentPoints=${this.oldBalance}`
|
||||
)
|
||||
|
||||
try {
|
||||
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.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING',
|
||||
`Prepared headers for SearchOnBing | offerId=${offerId} | cookieLength=${this.cookieHeader.length} | fingerprintHeaderKeys=${Object.keys(this.fingerprintHeader).length}`
|
||||
)
|
||||
|
||||
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-ON-BING', `Activating search task | offerId=${offerId}`)
|
||||
|
||||
const activated = await this.activateSearchTask(promotion)
|
||||
if (!activated) {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING',
|
||||
`Search activity couldn't be activated, aborting | offerId=${offerId}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Do the bing search here
|
||||
const queries = await this.getSearchQueries(promotion)
|
||||
|
||||
// Run through the queries
|
||||
await this.searchBing(page, queries)
|
||||
|
||||
if (this.success) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING',
|
||||
`Completed SearchOnBing | offerId=${offerId} | startBalance=${this.oldBalance} | finalBalance=${this.bot.userData.currentPoints}`
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING',
|
||||
`Failed SearchOnBing | offerId=${offerId} | startBalance=${this.oldBalance} | finalBalance=${this.bot.userData.currentPoints}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING',
|
||||
`Error in doSearchOnBing | offerId=${promotion.offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async searchBing(page: Page, queries: string[]) {
|
||||
queries = [...new Set(queries)]
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-SEARCH',
|
||||
`Starting search loop | queriesCount=${queries.length} | oldBalance=${this.oldBalance}`
|
||||
)
|
||||
|
||||
let i = 0
|
||||
for (const query of queries) {
|
||||
try {
|
||||
this.bot.logger.debug(this.bot.isMobile, 'SEARCH-ON-BING-SEARCH', `Processing query | query="${query}"`)
|
||||
|
||||
await this.bot.mainMobilePage.goto(this.bingHome)
|
||||
|
||||
// Wait until page loaded
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
||||
|
||||
await this.bot.browser.utils.tryDismissAllMessages(page)
|
||||
|
||||
const searchBar = '#sb_form_q'
|
||||
|
||||
const searchBox = page.locator(searchBar)
|
||||
await searchBox.waitFor({ state: 'attached', timeout: 15000 })
|
||||
|
||||
await this.bot.utils.wait(500)
|
||||
await this.bot.browser.utils.ghostClick(page, searchBar, { clickCount: 3 })
|
||||
await searchBox.fill('')
|
||||
|
||||
await page.keyboard.type(query, { delay: 50 })
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 7000))
|
||||
|
||||
// Check for point updates
|
||||
const newBalance = await this.bot.browser.func.getCurrentPoints()
|
||||
this.gainedPoints = newBalance - this.oldBalance
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-SEARCH',
|
||||
`Balance check after query | query="${query}" | oldBalance=${this.oldBalance} | newBalance=${newBalance} | gainedPoints=${this.gainedPoints}`
|
||||
)
|
||||
|
||||
if (this.gainedPoints > 0) {
|
||||
this.bot.userData.currentPoints = newBalance
|
||||
this.bot.userData.gainedPoints = (this.bot.userData.gainedPoints ?? 0) + this.gainedPoints
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-SEARCH',
|
||||
`SearchOnBing query completed | query="${query}" | gainedPoints=${this.gainedPoints} | oldBalance=${this.oldBalance} | newBalance=${newBalance}`,
|
||||
'green'
|
||||
)
|
||||
|
||||
this.success = true
|
||||
return
|
||||
} else {
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-SEARCH',
|
||||
`${++i}/${queries.length} | noPoints=1 | query="${query}"`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-SEARCH',
|
||||
`Error during search loop | query="${query}" | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
} finally {
|
||||
await this.bot.utils.wait(this.bot.utils.randomDelay(5000, 15000))
|
||||
await page.goto(this.bot.config.baseURL, { timeout: 5000 }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
this.bot.logger.warn(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-SEARCH',
|
||||
`Finished all queries with no points gained | queriesTried=${queries.length} | oldBalance=${this.oldBalance} | finalBalance=${this.bot.userData.currentPoints}`
|
||||
)
|
||||
}
|
||||
|
||||
// The task needs to be activated before being able to complete it
|
||||
private async activateSearchTask(promotion: BasePromotion): Promise<boolean> {
|
||||
try {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-ACTIVATE',
|
||||
`Preparing activation request | offerId=${promotion.offerId} | hash=${promotion.hash}`
|
||||
)
|
||||
|
||||
const formData = new URLSearchParams({
|
||||
id: promotion.offerId,
|
||||
hash: promotion.hash,
|
||||
timeZone: '60',
|
||||
activityAmount: '1',
|
||||
dbs: '0',
|
||||
form: '',
|
||||
type: '',
|
||||
__RequestVerificationToken: this.bot.requestToken
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const response = await this.bot.axios.request(request)
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-ACTIVATE',
|
||||
`Successfully activated activity | status=${response.status} | offerId=${promotion.offerId}`
|
||||
)
|
||||
return true
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-ACTIVATE',
|
||||
`Activation failed | offerId=${promotion.offerId} | message=${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private async getSearchQueries(promotion: BasePromotion): Promise<string[]> {
|
||||
interface Queries {
|
||||
title: string
|
||||
queries: string[]
|
||||
}
|
||||
|
||||
let queries: Queries[] = []
|
||||
|
||||
try {
|
||||
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')
|
||||
queries = JSON.parse(data)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`Loaded queries config | source=local | entries=${queries.length}`
|
||||
)
|
||||
} else {
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
'Fetching queries config from remote repository'
|
||||
)
|
||||
|
||||
// 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/main/src/functions/queries.json'
|
||||
})
|
||||
queries = response.data
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`Loaded queries config | source=remote | entries=${queries.length}`
|
||||
)
|
||||
}
|
||||
|
||||
const answers = queries.find(
|
||||
x => this.bot.utils.normalizeString(x.title) === this.bot.utils.normalizeString(promotion.title)
|
||||
)
|
||||
|
||||
if (answers && answers.queries.length > 0) {
|
||||
const answer = this.bot.utils.shuffleArray(answers.queries)
|
||||
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`Found answers for activity title | source=${this.bot.config.searchOnBingLocalQueries ? 'local' : 'remote'} | title="${promotion.title}" | answersCount=${answer.length} | firstQuery="${answer[0]}"`
|
||||
)
|
||||
|
||||
return answer
|
||||
} else {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`No matching title in queries config | source=${this.bot.config.searchOnBingLocalQueries ? 'local' : 'remote'} | title="${promotion.title}"`
|
||||
)
|
||||
|
||||
const queryCore = new QueryCore(this.bot)
|
||||
|
||||
const promotionDescription = promotion.description.toLowerCase().trim()
|
||||
const queryDescription = promotionDescription.replace('search on bing', '').trim()
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`Requesting Bing suggestions | queryDescription="${queryDescription}"`
|
||||
)
|
||||
|
||||
const bingSuggestions = await queryCore.getBingSuggestions(queryDescription)
|
||||
|
||||
this.bot.logger.debug(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`Bing suggestions result | count=${bingSuggestions.length} | title="${promotion.title}"`
|
||||
)
|
||||
|
||||
// If no suggestions found
|
||||
if (!bingSuggestions.length) {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`No suggestions found, falling back to activity title | title="${promotion.title}"`
|
||||
)
|
||||
return [promotion.title]
|
||||
} else {
|
||||
this.bot.logger.info(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`Using Bing suggestions as search queries | count=${bingSuggestions.length} | title="${promotion.title}"`
|
||||
)
|
||||
return bingSuggestions
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.bot.logger.error(
|
||||
this.bot.isMobile,
|
||||
'SEARCH-ON-BING-QUERY',
|
||||
`Error while resolving search queries | title="${promotion.title}" | message=${error instanceof Error ? error.message : String(error)} | fallback=promotionTitle`
|
||||
)
|
||||
return [promotion.title]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user