7 Commits

Author SHA1 Message Date
TheNetsky
d5fd06d229 1.5.3
Fix issue with pop-up dismissal
2025-04-30 16:20:56 +02:00
TheNetsky
0ddc964878 1.5.2
- Updated packages
- Cleanup
- Improved message dismissal
- Improved login functionality

With help from @LightZirconite
2025-04-24 14:50:22 +02:00
CNOCM
325bf65b30 Update README (#264)
* Update Search.ts

* Update README.md
2025-03-03 10:59:06 +01:00
HMCDAT
6f19bd4b0e Update Dockerfile (#262) 2025-02-28 12:34:47 +01:00
AariaX
caf6a42a38 Make shuffleArray more random? (#254) 2025-02-25 16:14:51 +01:00
TheNetsky
352d47229b Merge branch 'main' of https://github.com/TheNetsky/Microsoft-Rewards-Script 2025-02-24 17:59:28 +01:00
TheNetsky
9a12ee1ec8 Fix geoLocale not being uppercase 2025-02-24 17:59:20 +01:00
14 changed files with 109 additions and 63 deletions

View File

@@ -37,6 +37,7 @@ RUN touch /var/log/cron.log
# Define the command to run your application with cron optionally # Define the command to run your application with cron optionally
CMD ["sh", "-c", "echo \"$TZ\" > /etc/timezone && \ CMD ["sh", "-c", "echo \"$TZ\" > /etc/timezone && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \ dpkg-reconfigure -f noninteractive tzdata && \
envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron && \ envsubst < /etc/cron.d/microsoft-rewards-cron.template > /etc/cron.d/microsoft-rewards-cron && \
chmod 0644 /etc/cron.d/microsoft-rewards-cron && \ chmod 0644 /etc/cron.d/microsoft-rewards-cron && \

View File

@@ -57,7 +57,7 @@ A basic docker `compose.yaml` is provided. Follow these steps to configure and r
| baseURL | MS Rewards page | `https://rewards.bing.com` | | baseURL | MS Rewards page | `https://rewards.bing.com` |
| sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) | | sessionPath | Path to where you want sessions/fingerprints to be stored | `sessions` (In ./browser/sessions) |
| headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) | | headless | If the browser window should be visible be ran in the background | `false` (Browser is visible) |
| parallel | If you want mobile and desktop taks to run parallel or sequential| `true` | | parallel | If you want mobile and desktop tasks to run parallel or sequential| `true` |
| runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) | | runOnZeroPoints | Run the rest of the script if 0 points can be earned | `false` (Will not run on 0 points) |
| clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) | | clusters | Amount of instances ran on launch, 1 per account | `1` (Will run 1 account at the time) |
| saveFingerprint.mobile | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) | | saveFingerprint.mobile | Re-use the same fingerprint each time | `false` (Will generate a new fingerprint each time) |
@@ -74,8 +74,8 @@ A basic docker `compose.yaml` is provided. Follow these steps to configure and r
| searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) | | searchSettings.useGeoLocaleQueries | Generate search queries based on your geo-location | `false` (Uses EN-US generated queries) |
| searchSettings.scrollRandomResults | Scroll randomly in search results | `true` | | searchSettings.scrollRandomResults | Scroll randomly in search results | `true` |
| searchSettings.clickRandomResults | Visit random website from search result| `true` | | searchSettings.clickRandomResults | Visit random website from search result| `true` |
| searchSettings.searchDelay | Minimum and maximum time in miliseconds between search queries | `min: 1min` `max: 2min` | | searchSettings.searchDelay | Minimum and maximum time in milliseconds between search queries | `min: 3min` `max: 5min` |
| searchSettings.retryMobileSearchAmount | Keep retrying mobile searches for specified amount | `3` | | searchSettings.retryMobileSearchAmount | Keep retrying mobile searches for specified amount | `2` |
| logExcludeFunc | Functions to exclude out of the logs and webhooks | `SEARCH-CLOSE-TABS` | | logExcludeFunc | Functions to exclude out of the logs and webhooks | `SEARCH-CLOSE-TABS` |
| webhookLogExcludeFunc | Functions to exclude out of the webhooks log | `SEARCH-CLOSE-TABS` | | webhookLogExcludeFunc | Functions to exclude out of the webhooks log | `SEARCH-CLOSE-TABS` |
| proxy.proxyGoogleTrends | Enable or disable proxying the request via set proxy | `true` (will be proxied) | | proxy.proxyGoogleTrends | Enable or disable proxying the request via set proxy | `true` (will be proxied) |

View File

@@ -1,6 +1,6 @@
{ {
"name": "microsoft-rewards-script", "name": "microsoft-rewards-script",
"version": "1.5.1", "version": "1.5.3",
"description": "Automatically do tasks for Microsoft Rewards but in TS!", "description": "Automatically do tasks for Microsoft Rewards but in TS!",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
@@ -35,11 +35,11 @@
"typescript": "^5.5.4" "typescript": "^5.5.4"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.8.4",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"fingerprint-generator": "^2.1.62", "fingerprint-generator": "^2.1.66",
"fingerprint-injector": "^2.1.62", "fingerprint-injector": "^2.1.66",
"http-proxy-agent": "^7.0.2", "http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"ms": "^2.1.3", "ms": "^2.1.3",

View File

@@ -46,7 +46,7 @@ class Browser {
const context = await newInjectedContext(browser as any, { fingerprint: fingerprint }) const context = await newInjectedContext(browser as any, { fingerprint: fingerprint })
// Set timeout to preferred amount // Set timeout to preferred amount
context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30_000)) context.setDefaultTimeout(this.bot.utils.stringToMs(this.bot.config?.globalTimeout ?? 30000))
await context.addCookies(sessionData.cookies) await context.addCookies(sessionData.cookies)

View File

@@ -298,7 +298,7 @@ export default class BrowserFunc {
async waitForQuizRefresh(page: Page): Promise<boolean> { async waitForQuizRefresh(page: Page): Promise<boolean> {
try { try {
await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10_000 }) await page.waitForSelector('span.rqMCredits', { state: 'visible', timeout: 10000 })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
return true return true

View File

@@ -11,8 +11,9 @@ export default class BrowserUtil {
this.bot = bot this.bot = bot
} }
async tryDismissAllMessages(page: Page): Promise<boolean> { async tryDismissAllMessages(page: Page): Promise<void> {
const buttons = [ const buttons = [
{ selector: 'button[type="submit"]', label: 'Submit Button' },
{ selector: '#acceptButton', label: 'AcceptButton' }, { selector: '#acceptButton', label: 'AcceptButton' },
{ selector: '.ext-secondary.ext-button', label: '"Skip for now" Button' }, { selector: '.ext-secondary.ext-button', label: '"Skip for now" Button' },
{ selector: '#iLandingViewAction', label: 'iLandingViewAction' }, { selector: '#iLandingViewAction', label: 'iLandingViewAction' },
@@ -23,27 +24,23 @@ export default class BrowserUtil {
{ selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' }, { selector: '.ms-Button.ms-Button--primary', label: 'Primary Button' },
{ selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Button' }, { selector: '.c-glyph.glyph-cancel', label: 'Mobile Welcome Button' },
{ selector: '.maybe-later', label: 'Mobile Rewards App Banner' }, { selector: '.maybe-later', label: 'Mobile Rewards App Banner' },
{ selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Accept Cookie Consent Container' }, { selector: '//div[@id="cookieConsentContainer"]//button[contains(text(), "Accept")]', label: 'Accept Cookie Consent Container', isXPath: true },
{ selector: '#bnp_btn_accept', label: 'Bing Cookie Banner' }, { selector: '#bnp_btn_accept', label: 'Bing Cookie Banner' },
{ selector: '#reward_pivot_earn', label: 'Reward Coupon Accept' } { selector: '#reward_pivot_earn', label: 'Reward Coupon Accept' }
] ]
const dismissTasks = buttons.map(async (button) => { for (const button of buttons) {
try { try {
const element = page.locator(button.selector) const element = button.isXPath ? page.locator(`xpath=${button.selector}`) : page.locator(button.selector)
if (await element.first().isVisible({ timeout: 1000 })) { await element.first().click({ timeout: 500 })
await element.first().click({ timeout: 1000 }) await page.waitForTimeout(500)
this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${button.label}`)
return true
}
} catch (error) {
// Ignore errors and continue
}
return false
})
const results = await Promise.allSettled(dismissTasks) this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${button.label}`)
return results.some(result => result.status === 'fulfilled' && result.value === true)
} catch (error) {
// Silent fail
}
}
} }
async getLatestTab(page: Page): Promise<Page> { async getLatestTab(page: Page): Promise<Page> {

View File

@@ -39,7 +39,7 @@ export class Login {
// Check if account is locked // Check if account is locked
await this.checkAccountLocked(page) await this.checkAccountLocked(page)
const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }).then(() => true).catch(() => false) const isLoggedIn = await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 }).then(() => true).catch(() => false)
if (!isLoggedIn) { if (!isLoggedIn) {
await this.execLogin(page, email, password) await this.execLogin(page, email, password)
@@ -86,30 +86,80 @@ export class Login {
} }
private async enterEmail(page: Page, email: string) { private async enterEmail(page: Page, email: string) {
const emailPrefilled = await page.waitForSelector('#userDisplayName', { timeout: 2_000 }).catch(() => null) const emailInputSelector = 'input[type="email"]'
if (emailPrefilled) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
return
}
await page.fill('#i0116', email) try {
await page.click('#idSIButton9') // Wait for email field
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully') const emailField = await page.waitForSelector(emailInputSelector, { state: 'visible', timeout: 2000 }).catch(() => null)
if (!emailField) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email field not found', 'warn')
return
}
await this.bot.utils.wait(1000)
// Check if email is prefilled
const emailPrefilled = await page.waitForSelector('#userDisplayName', { timeout: 5000 }).catch(() => null)
if (emailPrefilled) {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email already prefilled by Microsoft')
} else {
// Else clear and fill email
await page.fill(emailInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(emailInputSelector, email)
await this.bot.utils.wait(1000)
}
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Email entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after email entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Email entry failed: ${error}`, 'error')
}
} }
private async enterPassword(page: Page, password: string) { private async enterPassword(page: Page, password: string) {
const passwordInputSelector = 'input[type="password"]'
try { try {
await page.waitForSelector('#i0118', { state: 'visible', timeout: 2000 }) // Wait for password field
await this.bot.utils.wait(2000) const passwordField = await page.waitForSelector(passwordInputSelector, { state: 'visible', timeout: 5000 }).catch(() => null)
await page.fill('#i0118', password) if (!passwordField) {
await page.click('#idSIButton9') this.bot.log(this.bot.isMobile, 'LOGIN', 'Password field not found, possibly 2FA required', 'warn')
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully') await this.handle2FA(page)
} catch { return
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entry failed or 2FA required') }
await this.bot.utils.wait(1000)
// Clear and fill password
await page.fill(passwordInputSelector, '')
await this.bot.utils.wait(500)
await page.fill(passwordInputSelector, password)
await this.bot.utils.wait(1000)
const nextButton = await page.waitForSelector('button[type="submit"]', { timeout: 2000 }).catch(() => null)
if (nextButton) {
await nextButton.click()
await this.bot.utils.wait(2000)
this.bot.log(this.bot.isMobile, 'LOGIN', 'Password entered successfully')
} else {
this.bot.log(this.bot.isMobile, 'LOGIN', 'Next button not found after password entry', 'warn')
}
} catch (error) {
this.bot.log(this.bot.isMobile, 'LOGIN', `Password entry failed: ${error}`, 'error')
await this.handle2FA(page) await this.handle2FA(page)
} }
} }
private async handle2FA(page: Page) { private async handle2FA(page: Page) {
try { try {
const numberToPress = await this.get2FACode(page) const numberToPress = await this.get2FACode(page)
@@ -138,7 +188,7 @@ export class Login {
while (true) { while (true) {
const button = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 2000 }).catch(() => null) const button = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { state: 'visible', timeout: 2000 }).catch(() => null)
if (button) { if (button) {
await this.bot.utils.wait(60_000) await this.bot.utils.wait(60000)
await button.click() await button.click()
continue continue
@@ -148,7 +198,7 @@ export class Login {
} }
} }
await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => {}) await page.click('button[aria-describedby="confirmSendTitle"]').catch(() => { })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 }) const element = await page.waitForSelector('#displaySign', { state: 'visible', timeout: 2000 })
return await element.textContent() return await element.textContent()
@@ -162,7 +212,7 @@ export class Login {
this.bot.log(this.bot.isMobile, 'LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`) this.bot.log(this.bot.isMobile, 'LOGIN', `Press the number ${numberToPress} on your Authenticator app to approve the login`)
this.bot.log(this.bot.isMobile, 'LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds') this.bot.log(this.bot.isMobile, 'LOGIN', 'If you press the wrong number or the "DENY" button, try again in 60 seconds')
await page.waitForSelector('#i0281', { state: 'detached', timeout: 60_000 }) await page.waitForSelector('#i0281', { state: 'detached', timeout: 60000 })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Login successfully approved!') this.bot.log(this.bot.isMobile, 'LOGIN', 'Login successfully approved!')
break break
@@ -203,7 +253,7 @@ export class Login {
} }
// Wait for login to complete // Wait for login to complete
await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10_000 }) await page.waitForSelector('html[data-role-name="RewardsPortal"]', { timeout: 10000 })
this.bot.log(this.bot.isMobile, 'LOGIN', 'Successfully logged into the rewards portal') this.bot.log(this.bot.isMobile, 'LOGIN', 'Successfully logged into the rewards portal')
} }
@@ -272,7 +322,7 @@ export class Login {
currentUrl = new URL(page.url()) currentUrl = new URL(page.url())
await this.bot.utils.wait(5000) await this.bot.utils.wait(5000)
} }
const body = new URLSearchParams() const body = new URLSearchParams()
body.append('grant_type', 'authorization_code') body.append('grant_type', 'authorization_code')
body.append('client_id', this.clientId) body.append('client_id', this.clientId)

View File

@@ -153,7 +153,7 @@ export class Workers {
Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster, Due to common false timeout on this function, we're ignoring the error regardless, if it worked then it's faster,
if it didn't then it gave enough time for the page to load. if it didn't then it gave enough time for the page to load.
*/ */
await activityPage.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => { }) await activityPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
switch (activity.promotionType) { switch (activity.promotionType) {

View File

@@ -15,18 +15,18 @@ export class ABC extends Workers {
const maxIterations = 15 const maxIterations = 15
let i let i
for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) { for (i = 0; i < maxIterations && !$('span.rw_icon').length; i++) {
await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10_000 }) await page.waitForSelector('.wk_OptionClickClass', { state: 'visible', timeout: 10000 })
const answers = $('.wk_OptionClickClass') const answers = $('.wk_OptionClickClass')
const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id'] const answer = answers[this.bot.utils.randomNumber(0, 2)]?.attribs['id']
await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10_000 }) await page.waitForSelector(`#${answer}`, { state: 'visible', timeout: 10000 })
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await page.click(`#${answer}`) // Click answer await page.click(`#${answer}`) // Click answer
await this.bot.utils.wait(4000) await this.bot.utils.wait(4000)
await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10_000 }) await page.waitForSelector('div.wk_button', { state: 'visible', timeout: 10000 })
await page.click('div.wk_button') // Click next question button await page.click('div.wk_button') // Click next question button
page = await this.bot.browser.utils.getLatestTab(page) page = await this.bot.browser.utils.getLatestTab(page)

View File

@@ -11,7 +11,7 @@ export class Poll extends Workers {
try { try {
const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}` const buttonId = `#btoption${Math.floor(this.bot.utils.randomNumber(0, 1))}`
await page.waitForSelector(buttonId, { state: 'visible', timeout: 10_000 }).catch(() => { }) // We're gonna click regardless or not await page.waitForSelector(buttonId, { state: 'visible', timeout: 10000 }).catch(() => { }) // We're gonna click regardless or not
await this.bot.utils.wait(2000) await this.bot.utils.wait(2000)
await page.click(buttonId) await page.click(buttonId)

View File

@@ -29,7 +29,7 @@ export class Quiz extends Workers {
const answers: string[] = [] const answers: string[] = []
for (let i = 0; i < quizData.numberOfOptions; i++) { for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 }) const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption')) const answerAttribute = await answerSelector?.evaluate(el => el.getAttribute('iscorrectoption'))
if (answerAttribute && answerAttribute.toLowerCase() === 'true') { if (answerAttribute && answerAttribute.toLowerCase() === 'true') {
@@ -59,7 +59,7 @@ export class Quiz extends Workers {
for (let i = 0; i < quizData.numberOfOptions; i++) { for (let i = 0; i < quizData.numberOfOptions; i++) {
const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10_000 }) const answerSelector = await page.waitForSelector(`#rqAnswerOption${i}`, { state: 'visible', timeout: 10000 })
const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option')) const dataOption = await answerSelector?.evaluate(el => el.getAttribute('data-option'))
if (dataOption === correctOption) { if (dataOption === correctOption) {

View File

@@ -47,7 +47,7 @@ export class Search extends Workers {
await this.bot.browser.utils.tryDismissAllMessages(page) await this.bot.browser.utils.tryDismissAllMessages(page)
let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it ddoesn't continue after 5 more searches with alternative queries, abort search let maxLoop = 0 // If the loop hits 10 this when not gaining any points, we're assuming it's stuck. If it doesn't continue after 5 more searches with alternative queries, abort search
const queries: string[] = [] const queries: string[] = []
// Mobile search doesn't seem to like related queries? // Mobile search doesn't seem to like related queries?
@@ -156,7 +156,7 @@ export class Search extends Workers {
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
const searchBar = '#sb_form_q' const searchBar = '#sb_form_q'
await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 }) await searchPage.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await searchPage.click(searchBar) // Focus on the textarea await searchPage.click(searchBar) // Focus on the textarea
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
await searchPage.keyboard.down(platformControlKey) await searchPage.keyboard.down(platformControlKey)
@@ -222,7 +222,7 @@ export class Search extends Workers {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}, },
data: `f.req=[[[i0OFE,"[null, null, \\"${geoLocale}\\", 0, null, 48]"]]]` 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 response = await this.bot.axios.request(request, this.bot.config.proxy.proxyGoogleTrends)
@@ -312,7 +312,7 @@ export class Search extends Workers {
await this.closeContinuePopup(page) await this.closeContinuePopup(page)
// Stay for 10 seconds for page to load and "visit" // Stay for 10 seconds for page to load and "visit"
await this.bot.utils.wait(10_000) 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 // 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 lastTab = await this.bot.browser.utils.getLatestTab(page)

View File

@@ -20,7 +20,7 @@ export class SearchOnBing extends Workers {
const query = await this.getSearchQuery(activity.title) const query = await this.getSearchQuery(activity.title)
const searchBar = '#sb_form_q' const searchBar = '#sb_form_q'
await page.waitForSelector(searchBar, { state: 'visible', timeout: 10_000 }) await page.waitForSelector(searchBar, { state: 'visible', timeout: 10000 })
await page.click(searchBar) await page.click(searchBar)
await this.bot.utils.wait(500) await this.bot.utils.wait(500)
await page.keyboard.type(query) await page.keyboard.type(query)

View File

@@ -18,11 +18,9 @@ export default class Util {
} }
shuffleArray<T>(array: T[]): T[] { shuffleArray<T>(array: T[]): T[] {
const shuffledArray = array.slice() return array.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
shuffledArray.sort(() => Math.random() - 0.5) .map(({ value }) => value)
return shuffledArray
} }
randomNumber(min: number, max: number): number { randomNumber(min: number, max: number): number {