Initial commit

This commit is contained in:
2025-11-01 20:44:31 +01:00
commit 6d549e6590
91 changed files with 20404 additions and 0 deletions

164
src/functions/Activities.ts Normal file
View File

@@ -0,0 +1,164 @@
import { Page } from 'rebrowser-playwright'
import { MicrosoftRewardsBot } from '../index'
import { Search } from './activities/Search'
import { ABC } from './activities/ABC'
import { Poll } from './activities/Poll'
import { Quiz } from './activities/Quiz'
import { ThisOrThat } from './activities/ThisOrThat'
import { UrlReward } from './activities/UrlReward'
import { SearchOnBing } from './activities/SearchOnBing'
import { ReadToEarn } from './activities/ReadToEarn'
import { DailyCheckIn } from './activities/DailyCheckIn'
import { DashboardData, MorePromotion, PromotionalItem } from '../interface/DashboardData'
import type { ActivityHandler } from '../interface/ActivityHandler'
type ActivityKind =
| { type: 'poll' }
| { type: 'abc' }
| { type: 'thisOrThat' }
| { type: 'quiz' }
| { type: 'urlReward' }
| { type: 'searchOnBing' }
| { type: 'unsupported' }
export default class Activities {
private bot: MicrosoftRewardsBot
private handlers: ActivityHandler[] = []
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
}
// Register external/custom handlers (optional extension point)
registerHandler(handler: ActivityHandler) {
this.handlers.push(handler)
}
// Centralized dispatcher for activities from dashboard/punchcards
async run(page: Page, activity: MorePromotion | PromotionalItem): Promise<void> {
// First, try custom handlers (if any)
for (const h of this.handlers) {
try {
if (h.canHandle(activity)) {
await h.run(page, activity)
return
}
} catch (e) {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Custom handler ${(h.id || 'unknown')} failed: ${e instanceof Error ? e.message : e}`, 'error')
}
}
const kind = this.classifyActivity(activity)
try {
switch (kind.type) {
case 'poll':
await this.doPoll(page)
break
case 'abc':
await this.doABC(page)
break
case 'thisOrThat':
await this.doThisOrThat(page)
break
case 'quiz':
await this.doQuiz(page)
break
case 'searchOnBing':
await this.doSearchOnBing(page, activity)
break
case 'urlReward':
await this.doUrlReward(page)
break
default:
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${String((activity as { promotionType?: string }).promotionType)}"!`, 'warn')
break
}
} catch (e) {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Dispatcher error for "${activity.title}": ${e instanceof Error ? e.message : e}`, 'error')
}
}
public getTypeLabel(activity: MorePromotion | PromotionalItem): string {
const k = this.classifyActivity(activity)
switch (k.type) {
case 'poll': return 'Poll'
case 'abc': return 'ABC'
case 'thisOrThat': return 'ThisOrThat'
case 'quiz': return 'Quiz'
case 'searchOnBing': return 'SearchOnBing'
case 'urlReward': return 'UrlReward'
default: return 'Unsupported'
}
}
private classifyActivity(activity: MorePromotion | PromotionalItem): ActivityKind {
const type = (activity.promotionType || '').toLowerCase()
if (type === 'quiz') {
// Distinguish Poll/ABC/ThisOrThat vs general quiz using current heuristics
const max = activity.pointProgressMax
const url = (activity.destinationUrl || '').toLowerCase()
if (max === 10) {
if (url.includes('pollscenarioid')) return { type: 'poll' }
return { type: 'abc' }
}
if (max === 50) return { type: 'thisOrThat' }
return { type: 'quiz' }
}
if (type === 'urlreward') {
const name = (activity.name || '').toLowerCase()
if (name.includes('exploreonbing')) return { type: 'searchOnBing' }
return { type: 'urlReward' }
}
return { type: 'unsupported' }
}
doSearch = async (page: Page, data: DashboardData): Promise<void> => {
const search = new Search(this.bot)
await search.doSearch(page, data)
}
doABC = async (page: Page): Promise<void> => {
const abc = new ABC(this.bot)
await abc.doABC(page)
}
doPoll = async (page: Page): Promise<void> => {
const poll = new Poll(this.bot)
await poll.doPoll(page)
}
doThisOrThat = async (page: Page): Promise<void> => {
const thisOrThat = new ThisOrThat(this.bot)
await thisOrThat.doThisOrThat(page)
}
doQuiz = async (page: Page): Promise<void> => {
const quiz = new Quiz(this.bot)
await quiz.doQuiz(page)
}
doUrlReward = async (page: Page): Promise<void> => {
const urlReward = new UrlReward(this.bot)
await urlReward.doUrlReward(page)
}
doSearchOnBing = async (page: Page, activity: MorePromotion | PromotionalItem): Promise<void> => {
const searchOnBing = new SearchOnBing(this.bot)
await searchOnBing.doSearchOnBing(page, activity)
}
doReadToEarn = async (accessToken: string, data: DashboardData): Promise<void> => {
const readToEarn = new ReadToEarn(this.bot)
await readToEarn.doReadToEarn(accessToken, data)
}
doDailyCheckIn = async (accessToken: string, data: DashboardData): Promise<void> => {
const dailyCheckIn = new DailyCheckIn(this.bot)
await dailyCheckIn.doDailyCheckIn(accessToken, data)
}
}

1303
src/functions/Login.ts Normal file

File diff suppressed because it is too large Load Diff

248
src/functions/Workers.ts Normal file
View File

@@ -0,0 +1,248 @@
import { Page } from 'rebrowser-playwright'
import { DashboardData, MorePromotion, PromotionalItem, PunchCard } from '../interface/DashboardData'
import { MicrosoftRewardsBot } from '../index'
import JobState from '../util/JobState'
import Retry from '../util/Retry'
import { AdaptiveThrottler } from '../util/AdaptiveThrottler'
export class Workers {
public bot: MicrosoftRewardsBot
private jobState: JobState
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
this.jobState = new JobState(this.bot.config)
}
// Daily Set
async doDailySet(page: Page, data: DashboardData) {
const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()]
const today = this.bot.utils.getFormattedDate()
const activitiesUncompleted = (todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [])
.filter(x => {
if (this.bot.config.jobState?.enabled === false) return true
const email = this.bot.currentAccountEmail || 'unknown'
return !this.jobState.isDone(email, today, x.offerId)
})
if (!activitiesUncompleted.length) {
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All Daily Set" items have already been completed')
return
}
// Solve Activities
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'Started solving "Daily Set" items')
await this.solveActivities(page, activitiesUncompleted)
// Mark as done to prevent duplicate work if checkpoints enabled
if (this.bot.config.jobState?.enabled !== false) {
const email = this.bot.currentAccountEmail || 'unknown'
for (const a of activitiesUncompleted) {
this.jobState.markDone(email, today, a.offerId)
}
}
page = await this.bot.browser.utils.getLatestTab(page)
// Always return to the homepage if not already
await this.bot.browser.func.goHome(page)
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All "Daily Set" items have been completed')
// Optional: immediately run desktop search bundle
if (!this.bot.isMobile && this.bot.config.workers.bundleDailySetWithSearch && this.bot.config.workers.doDesktopSearch) {
try {
await this.bot.utils.waitRandom(1200, 2600)
await this.bot.activities.doSearch(page, data)
} catch (e) {
this.bot.log(this.bot.isMobile, 'DAILY-SET', `Post-DailySet search failed: ${e instanceof Error ? e.message : e}`, 'warn')
}
}
}
// Punch Card
async doPunchCard(page: Page, data: DashboardData) {
const punchCardsUncompleted = data.punchCards?.filter(x => x.parentPromotion && !x.parentPromotion.complete) ?? [] // Only return uncompleted punch cards
if (!punchCardsUncompleted.length) {
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', 'All "Punch Cards" have already been completed')
return
}
for (const punchCard of punchCardsUncompleted) {
// Ensure parentPromotion exists before proceeding
if (!punchCard.parentPromotion?.title) {
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `Skipped punchcard "${punchCard.name}" | Reason: Parent promotion is missing!`, 'warn')
continue
}
// Get latest page for each card
page = await this.bot.browser.utils.getLatestTab(page)
const activitiesUncompleted = punchCard.childPromotions.filter(x => !x.complete) // Only return uncompleted activities
// Solve Activities
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `Started solving "Punch Card" items for punchcard: "${punchCard.parentPromotion.title}"`)
// Got to punch card index page in a new tab
await page.goto(punchCard.parentPromotion.destinationUrl, { referer: this.bot.config.baseURL })
// Wait for new page to load, max 10 seconds, however try regardless in case of error
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { })
await this.solveActivities(page, activitiesUncompleted, punchCard)
page = await this.bot.browser.utils.getLatestTab(page)
const pages = page.context().pages()
if (pages.length > 3) {
await page.close()
} else {
await this.bot.browser.func.goHome(page)
}
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', `All items for punchcard: "${punchCard.parentPromotion.title}" have been completed`)
}
this.bot.log(this.bot.isMobile, 'PUNCH-CARD', 'All "Punch Card" items have been completed')
}
// More Promotions
async doMorePromotions(page: Page, data: DashboardData) {
const morePromotions = data.morePromotions
// Check if there is a promotional item
if (data.promotionalItem) { // Convert and add the promotional item to the array
morePromotions.push(data.promotionalItem as unknown as MorePromotion)
}
const activitiesUncompleted = morePromotions?.filter(x => !x.complete && x.pointProgressMax > 0 && x.exclusiveLockedFeatureStatus !== 'locked') ?? []
if (!activitiesUncompleted.length) {
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'All "More Promotion" items have already been completed')
return
}
// Solve Activities
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'Started solving "More Promotions" items')
page = await this.bot.browser.utils.getLatestTab(page)
await this.solveActivities(page, activitiesUncompleted)
page = await this.bot.browser.utils.getLatestTab(page)
// Always return to the homepage if not already
await this.bot.browser.func.goHome(page)
this.bot.log(this.bot.isMobile, 'MORE-PROMOTIONS', 'All "More Promotion" items have been completed')
}
// Solve all the different types of activities
private async solveActivities(activityPage: Page, activities: PromotionalItem[] | MorePromotion[], punchCard?: PunchCard) {
const activityInitial = activityPage.url()
const retry = new Retry(this.bot.config.retryPolicy)
const throttle = new AdaptiveThrottler()
for (const activity of activities) {
try {
activityPage = await this.manageTabLifecycle(activityPage, activityInitial)
await this.applyThrottle(throttle, 800, 1400)
const selector = await this.buildActivitySelector(activityPage, activity, punchCard)
await this.prepareActivityPage(activityPage, selector, throttle)
const typeLabel = this.bot.activities.getTypeLabel(activity)
if (typeLabel !== 'Unsupported') {
await this.executeActivity(activityPage, activity, selector, throttle, retry)
} else {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Skipped activity "${activity.title}" | Reason: Unsupported type: "${activity.promotionType}"!`, 'warn')
}
await this.applyThrottle(throttle, 1200, 2600)
} catch (error) {
await this.bot.browser.utils.captureDiagnostics(activityPage, `activity_error_${activity.title || activity.offerId}`)
this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
throttle.record(false)
}
}
}
private async manageTabLifecycle(page: Page, initialUrl: string): Promise<Page> {
page = await this.bot.browser.utils.getLatestTab(page)
const pages = page.context().pages()
if (pages.length > 3) {
await page.close()
page = await this.bot.browser.utils.getLatestTab(page)
}
if (page.url() !== initialUrl) {
await page.goto(initialUrl)
}
return page
}
private async buildActivitySelector(page: Page, activity: PromotionalItem | MorePromotion, punchCard?: PunchCard): Promise<string> {
if (punchCard) {
return await this.bot.browser.func.getPunchCardActivity(page, activity)
}
const name = activity.name.toLowerCase()
if (name.includes('membercenter') || name.includes('exploreonbing')) {
return `[data-bi-id^="${activity.name}"] .pointLink:not(.contentContainer .pointLink)`
}
return `[data-bi-id^="${activity.offerId}"] .pointLink:not(.contentContainer .pointLink)`
}
private async prepareActivityPage(page: Page, selector: string, throttle: AdaptiveThrottler): Promise<void> {
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
await this.bot.browser.utils.humanizePage(page)
await this.applyThrottle(throttle, 1200, 2600)
}
private async executeActivity(page: Page, activity: PromotionalItem | MorePromotion, selector: string, throttle: AdaptiveThrottler, retry: Retry): Promise<void> {
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Found activity type: "${this.bot.activities.getTypeLabel(activity)}" title: "${activity.title}"`)
await page.click(selector)
page = await this.bot.browser.utils.getLatestTab(page)
const timeoutMs = this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? '30s') * 2
const runWithTimeout = (p: Promise<void>) => Promise.race([
p,
new Promise<void>((_, rej) => setTimeout(() => rej(new Error('activity-timeout')), timeoutMs))
])
await retry.run(async () => {
try {
await runWithTimeout(this.bot.activities.run(page, activity))
throttle.record(true)
} catch (e) {
await this.bot.browser.utils.captureDiagnostics(page, `activity_timeout_${activity.title || activity.offerId}`)
throttle.record(false)
throw e
}
}, () => true)
await this.bot.browser.utils.humanizePage(page)
}
private async applyThrottle(throttle: AdaptiveThrottler, min: number, max: number): Promise<void> {
const multiplier = throttle.getDelayMultiplier()
const riskMultiplier = this.bot.getRiskDelayMultiplier()
await this.bot.utils.waitRandom(
Math.floor(min * multiplier * riskMultiplier),
Math.floor(max * multiplier * riskMultiplier)
)
}
}

View File

@@ -0,0 +1,51 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { RETRY_LIMITS, TIMEOUTS } from '../../constants'
export class ABC extends Workers {
async doABC(page: Page) {
this.bot.log(this.bot.isMobile, 'ABC', 'Trying to complete poll')
try {
let $ = await this.bot.browser.func.loadInCheerio(page)
let i
for (i = 0; i < RETRY_LIMITS.ABC_MAX && !$('span.rw_icon').length; i++) {
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT })
const answers = $('.wk_OptionClickClass')
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT })
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
await page.click(`#${answer}`) // Click answer
await this.bot.utils.wait(TIMEOUTS.LONG + 1000)
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT })
await page.click('div.wk_button') // Click next question button
page = await this.bot.browser.utils.getLatestTab(page)
$ = await this.bot.browser.func.loadInCheerio(page)
await this.bot.utils.wait(TIMEOUTS.MEDIUM)
}
await this.bot.utils.wait(TIMEOUTS.LONG + 1000)
await page.close()
if (i === RETRY_LIMITS.ABC_MAX) {
this.bot.log(this.bot.isMobile, 'ABC', `Failed to solve quiz, exceeded max iterations of ${RETRY_LIMITS.ABC_MAX}`, 'warn')
} else {
this.bot.log(this.bot.isMobile, 'ABC', 'Completed the ABC successfully')
}
} catch (error) {
await page.close()
this.bot.log(this.bot.isMobile, 'ABC', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -0,0 +1,48 @@
import { randomBytes } from 'crypto'
import { AxiosRequestConfig } from 'axios'
import { Workers } from '../Workers'
import { DashboardData } from '../../interface/DashboardData'
export class DailyCheckIn extends Workers {
public async doDailyCheckIn(accessToken: string, data: DashboardData) {
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', 'Starting Daily Check In')
try {
let geoLocale = data.userProfile.attributes.country
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
const jsonData = {
amount: 1,
country: geoLocale,
id: randomBytes(64).toString('hex'),
type: 101,
attributes: {
offerid: 'Gamification_Sapphire_DailyCheckIn'
}
}
const claimRequest: AxiosRequestConfig = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
},
data: JSON.stringify(jsonData)
}
const claimResponse = await this.bot.axios.request(claimRequest)
const claimedPoint = parseInt((await claimResponse.data).response?.activity?.p, 10) ?? 0
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', claimedPoint > 0 ? `Claimed ${claimedPoint} points` : 'Already claimed today')
} catch (error) {
this.bot.log(this.bot.isMobile, 'DAILY-CHECK-IN', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -0,0 +1,32 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { TIMEOUTS } from '../../constants'
export class Poll extends Workers {
async doPoll(page: Page) {
this.bot.log(this.bot.isMobile, 'POLL', 'Trying to complete poll')
try {
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.waitForSelector(buttonId, { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT }).catch((e) => {
this.bot.log(this.bot.isMobile, 'POLL', `Could not find poll button: ${e}`, 'warn')
})
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
await page.click(buttonId)
await this.bot.utils.wait(TIMEOUTS.LONG + 1000)
await page.close()
this.bot.log(this.bot.isMobile, 'POLL', 'Completed the poll successfully')
} catch (error) {
await page.close()
this.bot.log(this.bot.isMobile, 'POLL', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -0,0 +1,132 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { RETRY_LIMITS, TIMEOUTS, DELAYS } from '../../constants'
export class Quiz extends Workers {
async doQuiz(page: Page) {
this.bot.log(this.bot.isMobile, 'QUIZ', 'Trying to complete quiz')
try {
// Check if the quiz has been started or not
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: TIMEOUTS.MEDIUM_LONG }).then(() => true).catch(() => false)
if (quizNotStarted) {
await page.click('#rqStartQuiz')
} else {
this.bot.log(this.bot.isMobile, 'QUIZ', 'Quiz has already been started, trying to finish it')
}
await this.bot.utils.wait(TIMEOUTS.MEDIUM_LONG)
let quizData = await this.bot.browser.func.getQuizData(page)
// Verify quiz is actually loaded before proceeding
const firstOptionExists = await page.waitForSelector('#rqAnswerOption0', { state: 'attached', timeout: TIMEOUTS.VERY_LONG }).then(() => true).catch(() => false)
if (!firstOptionExists) {
this.bot.log(this.bot.isMobile, 'QUIZ', 'Quiz options not found - page may not have loaded correctly. Skipping.', 'warn')
await page.close()
return
}
const questionsRemaining = quizData.maxQuestions - quizData.CorrectlyAnsweredQuestionCount // Amount of questions remaining
// All questions
for (let question = 0; question < questionsRemaining; question++) {
if (quizData.numberOfOptions === 8) {
const answers: string[] = []
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: TIMEOUTS.DASHBOARD_WAIT }).catch(() => null)
if (!answerSelector) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Option ${i} not found - quiz structure may have changed. Skipping remaining options.`, 'warn')
break
}
const answerAttribute = await answerSelector?.evaluate((el: Element) => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
answers.push(`#rqAnswerOption${i}`)
}
}
// If no correct answers found, skip this question
if (answers.length === 0) {
this.bot.log(this.bot.isMobile, 'QUIZ', 'No correct answers found for 8-option quiz. Skipping.', 'warn')
await page.close()
return
}
// Click the answers
for (const answer of answers) {
await page.waitForSelector(answer, { state: 'visible', timeout: DELAYS.QUIZ_ANSWER_WAIT })
// Click the answer on page
await page.click(answer)
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) {
await page.close()
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return
}
}
// Other type quiz, lightspeed
} else if ([2, 3, 4].includes(quizData.numberOfOptions)) {
quizData = await this.bot.browser.func.getQuizData(page) // Refresh Quiz Data
const correctOption = quizData.correctAnswer
let answerClicked = false
for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: RETRY_LIMITS.QUIZ_ANSWER_TIMEOUT }).catch(() => null)
if (!answerSelector) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Option ${i} not found for ${quizData.numberOfOptions}-option quiz. Skipping.`, 'warn')
continue
}
const dataOption = await answerSelector?.evaluate((el: Element) => el.getAttribute('data-option'))
if (dataOption === correctOption) {
// Click the answer on page
await page.click(`#rqAnswerOption${i}`)
answerClicked = true
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) {
await page.close()
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return
}
break
}
}
if (!answerClicked) {
this.bot.log(this.bot.isMobile, 'QUIZ', `Could not find correct answer for ${quizData.numberOfOptions}-option quiz. Skipping.`, 'warn')
await page.close()
return
}
await this.bot.utils.wait(DELAYS.QUIZ_ANSWER_WAIT)
}
}
// Done with
await this.bot.utils.wait(DELAYS.QUIZ_ANSWER_WAIT)
await page.close()
this.bot.log(this.bot.isMobile, 'QUIZ', 'Completed the quiz successfully')
} catch (error) {
await this.bot.browser.utils.captureDiagnostics(page, 'quiz_error')
await page.close()
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -0,0 +1,73 @@
import { randomBytes } from 'crypto'
import { AxiosRequestConfig } from 'axios'
import { Workers } from '../Workers'
import { DashboardData } from '../../interface/DashboardData'
export class ReadToEarn extends Workers {
public async doReadToEarn(accessToken: string, data: DashboardData) {
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Starting Read to Earn')
try {
let geoLocale = data.userProfile.attributes.country
geoLocale = (this.bot.config.searchSettings.useGeoLocaleQueries && geoLocale.length === 2) ? geoLocale.toLowerCase() : 'us'
const userDataRequest: AxiosRequestConfig = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me',
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
}
}
const userDataResponse = await this.bot.axios.request(userDataRequest)
const userData = (await userDataResponse.data).response
let userBalance = userData.balance
const jsonData = {
amount: 1,
country: geoLocale,
id: '1',
type: 101,
attributes: {
offerid: 'ENUS_readarticle3_30points'
}
}
const articleCount = 10
for (let i = 0; i < articleCount; ++i) {
jsonData.id = randomBytes(64).toString('hex')
const claimRequest = {
url: 'https://prod.rewardsplatform.microsoft.com/dapi/me/activities',
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Rewards-Country': geoLocale,
'X-Rewards-Language': 'en'
},
data: JSON.stringify(jsonData)
}
const claimResponse = await this.bot.axios.request(claimRequest)
const newBalance = (await claimResponse.data).response.balance
if (newBalance == userBalance) {
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Read all available articles')
break
} else {
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', `Read article ${i + 1} of ${articleCount} max | Gained ${newBalance - userBalance} Points`)
userBalance = newBalance
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.min), this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.max))))
}
}
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'Completed Read to Earn')
} catch (error) {
this.bot.log(this.bot.isMobile, 'READ-TO-EARN', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -0,0 +1,458 @@
import { Page } from 'rebrowser-playwright'
import { platform } from 'os'
import { Workers } from '../Workers'
import { Counters, DashboardData } from '../../interface/DashboardData'
import { GoogleSearch } from '../../interface/Search'
import { AxiosRequestConfig } from 'axios'
type GoogleTrendsResponse = [
string,
[
string,
...null[],
[string, ...string[]]
][]
];
export class Search extends Workers {
private bingHome = 'https://bing.com'
private searchPageURL = ''
public async doSearch(page: Page, data: DashboardData) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Starting Bing searches')
page = await this.bot.browser.utils.getLatestTab(page)
let searchCounters: Counters = await this.bot.browser.func.getSearchPoints()
let missingPoints = this.calculatePoints(searchCounters)
if (missingPoints === 0) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Bing searches have already been completed')
return
}
// Generate search queries (primary: Google Trends)
const geo = this.bot.config.searchSettings.useGeoLocaleQueries ? data.userProfile.attributes.country : 'US'
let googleSearchQueries = await this.getGoogleTrends(geo)
// Fallback: if trends failed or insufficient, sample from local queries file
if (!googleSearchQueries.length || googleSearchQueries.length < 10) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Primary trends source insufficient, falling back to local queries.json', 'warn')
try {
const local = await import('../queries.json')
// Flatten & sample
const sampleSize = Math.max(5, Math.min(this.bot.config.searchSettings.localFallbackCount || 25, local.default.length))
const sampled = this.bot.utils.shuffleArray(local.default).slice(0, sampleSize)
googleSearchQueries = sampled.map((x: { title: string; queries: string[] }) => ({ topic: x.queries[0] || x.title, related: x.queries.slice(1) }))
} catch (e) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Failed loading local queries fallback: ' + (e instanceof Error ? e.message : e), 'error')
}
}
if (this.bot.config.queryDiversity?.enabled && this.bot.queryEngine) {
try {
const targetCount = Math.max(20, missingPoints * 2)
const extraTerms = await this.bot.queryEngine.fetchQueries(targetCount)
if (extraTerms.length) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Query diversity enabled — adding ${extraTerms.length} mixed-source terms`)
googleSearchQueries.push(...extraTerms.map(term => ({ topic: term, related: [] })))
}
} catch (err) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Query diversity error: ${err instanceof Error ? err.message : err}`, 'warn')
}
}
googleSearchQueries = this.bot.utils.shuffleArray(googleSearchQueries)
// Deduplicate topics
const seen = new Set<string>()
googleSearchQueries = googleSearchQueries.filter(q => {
if (seen.has(q.topic.toLowerCase())) return false
seen.add(q.topic.toLowerCase())
return true
})
// Go to bing
await page.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
await this.bot.utils.wait(2000)
await this.bot.browser.utils.tryDismissAllMessages(page)
let stagnation = 0 // consecutive searches without point progress
const queries: string[] = []
// 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) })
// Loop over Google search queries
for (let i = 0; i < queries.length; i++) {
const query = queries[i] as string
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `${missingPoints} Points Remaining | Query: ${query}`)
searchCounters = await this.bingSearch(page, query)
const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before
if (newMissingPoints === missingPoints) {
stagnation++
} else {
stagnation = 0
}
missingPoints = newMissingPoints
if (missingPoints === 0) break
// Only for mobile searches
if (stagnation > 5 && this.bot.isMobile) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 5 iterations, likely bad User-Agent', 'warn')
break
}
// If we didn't gain points for 10 iterations, assume it's stuck
if (stagnation > 10) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search didn\'t gain point for 10 iterations aborting searches', 'warn')
stagnation = 0 // allow fallback loop below
break
}
}
// Only for mobile searches
if (missingPoints > 0 && this.bot.isMobile) {
return
}
// If we still got remaining search queries, generate extra ones
if (missingPoints > 0) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Search completed but we're missing ${missingPoints} points, generating extra searches`)
let i = 0
let fallbackRounds = 0
const extraRetries = this.bot.config.searchSettings.extraFallbackRetries || 1
while (missingPoints > 0 && fallbackRounds <= extraRetries) {
const query = googleSearchQueries[i++] as GoogleSearch
if (!query) break
// Get related search terms to the Google search queries
const relatedTerms = await this.getRelatedTerms(query?.topic)
if (relatedTerms.length > 3) {
// Search for the first 2 related terms
for (const term of relatedTerms.slice(1, 3)) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING-EXTRA', `${missingPoints} Points Remaining | Query: ${term}`)
searchCounters = await this.bingSearch(page, term)
const newMissingPoints = this.calculatePoints(searchCounters)
// If the new point amount is the same as before
if (newMissingPoints === missingPoints) {
stagnation++
} else {
stagnation = 0
}
missingPoints = newMissingPoints
// If we satisfied the searches
if (missingPoints === 0) {
break
}
// Try 5 more times, then we tried a total of 15 times, fair to say it's stuck
if (stagnation > 5) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING-EXTRA', 'Search didn\'t gain point for 5 iterations aborting searches', 'warn')
return
}
}
fallbackRounds++
}
}
}
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Completed searches')
}
private async bingSearch(searchPage: Page, query: string) {
const platformControlKey = platform() === 'darwin' ? 'Meta' : 'Control'
// Try a max of 5 times
for (let i = 0; i < 5; i++) {
try {
// This page had already been set to the Bing.com page or the previous search listing, we just need to select it
searchPage = await this.bot.browser.utils.getLatestTab(searchPage)
// Go to top of the page
await searchPage.evaluate(() => {
window.scrollTo(0, 0)
})
await this.bot.utils.wait(500)
const searchBar = '#sb_form_q'
// Prefer attached over visible to avoid strict visibility waits when overlays exist
const box = searchPage.locator(searchBar)
await box.waitFor({ state: 'attached', timeout: 15000 })
// Try dismissing overlays before interacting
await this.bot.browser.utils.tryDismissAllMessages(searchPage)
await this.bot.utils.wait(200)
let navigatedDirectly = false
try {
// Try focusing and filling instead of clicking (more reliable on mobile)
await box.focus({ timeout: 2000 }).catch(() => { /* ignore focus errors */ })
await box.fill('')
await this.bot.utils.wait(200)
await searchPage.keyboard.down(platformControlKey)
await searchPage.keyboard.press('A')
await searchPage.keyboard.press('Backspace')
await searchPage.keyboard.up(platformControlKey)
await box.type(query, { delay: 20 })
await searchPage.keyboard.press('Enter')
} catch (typeErr) {
// As a robust fallback, navigate directly to the search results URL
const q = encodeURIComponent(query)
const url = `https://www.bing.com/search?q=${q}`
await searchPage.goto(url)
navigatedDirectly = true
}
await this.bot.utils.wait(3000)
// Bing.com in Chrome opens a new tab when searching via Enter; if we navigated directly, stay on current tab
const resultPage = navigatedDirectly ? searchPage : await this.bot.browser.utils.getLatestTab(searchPage)
this.searchPageURL = new URL(resultPage.url()).href // Set the results page
await this.bot.browser.utils.reloadBadPage(resultPage)
if (this.bot.config.searchSettings.scrollRandomResults) {
await this.bot.utils.wait(2000)
await this.randomScroll(resultPage)
}
if (this.bot.config.searchSettings.clickRandomResults) {
await this.bot.utils.wait(2000)
await this.clickRandomLink(resultPage)
}
// Delay between searches
const minDelay = this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.min)
const maxDelay = this.bot.utils.stringToMs(this.bot.config.searchSettings.searchDelay.max)
const adaptivePad = Math.min(4000, Math.max(0, Math.floor(Math.random() * 800)))
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(minDelay, maxDelay)) + adaptivePad)
return await this.bot.browser.func.getSearchPoints()
} catch (error) {
if (i === 5) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Failed after 5 retries... An error occurred:' + error, 'error')
break
}
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search failed, An error occurred:' + error, 'error')
this.bot.log(this.bot.isMobile, 'SEARCH-BING', `Retrying search, attempt ${i}/5`, 'warn')
// Reset the tabs
const lastTab = await this.bot.browser.utils.getLatestTab(searchPage)
await this.closeTabs(lastTab)
await this.bot.utils.wait(4000)
}
}
this.bot.log(this.bot.isMobile, 'SEARCH-BING', 'Search failed after 5 retries, ending', 'error')
return await this.bot.browser.func.getSearchPoints()
}
private async getGoogleTrends(geoLocale: string = 'US'): Promise<GoogleSearch[]> {
const queryTerms: GoogleSearch[] = []
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Generating search queries, can take a while! | GeoLocale: ${geoLocale}`)
try {
const request: AxiosRequestConfig = {
url: 'https://trends.google.com/_/TrendsUi/data/batchexecute',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
data: `f.req=[[[i0OFE,"[null, null, \\"${geoLocale.toUpperCase()}\\", 0, null, 48]"]]]`
}
const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyGoogleTrends)
const rawText = response.data
const trendsData = this.extractJsonFromResponse(rawText)
if (!trendsData) {
throw this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'Failed to parse Google Trends response', 'error')
}
const mappedTrendsData = trendsData.map(query => [query[0], query[9]!.slice(1)])
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Found ${mappedTrendsData.length} search queries for ${geoLocale}`)
if (mappedTrendsData.length < 30 && geoLocale.toUpperCase() !== 'US') {
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', `Insufficient search queries (${mappedTrendsData.length} < 30), falling back to US`, 'warn')
return this.getGoogleTrends()
}
for (const [topic, relatedQueries] of mappedTrendsData) {
queryTerms.push({
topic: topic as string,
related: relatedQueries as string[]
})
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-GOOGLE-TRENDS', 'An error occurred:' + error, 'error')
}
return queryTerms
}
private extractJsonFromResponse(text: string): GoogleTrendsResponse[1] | null {
const lines = text.split('\n')
for (const line of lines) {
const trimmed = line.trim()
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
return JSON.parse(JSON.parse(trimmed)[0][2])[1]
} catch {
continue
}
}
}
return null
}
private async getRelatedTerms(term: string): Promise<string[]> {
try {
const request = {
url: `https://api.bing.com/osjson.aspx?query=${term}`,
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
const response = await this.bot.axios.request(request, this.bot.config.proxy.proxyBingTerms)
return response.data[1] as string[]
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-BING-RELATED', 'An error occurred:' + error, 'error')
}
return []
}
private async randomScroll(page: Page) {
try {
const viewportHeight = await page.evaluate(() => window.innerHeight)
const totalHeight = await page.evaluate(() => document.body.scrollHeight)
const randomScrollPosition = Math.floor(Math.random() * (totalHeight - viewportHeight))
await page.evaluate((scrollPos: number) => {
window.scrollTo(0, scrollPos)
}, randomScrollPosition)
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
}
}
private async clickRandomLink(page: Page) {
try {
await page.click('#b_results .b_algo h2', { timeout: 2000 }).catch(() => { }) // Since we don't really care if it did it or not
// Only used if the browser is not the edge browser (continue on Edge popup)
await this.closeContinuePopup(page)
// Stay for 10 seconds for page to load and "visit"
await this.bot.utils.wait(10000)
// Will get current tab if no new one is created, this will always be the visited site or the result page if it failed to click
let lastTab = await this.bot.browser.utils.getLatestTab(page)
let lastTabURL = new URL(lastTab.url()) // Get new tab info, this is the website we're visiting
// Check if the URL is different from the original one, don't loop more than 5 times.
let i = 0
while (lastTabURL.href !== this.searchPageURL && i < 5) {
await this.closeTabs(lastTab)
// End of loop, refresh lastPage
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
lastTabURL = new URL(lastTab.url()) // Get new tab info
i++
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-RANDOM-CLICK', 'An error occurred:' + error, 'error')
}
}
private async closeTabs(lastTab: Page) {
const browser = lastTab.context()
const tabs = browser.pages()
try {
if (tabs.length > 2) {
// If more than 2 tabs are open, close the last tab
await lastTab.close()
this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', `More than 2 were open, closed the last tab: "${new URL(lastTab.url()).host}"`)
} else if (tabs.length === 1) {
// If only 1 tab is open, open a new one to search in
const newPage = await browser.newPage()
await this.bot.utils.wait(1000)
await newPage.goto(this.bingHome)
await this.bot.utils.wait(3000)
this.searchPageURL = newPage.url()
this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', 'There was only 1 tab open, crated a new one')
} else {
// Else reset the last tab back to the search listing or Bing.com
lastTab = await this.bot.browser.utils.getLatestTab(lastTab)
await lastTab.goto(this.searchPageURL ? this.searchPageURL : this.bingHome)
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-CLOSE-TABS', 'An error occurred:' + error, 'error')
}
}
private calculatePoints(counters: Counters) {
const mobileData = counters.mobileSearch?.[0] // Mobile searches
const genericData = counters.pcSearch?.[0] // Normal searches
const edgeData = counters.pcSearch?.[1] // Edge searches
const missingPoints = (this.bot.isMobile && mobileData)
? mobileData.pointProgressMax - mobileData.pointProgress
: (edgeData ? edgeData.pointProgressMax - edgeData.pointProgress : 0)
+ (genericData ? genericData.pointProgressMax - genericData.pointProgress : 0)
return missingPoints
}
private async closeContinuePopup(page: Page) {
try {
await page.waitForSelector('#sacs_close', { timeout: 1000 })
const continueButton = await page.$('#sacs_close')
if (continueButton) {
await continueButton.click()
}
} catch (error) {
// Continue if element is not found or other error occurs
}
}
}

View File

@@ -0,0 +1,85 @@
import type { Page } from 'playwright'
import * as fs from 'fs'
import path from 'path'
import { Workers } from '../Workers'
import { DELAYS } from '../../constants'
import { MorePromotion, PromotionalItem } from '../../interface/DashboardData'
export class SearchOnBing extends Workers {
async doSearchOnBing(page: Page, activity: MorePromotion | PromotionalItem) {
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'Trying to complete SearchOnBing')
try {
await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_WAIT)
await this.bot.browser.utils.tryDismissAllMessages(page)
const query = await this.getSearchQuery(activity.title)
const searchBar = '#sb_form_q'
const box = page.locator(searchBar)
await box.waitFor({ state: 'attached', timeout: DELAYS.SEARCH_BAR_TIMEOUT })
await this.bot.browser.utils.tryDismissAllMessages(page)
await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_FOCUS)
try {
await box.focus({ timeout: DELAYS.THIS_OR_THAT_START }).catch(() => { /* ignore */ })
await box.fill('')
await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_FOCUS)
await page.keyboard.type(query, { delay: DELAYS.TYPING_DELAY })
await page.keyboard.press('Enter')
} catch {
const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}`
await page.goto(url)
}
await this.bot.utils.wait(DELAYS.SEARCH_ON_BING_COMPLETE)
await page.close()
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'Completed the SearchOnBing successfully')
} catch (error) {
await page.close()
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING', 'An error occurred:' + error, 'error')
}
}
private async getSearchQuery(title: string): Promise<string> {
interface Queries {
title: string;
queries: string[]
}
let queries: Queries[] = []
try {
if (this.bot.config.searchOnBingLocalQueries) {
const data = fs.readFileSync(path.join(__dirname, '../queries.json'), 'utf8')
queries = JSON.parse(data)
} else {
// 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/LightZirconite/Microsoft-Rewards-Rewi/refs/heads/main/src/functions/queries.json'
})
queries = response.data
}
const answers = queries.find(x => this.normalizeString(x.title) === this.normalizeString(title))
const answer = answers ? this.bot.utils.shuffleArray(answers?.queries)[0] as string : title
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', `Fetched answer: ${answer} | question: ${title}`)
return answer
} catch (error) {
this.bot.log(this.bot.isMobile, 'SEARCH-ON-BING-QUERY', 'An error occurred:' + error, 'error')
return title
}
}
private normalizeString(string: string): string {
return string.normalize('NFD').trim().toLowerCase().replace(/[^\x20-\x7E]/g, '').replace(/[?!]/g, '')
}
}

View File

@@ -0,0 +1,48 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
import { DELAYS } from '../../constants'
export class ThisOrThat extends Workers {
async doThisOrThat(page: Page) {
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'Trying to complete ThisOrThat')
try {
// Check if the quiz has been started or not
const quizNotStarted = await page.waitForSelector('#rqStartQuiz', { state: 'visible', timeout: DELAYS.THIS_OR_THAT_START }).then(() => true).catch(() => false)
if (quizNotStarted) {
await page.click('#rqStartQuiz')
} else {
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'ThisOrThat has already been started, trying to finish it')
}
await this.bot.utils.wait(DELAYS.THIS_OR_THAT_START)
// Solving
const quizData = await this.bot.browser.func.getQuizData(page)
const questionsRemaining = quizData.maxQuestions - (quizData.currentQuestionNumber - 1) // Amount of questions remaining
for (let question = 0; question < questionsRemaining; question++) {
// Since there's no solving logic yet, randomly guess to complete
const buttonId = `#rqAnswerOption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.click(buttonId)
const refreshSuccess = await this.bot.browser.func.waitForQuizRefresh(page)
if (!refreshSuccess) {
await page.close()
this.bot.log(this.bot.isMobile, 'QUIZ', 'An error occurred, refresh was unsuccessful', 'error')
return
}
}
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'Completed the ThisOrThat successfully')
} catch (error) {
await page.close()
this.bot.log(this.bot.isMobile, 'THIS-OR-THAT', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -0,0 +1,23 @@
import { Page } from 'rebrowser-playwright'
import { Workers } from '../Workers'
export class UrlReward extends Workers {
async doUrlReward(page: Page) {
this.bot.log(this.bot.isMobile, 'URL-REWARD', 'Trying to complete UrlReward')
try {
await this.bot.utils.wait(2000)
await page.close()
this.bot.log(this.bot.isMobile, 'URL-REWARD', 'Completed the UrlReward successfully')
} catch (error) {
await page.close()
this.bot.log(this.bot.isMobile, 'URL-REWARD', 'An error occurred:' + error, 'error')
}
}
}

289
src/functions/queries.json Normal file
View File

@@ -0,0 +1,289 @@
[
{
"title": "Houses near you",
"queries": [
"Houses near me"
]
},
{
"title": "Feeling symptoms?",
"queries": [
"Rash on forearm",
"Stuffy nose",
"Tickling cough"
]
},
{
"title": "Get your shopping done faster",
"queries": [
"Buy PS5",
"Buy Xbox",
"Chair deals"
]
},
{
"title": "Translate anything",
"queries": [
"Translate welcome home to Korean",
"Translate welcome home to Japanese",
"Translate goodbye to Japanese"
]
},
{
"title": "Search the lyrics of a song",
"queries": [
"Debarge rhythm of the night lyrics"
]
},
{
"title": "Let's watch that movie again!",
"queries": [
"Alien movie",
"Aliens movie",
"Alien 3 movie",
"Predator movie"
]
},
{
"title": "Plan a quick getaway",
"queries": [
"Flights Amsterdam to Tokyo",
"Flights New York to Tokyo"
]
},
{
"title": "Discover open job roles",
"queries": [
"jobs at Microsoft",
"Microsoft Job Openings",
"Jobs near me",
"jobs at Boeing worked"
]
},
{
"title": "You can track your package",
"queries": [
"USPS tracking"
]
},
{
"title": "Find somewhere new to explore",
"queries": [
"Directions to Berlin",
"Directions to Tokyo",
"Directions to New York"
]
},
{
"title": "Too tired to cook tonight?",
"queries": [
"KFC near me",
"Burger King near me",
"McDonalds near me"
]
},
{
"title": "Quickly convert your money",
"queries": [
"convert 250 USD to yen",
"convert 500 USD to yen"
]
},
{
"title": "Learn to cook a new recipe",
"queries": [
"How to cook ratatouille",
"How to cook lasagna"
]
},
{
"title": "Find places to stay!",
"queries": [
"Hotels Berlin Germany",
"Hotels Amsterdam Netherlands"
]
},
{
"title": "How's the economy?",
"queries": [
"sp 500"
]
},
{
"title": "Who won?",
"queries": [
"braves score"
]
},
{
"title": "Gaming time",
"queries": [
"Overwatch video game",
"Call of duty video game"
]
},
{
"title": "Expand your vocabulary",
"queries": [
"definition definition"
]
},
{
"title": "What time is it?",
"queries": [
"Japan time",
"New York time"
]
},
{
"title": "Maisons près de chez vous",
"queries": [
"Maisons près de chez moi"
]
},
{
"title": "Vous ressentez des symptômes ?",
"queries": [
"Éruption cutanée sur l'avant-bras",
"Nez bouché",
"Toux chatouilleuse"
]
},
{
"title": "Faites vos achats plus vite",
"queries": [
"Acheter une PS5",
"Acheter une Xbox",
"Offres sur les chaises"
]
},
{
"title": "Traduisez tout !",
"queries": [
"Traduction bienvenue à la maison en coréen",
"Traduction bienvenue à la maison en japonais",
"Traduction au revoir en japonais"
]
},
{
"title": "Rechercher paroles de chanson",
"queries": [
"Paroles de Debarge rhythm of the night"
]
},
{
"title": "Et si nous regardions ce film une nouvelle fois?",
"queries": [
"Alien film",
"Film Aliens",
"Film Alien 3",
"Film Predator"
]
},
{
"title": "Planifiez une petite escapade",
"queries": [
"Vols Amsterdam-Tokyo",
"Vols New York-Tokyo"
]
},
{
"title": "Consulter postes à pourvoir",
"queries": [
"emplois chez Microsoft",
"Offres d'emploi Microsoft",
"Emplois près de chez moi",
"emplois chez Boeing"
]
},
{
"title": "Vous pouvez suivre votre colis",
"queries": [
"Suivi Chronopost"
]
},
{
"title": "Trouver un endroit à découvrir",
"queries": [
"Itinéraire vers Berlin",
"Itinéraire vers Tokyo",
"Itinéraire vers New York"
]
},
{
"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"
]
},
{
"title": "Convertissez rapidement votre argent",
"queries": [
"convertir 250 EUR en yen",
"convertir 500 EUR en yen"
]
},
{
"title": "Apprenez à cuisiner une nouvelle recette",
"queries": [
"Comment faire cuire la ratatouille",
"Comment faire cuire les lasagnes"
]
},
{
"title": "Trouvez des emplacements pour rester!",
"queries": [
"Hôtels Berlin Allemagne",
"Hôtels Amsterdam Pays-Bas"
]
},
{
"title": "Comment se porte l'économie ?",
"queries": [
"CAC 40"
]
},
{
"title": "Qui a gagné ?",
"queries": [
"score du Paris Saint-Germain"
]
},
{
"title": "Temps de jeu",
"queries": [
"Jeu vidéo Overwatch",
"Jeu vidéo Call of Duty"
]
},
{
"title": "Enrichissez votre vocabulaire",
"queries": [
"definition definition"
]
},
{
"title": "Quelle heure est-il ?",
"queries": [
"Heure du Japon",
"Heure de New York"
]
},
{
"title": "Vérifier la météo",
"queries": [
"Météo de Paris",
"Météo de la France"
]
},
{
"title": "Tenez-vous informé des sujets d'actualité",
"queries": [
"Augmentation Impots",
"Mort célébrité"
]
}
]