Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions scripts/api/readingBrowserIdle.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
const assert = require('node:assert/strict')
const test = require('node:test')
require('../../node_modules/ts-node/register/transpile-only.js')
const Activities = require('../../src/functions/Activities.ts').default
const { createActivityBot, createFlowBot, account } = require('./workerGaps.fixture.cjs')

function setupFlow(t, { needMobile = false, punchCards = false, restoreError = false, closeError = false } = {}) {
const { bot, events } = createFlowBot(t, 'browser')
bot.config.workers = { doReadToEarn: true, doMobileSearch: true, doPunchCards: punchCards }
bot.config.ensureStreakProtection = false
let session = 0
bot.browserFactory.createBrowser = t.mock.fn(async () => {
session++
events.push('create:' + session)
if (session > 1 && restoreError) throw new Error('restore failed')
return { context: { newPage: async () => ({ id: session }), cookies: async () => [] }, fingerprint: {} }
})
bot.browser.func.closeBrowser = t.mock.fn(async () => {
events.push('close')
if (closeError) throw new Error('close failed')
})
bot.login.login = t.mock.fn(async page => events.push('login:' + page.id))
bot.activities.doReadToEarn = t.mock.fn(async cb => {
events.push('read')
if (cb) {
events.push('read-release')
try {
await cb()
} catch {
events.push('release-error')
}
}
events.push('read-wait')
return true
})
bot.activities.doPunchCardsMobile = t.mock.fn(async () => {
events.push('punch:' + bot.mainMobilePage.id)
return false
})
bot.searchManager.getSearchPoints = t.mock.fn(async () => ({ doMobile: needMobile, doDesktop: false }))
bot.searchManager.searchMobile = t.mock.fn(async () => {
events.push('search:' + bot.mainMobilePage.id)
return 0
})
return { bot, events }
}

test('closes the idle mobile browser before reading waits', async t => {
const { bot, events } = setupFlow(t)
await bot.Main(account)
assert.equal(bot.browserFactory.createBrowser.mock.callCount(), 1)
assert.ok(events.indexOf('close') < events.indexOf('read-wait'))
})

test('restores the mobile session before punchcards and reuses it for mobile search', async t => {
const { bot, events } = setupFlow(t, { needMobile: true, punchCards: true })
await bot.Main(account)
assert.equal(bot.browserFactory.createBrowser.mock.callCount(), 3)
assert.ok(events.indexOf('read-wait') < events.indexOf('login:2'))
assert.ok(events.indexOf('login:2') < events.indexOf('punch:2'))
assert.ok(events.indexOf('punch:2') < events.indexOf('search:2'))
assert.equal(bot.browser.func.closeBrowser.mock.callCount(), 3)
})

test('restores the mobile session before search when punchcards are disabled', async t => {
const { bot, events } = setupFlow(t, { needMobile: true })
await bot.Main(account)
assert.ok(events.indexOf('login:2') < events.indexOf('search:2'))
})

test('a failed session restore does not run punchcards or search on a closed page', async t => {
const { bot } = setupFlow(t, { needMobile: true, punchCards: true, restoreError: true })
await assert.rejects(() => bot.Main(account), /restore failed/)
assert.equal(bot.activities.doPunchCardsMobile.mock.callCount(), 0)
assert.equal(bot.searchManager.searchMobile.mock.callCount(), 0)
})

test('a failed close keeps the session available for cleanup', async t => {
const { bot, events } = setupFlow(t, { closeError: true })
await assert.rejects(() => bot.Main(account), /close failed/)
assert.ok(events.includes('release-error'))
assert.ok(bot.browser.func.closeBrowser.mock.callCount() >= 2)
})

for (const [name, configure] of [
['parallel search', bot => (bot.config.searchSettings.parallelSearching = true)],
['bonus search', bot => (bot.config.workers.doBonusSearches = true)],
['background Edge activity', bot => (bot.config.experimental.edgeBrowsing = true)],
['API search', bot => (bot.config.experimental.apiSearch = true)]
]) {
test(name + ' retains its original browser lifecycle', async t => {
const { bot, events } = setupFlow(t)
configure(bot)
bot.searchManager.bonusMobile = async () => 0
bot.activities.doEdgeBrowsing = async () => {}
await bot.Main(account)
assert.equal(events.includes('read-release'), false)
})
}

test('releases once before the first reading wait without changing requests or intervals', async t => {
const { bot } = createActivityBot(t)
bot.accessToken = 'test-token'
let balance = 100
bot.http.request = t.mock.fn(async () => ({ status: 200, data: { response: { balance: (balance += 3) } } }))
let released = 0
await new Activities(bot).doReadToEarn(async () => {
assert.equal(bot.http.request.mock.callCount(), 1)
assert.equal(bot.utils.wait.mock.callCount(), 0)
released++
})
assert.equal(released, 1)
assert.equal(bot.http.request.mock.callCount(), 10)
assert.equal(bot.utils.wait.mock.callCount(), 9)
})

test('a first response without points does not release or wait', async t => {
const { bot } = createActivityBot(t)
bot.accessToken = 'test-token'
const release = t.mock.fn(async () => {})
await new Activities(bot).doReadToEarn(release)
assert.equal(release.mock.callCount(), 0)
assert.equal(bot.utils.wait.mock.callCount(), 0)
})

test('a release error preserves reading requests and intervals', async t => {
const { bot } = createActivityBot(t)
bot.accessToken = 'test-token'
let balance = 100
bot.http.request = t.mock.fn(async () => ({ status: 200, data: { response: { balance: (balance += 3) } } }))
await new Activities(bot).doReadToEarn(async () => {
throw new Error('close failed')
})
assert.equal(bot.http.request.mock.callCount(), 10)
assert.equal(bot.utils.wait.mock.callCount(), 9)
assert.ok(bot.logger.warn.mock.calls.some(call => String(call.arguments[2]).includes('close failed')))
})
4 changes: 2 additions & 2 deletions src/functions/Activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export default class Activities {
await urlReward.doAppReward(promotion)
}

doReadToEarn = async (): Promise<boolean> => {
doReadToEarn = async (beforeFirstWait?: () => Promise<void>): Promise<boolean> => {
const activity = new ReadToEarn(this.bot)
await activity.doReadToEarn()
await activity.doReadToEarn(beforeFirstWait)
return activity.didExecute()
}

Expand Down
13 changes: 12 additions & 1 deletion src/functions/activities/app/ReadToEarn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { randomBytes } from 'crypto'
import { BaseActivity } from '../BaseActivity'

export class ReadToEarn extends BaseActivity {
public async doReadToEarn() {
public async doReadToEarn(beforeFirstWait?: () => Promise<void>) {
if (!this.bot.accessToken) {
this.bot.logger.warn(this.bot.isMobile, 'READ-TO-EARN', '跳过:应用访问令牌不可用,此活动需要它!')
return
Expand Down Expand Up @@ -108,6 +108,17 @@ export class ReadToEarn extends BaseActivity {
)

if (i < articleCount - 1) {
if (i === 0 && beforeFirstWait) {
try {
await beforeFirstWait()
} catch (error) {
this.bot.logger.warn(
this.bot.isMobile,
'READ-TO-EARN',
'释放空闲浏览器失败:' + (error instanceof Error ? error.message : String(error))
)
}
}
await this.bot.utils.wait(this.bot.utils.randomDelay(delayMin, delayMax))
}
}
Expand Down
20 changes: 18 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,12 +729,22 @@ export class MicrosoftRewardsBot {
const closeMobileSession = async (): Promise<void> => {
const session = mobileSession
if (!session) return
mobileSession = null

await executionContext.run({ isMobile: true, account }, async () => {
await this.browser.func.checkpointActiveSession('PRE-BROWSER-CLOSE')
await this.browser.func.closeBrowser(session.context, accountEmail)
})
mobileSession = null
}

const restoreMobileSession = async (): Promise<void> => {
if (mobileSession) return
mobileSession = await this.browserFactory.createBrowser(account)
this.mainMobilePage = await mobileSession.context.newPage()
this.fingerprintMobile = mobileSession.fingerprint
await this.login.login(this.mainMobilePage, account)
await this.browser.func.checkpointActiveSession('LOGIN-CHECKPOINT')
this.cookies.mobile = await mobileSession.context.cookies()
}

const closeDesktopSession = async (): Promise<void> => {
Expand Down Expand Up @@ -1003,15 +1013,21 @@ export class MicrosoftRewardsBot {
if (await this.activities.doAppPromotions(appData)) await this.gapBetweenWorkers()
}
if (appAvailable && this.config.workers.doReadToEarn) {
if (await this.activities.doReadToEarn()) await this.gapBetweenWorkers()
const releaseDuringReading =
!apiSearch && !parallel && !doBonus && !this.config.experimental.edgeBrowsing
? closeMobileSession
: undefined
if (await this.activities.doReadToEarn(releaseDuringReading)) await this.gapBetweenWorkers()
}
if (this.config.workers.doPunchCards) {
if (!mobileSession && !apiSearch) await restoreMobileSession()
if (await this.activities.doPunchCardsMobile(data)) await this.gapBetweenWorkers()
}

const plan = await this.searchManager.getSearchPoints()
const doMobileSearch = plan.doMobile
const doDesktopSearch = plan.doDesktop
if (doMobileSearch && !apiSearch && !mobileSession) await restoreMobileSession()

const desktopBrowserNeeded =
(this.config.workers.doPunchCards || doVisualSearch || (doDesktopSearch && !apiSearch)) &&
Expand Down
Loading