Support new quest to get points (#138)

* support passwordless auth (using Authenticator app)

* update readme

* feat: added mobile app tasks (daily check in + read to earn)

* fix some stuff

* make ReadToEarn use the delay config

* fix daily reward search per week

* reorder mobile tasks

* fix message

* Search fixes, reformatting and types

---------

Co-authored-by: TheNetsky <56271887+TheNetsky@users.noreply.github.com>
This commit is contained in:
HMCDAT
2024-08-30 18:26:52 +07:00
committed by GitHub
parent ce2a72ee36
commit a47b86e74d
15 changed files with 584 additions and 80 deletions

View File

@@ -8,6 +8,8 @@ import { Poll } from './activities/Poll'
import { Quiz } from './activities/Quiz'
import { ThisOrThat } from './activities/ThisOrThat'
import { UrlReward } from './activities/UrlReward'
import { ReadToEarn } from './activities/ReadToEarn'
import { DailyCheckIn } from './activities/DailyCheckIn'
import { DashboardData } from '../interface/DashboardData'
@@ -49,4 +51,14 @@ export default class Activities {
await urlReward.doUrlReward(page)
}
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)
}
}

View File

@@ -3,6 +3,9 @@ import readline from 'readline'
import { MicrosoftRewardsBot } from '../index'
import { saveSessionData } from '../util/Load'
import axios from 'axios'
import { OAuth } from '../interface/OAuth'
import * as crypto from 'crypto'
const rl = readline.createInterface({
input: process.stdin,
@@ -12,6 +15,11 @@ const rl = readline.createInterface({
export class Login {
private bot: MicrosoftRewardsBot
private clientId: string = '0000000040170455'
private authBaseUrl: string = 'https://login.live.com/oauth20_authorize.srf'
private redirectUrl: string = 'https://login.live.com/oauth20_desktop.srf'
private tokenUrl: string = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token'
private scope: string = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL'
constructor(bot: MicrosoftRewardsBot) {
this.bot = bot
@@ -200,4 +208,50 @@ export class Login {
}
}
async getMobileAccessToken(page: Page, email: string) {
const authorizeUrl = new URL(this.authBaseUrl)
authorizeUrl.searchParams.append('response_type', 'code')
authorizeUrl.searchParams.append('client_id', this.clientId)
authorizeUrl.searchParams.append('redirect_uri', this.redirectUrl)
authorizeUrl.searchParams.append('scope', this.scope)
authorizeUrl.searchParams.append('state', crypto.randomBytes(16).toString('hex'))
authorizeUrl.searchParams.append('access_type', 'offline_access')
authorizeUrl.searchParams.append('login_hint', email)
await page.goto(authorizeUrl.href)
const currentUrl = new URL(page.url())
let code: string
// eslint-disable-next-line no-constant-condition
while (true) {
this.bot.log('LOGIN-APP', 'Waiting for authorization')
if (currentUrl.hostname === 'login.live.com' && currentUrl.pathname === '/oauth20_desktop.srf') {
code = currentUrl.searchParams.get('code')!
break
}
}
const body = new URLSearchParams()
body.append('grant_type', 'authorization_code')
body.append('client_id', this.clientId)
body.append('code', code)
body.append('redirect_uri', this.redirectUrl)
const tokenRequest = {
url: this.tokenUrl,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: body.toString()
}
const tokenResponse = await axios(tokenRequest)
const tokenData: OAuth = await tokenResponse.data
this.bot.log('LOGIN-APP', 'Successfully authorized')
return tokenData.access_token
}
}

View File

@@ -58,7 +58,7 @@ export class Workers {
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: 5_000 }).catch(() => { })
await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { })
await this.solveActivities(page, activitiesUncompleted, punchCard)

View File

@@ -0,0 +1,48 @@
import axios from 'axios'
import { randomBytes } from 'crypto'
import { Workers } from '../Workers'
import { DashboardData } from '../../interface/DashboardData'
export class DailyCheckIn extends Workers {
public async doDailyCheckIn(accessToken: string, data: DashboardData) {
this.bot.log('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 = {
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 axios(claimRequest)
const claimedPoint = parseInt((await claimResponse.data).response.activity.p)
this.bot.log('DAILY-CHECK-IN', claimedPoint > 0 ? `Claimed ${claimedPoint} points` : 'Already claimed today')
} catch (error) {
this.bot.log('DAILY-CHECK-IN', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -0,0 +1,73 @@
import axios from 'axios'
import { randomBytes } from 'crypto'
import { Workers } from '../Workers'
import { DashboardData } from '../../interface/DashboardData'
export class ReadToEarn extends Workers {
public async doReadToEarn(accessToken: string, data: DashboardData) {
this.bot.log('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 = {
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 axios(userDataRequest)
const userData = (await userDataResponse.data).response
let balance: number = userData.balance
const jsonData = {
amount: 1,
country: geoLocale,
id: '1',
type: 101,
attributes: {
offerid: 'ENUS_readarticle3_30points'
}
}
for (let i = 0; i < 10; ++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 axios(claimRequest)
const newBalance = (await claimResponse.data).response.balance
if (newBalance == balance) {
this.bot.log('READ-TO-EARN', 'Read all available articles')
break
} else {
balance = newBalance
this.bot.log('READ-TO-EARN', `Read article ${i + 1}`)
await this.bot.utils.wait(Math.floor(this.bot.utils.randomNumber(this.bot.config.searchSettings.searchDelay.min, this.bot.config.searchSettings.searchDelay.max)))
}
}
this.bot.log('READ-TO-EARN', 'Completed Read to Earn')
} catch (error) {
this.bot.log('READ-TO-EARN', 'An error occurred:' + error, 'error')
}
}
}

View File

@@ -133,15 +133,19 @@ export class Search extends Workers {
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 {
// Go to top of the page
await searchPage.evaluate(() => {
window.scrollTo(0, 0)
})
// Set it since params get added after visiting
this.searchPageURL = searchPage.url()
await this.bot.utils.wait(500)
const searchBar = '#sb_form_q'
@@ -155,14 +159,19 @@ export class Search extends Workers {
await searchPage.keyboard.type(query)
await searchPage.keyboard.press('Enter')
await this.bot.utils.wait(1000)
// Bing.com in Chrome opens a new tab when searching
const resultPage = await this.bot.browser.utils.getLatestTab(searchPage)
if (this.bot.config.searchSettings.scrollRandomResults) {
await this.bot.utils.wait(2000)
await this.randomScroll(searchPage)
await this.randomScroll(resultPage)
}
if (this.bot.config.searchSettings.clickRandomResults) {
await this.bot.utils.wait(2000)
await this.clickRandomLink(searchPage)
await this.clickRandomLink(resultPage)
}
// Delay between searches
@@ -181,7 +190,7 @@ export class Search extends Workers {
// Reset the tabs
const lastTab = await this.bot.browser.utils.getLatestTab(searchPage)
await this.closeTabs(lastTab, this.searchPageURL)
await this.closeTabs(lastTab)
await this.bot.utils.wait(4000)
}
@@ -262,11 +271,13 @@ export class Search extends Workers {
private async randomScroll(page: Page) {
try {
const scrollAmount = this.bot.utils.randomNumber(5, 5000)
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((scrollAmount) => {
window.scrollBy(0, scrollAmount)
}, scrollAmount)
await page.evaluate((scrollPos) => {
window.scrollTo(0, scrollPos)
}, randomScrollPosition)
} catch (error) {
this.bot.log('SEARCH-RANDOM-SCROLL', 'An error occurred:' + error, 'error')
@@ -275,23 +286,22 @@ export class Search extends Workers {
private async clickRandomLink(page: Page) {
try {
const searchListingURL = new URL(page.url()) // Get searchPage info before clicking
await page.click('#b_results .b_algo h2').catch(() => { }) // Since we don't really care if it did it or not
await page.click('#b_results .b_algo h2', { timeout: 2000 }).catch(() => { }) // Since we don't really care if it did it or not
// Will get current tab if no new one is created
let lastTab = await this.bot.browser.utils.getLatestTab(page)
// Let website load, if it doesn't load within 5 sec. exit regardless
await this.bot.utils.wait(5000)
// Stay for 10 seconds
await this.bot.utils.wait(10_000)
let lastTabURL = new URL(lastTab.url()) // Get new tab info
// Check if the URL is different from the original one, don't loop more than 5 times.
let i = 0
while (lastTabURL.href !== searchListingURL.href && i < 5) {
while (lastTabURL.href !== this.searchPageURL && i < 5) {
await this.closeTabs(lastTab, searchListingURL.href)
await this.closeTabs(lastTab)
// End of loop, refresh lastPage
lastTab = await this.bot.browser.utils.getLatestTab(page) // Finally update the lastTab var again
@@ -304,26 +314,25 @@ export class Search extends Workers {
}
}
private async closeTabs(lastTab: Page, url: string) {
private async closeTabs(lastTab: Page) {
const browser = lastTab.context()
const tabs = browser.pages()
// If more than 3 tabs are open, close the last tab
// If more than 2 tabs are open, close the last tab
if (tabs.length > 2) {
await lastTab.close()
// If only 1 tab is open, open a new one to search in
} else if (tabs.length === 1) {
const newPage = await browser.newPage()
await newPage.goto(url)
await newPage.goto(this.searchPageURL)
// Else go back one page
// Else go back one page, this means the correct amount is open
} else {
await lastTab.goBack()
await lastTab.goBack().catch(() => { })
}
}
private calculatePoints(counters: Counters) {
const mobileData = counters.mobileSearch?.[0] // Mobile searches
const genericData = counters.pcSearch?.[0] // Normal searches