diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index c15f8c5..882e541 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -20,9 +20,32 @@ jobs: - name: Run ESLint run: npm run lint - docker: + sonar: needs: lint runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: SonarQube Scan + env: + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + docker run --rm \ + -e SONAR_HOST_URL="$SONAR_HOST_URL" \ + -e SONAR_TOKEN="$SONAR_TOKEN" \ + -v "$PWD:/usr/src" \ + -w /usr/src \ + sonarsource/sonar-scanner-cli \ + sonar-scanner \ + -Dsonar.host.url="$SONAR_HOST_URL" \ + -Dsonar.login="$SONAR_TOKEN" + + docker: + needs: [lint, sonar] + runs-on: self-hosted steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/aliexpress.js b/aliexpress.js index 52e75a0..6d654d3 100644 --- a/aliexpress.js +++ b/aliexpress.js @@ -1,5 +1,5 @@ import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra -import { datetime, filenamify, prompt, handleSIGINT, stealth } from './src/util.js'; +import { datetime, filenamify, prompt, handleSIGINT } from './src/util.js'; import { cfg } from './src/config.js'; // using https://github.com/apify/fingerprint-suite worked, but has no launchPersistentContext... @@ -8,8 +8,8 @@ import { FingerprintInjector } from 'fingerprint-injector'; import { FingerprintGenerator } from 'fingerprint-generator'; const { fingerprint, headers } = new FingerprintGenerator().getFingerprint({ - devices: ["mobile"], - operatingSystems: ["android"], + devices: ['mobile'], + operatingSystems: ['android'], }); const context = await firefox.launchPersistentContext(cfg.dir.browser, { @@ -21,11 +21,11 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved userAgent: fingerprint.navigator.userAgent, viewport: { - width: fingerprint.screen.width, - height: fingerprint.screen.height, + width: fingerprint.screen.width, + height: fingerprint.screen.height, }, extraHTTPHeaders: { - 'accept-language': headers['accept-language'], + 'accept-language': headers['accept-language'], }, }); handleSIGINT(context); @@ -36,7 +36,7 @@ context.setDefaultTimeout(cfg.debug ? 0 : cfg.timeout); const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist -const auth = async (url) => { +const auth = async url => { console.log('auth', url); await page.goto(url, { waitUntil: 'domcontentloaded' }); // redirects to https://login.aliexpress.com/?return_url=https%3A%2F%2Fwww.aliexpress.com%2Fp%2Fcoin-pc-index%2Findex.html @@ -45,7 +45,7 @@ const auth = async (url) => { console.error('Not logged in! Will wait for 120s for you to login...'); // await page.waitForTimeout(120*1000); // or try automated - page.locator('span:has-text("Switch account")').click().catch(_ => {}); // sometimes no longer logged in, but previous user/email is pre-selected -> in this case we want to go back to the classic login + page.locator('span:has-text("Switch account")').click().catch(() => {}); // sometimes no longer logged in, but previous user/email is pre-selected -> in this case we want to go back to the classic login const login = page.locator('.login-container'); const email = cfg.ae_email || await prompt({ message: 'Enter email' }); const emailInput = login.locator('input[label="Email or phone number"]'); @@ -58,11 +58,11 @@ const auth = async (url) => { await login.locator('input[label="Password"]').fill(password); await login.locator('button:has-text("Sign in")').click(); const error = login.locator('.error-text'); - error.waitFor().then(async _ => console.error('Login error:', await error.innerText())); + error.waitFor().then(async () => console.error('Login error:', await error.innerText())); await page.waitForURL(url); // await page.addLocatorHandler(page.getByRole('button', { name: 'Accept cookies' }), btn => btn.click()); - page.getByRole('button', { name: 'Accept cookies' }).click().then(_ => console.log('Accepted cookies')).catch(_ => { }); - }), page.locator('#nav-user-account').waitFor()]).catch(_ => {}); + page.getByRole('button', { name: 'Accept cookies' }).click().then(() => console.log('Accepted cookies')).catch(() => { }); + }), page.locator('#nav-user-account').waitFor()]).catch(() => {}); // await page.locator('#nav-user-account').hover(); // console.log('Logged in as:', await page.locator('.welcome-name').innerText()); @@ -80,6 +80,7 @@ const urls = { merge: 'https://m.aliexpress.com/p/merge-market/index.html', }; +/* eslint-disable no-unused-vars */ const coins = async () => { // await auth(urls.coins); await Promise.any([page.locator('.checkin-button').click(), page.locator('.addcoin').waitFor()]); @@ -103,6 +104,7 @@ const euro = async () => { const merge = async () => { await page.pause(); }; +/* eslint-enable no-unused-vars */ try { // await coins(); @@ -112,7 +114,11 @@ try { // gogo, // euro, merge, - ].reduce((a, f) => a.then(async _ => { await auth(urls[f.name]); await f(); console.log() }), Promise.resolve()); + ].reduce((a, f) => a.then(async () => { + await auth(urls[f.name]); + await f(); + console.log(); + }), Promise.resolve()); // await page.pause(); } catch (error) { diff --git a/prime-gaming.js b/prime-gaming.js index dc39bba..0c982bd 100644 --- a/prime-gaming.js +++ b/prime-gaming.js @@ -41,7 +41,7 @@ const handleMFA = async p => { const otpField = p.locator('#auth-mfa-otpcode, input[name=otpCode]'); if (!await otpField.count()) return false; console.log('Two-Step Verification - enter the One Time Password (OTP), e.g. generated by your Authenticator App'); - await p.locator('#auth-mfa-remember-device, [name=rememberDevice]').check().catch(_ => {}); + await p.locator('#auth-mfa-remember-device, [name=rememberDevice]').check().catch(() => {}); const otp = cfg.pg_otpkey && authenticator.generate(cfg.pg_otpkey) || await prompt({ type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 6 || 'The code must be 6 digits!' }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them await otpField.first().pressSequentially(otp.toString()); await p.locator('input[type="submit"], button[type="submit"]').first().click(); @@ -61,7 +61,7 @@ try { await page.click('input[type="submit"]'); await page.fill('[name=password]', password); await page.click('input[type="submit"]'); - await handleMFA(page).catch(_ => {}); + await handleMFA(page).catch(() => {}); page.waitForURL('**/ap/signin**').then(async () => { // check for wrong credentials const error = await page.locator('.a-alert-content').first().innerText(); if (!error.trim.length) return; @@ -91,7 +91,7 @@ try { '[data-a-target="user-dropdown-first-name-text"]', '[data-testid="user-dropdown-first-name-text"]', ].map(s => page.waitForSelector(s))); - page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")').catch(_ => { }); // to not waste screen space when non-headless, TODO does not work reliably, need to wait for something else first? + page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")').catch(() => { }); // to not waste screen space when non-headless, TODO does not work reliably, need to wait for something else first? while (await page.locator('button:has-text("Sign in"), button:has-text("Anmelden")').count() > 0) { console.error('Not signed in anymore.'); await page.click('button:has-text("Sign in")'); @@ -115,7 +115,7 @@ try { await context.close(); // finishes potential recording process.exit(1); }); - handleMFA(page).catch(_ => {}); + handleMFA(page).catch(() => {}); } else { console.log('Waiting for you to login in the browser.'); await notify('prime-gaming: no longer signed in and not enough options set for automatic login.'); @@ -209,7 +209,7 @@ try { const games = await locateGamesList(); // Load all cards (old and new layout) by scrolling the container or the page if (games) await scrollUntilStable(() => games.evaluate(el => el.scrollHeight).catch(() => 0)); - await scrollUntilStable(() => page.evaluate(() => document.scrollingElement?.scrollHeight ?? 0)); + await scrollUntilStable(() => page.evaluate(() => globalThis.document?.scrollingElement?.scrollHeight ?? 0)); const normalizeClaimUrl = url => { if (!url) return { url, key: null }; @@ -241,7 +241,7 @@ try { const hrefs = [...new Set(await anchorClaims.evaluateAll(anchors => anchors.map(a => a.getAttribute('href')).filter(Boolean)))]; for (const href of hrefs) { const { url, key } = normalizeClaimUrl(href); - const title = key || (await anchorClaims.first().innerText()) || 'Unknown title'; + const title = key || await anchorClaims.first().innerText() || 'Unknown title'; cards.push({ kind: 'external', title, url, key }); } } @@ -286,15 +286,15 @@ try { // bottom to top: oldest to newest games internal.reverse(); external.reverse(); - const sameOrNewPage = async url => new Promise(async (resolve, _reject) => { + const sameOrNewPage = async url => { const isNew = page.url() != url; let p = page; if (isNew) { p = await context.newPage(); await p.goto(url, { waitUntil: 'domcontentloaded' }); } - resolve([p, isNew]); - }); + return [p, isNew]; + }; const skipBasedOnTime = async url => { // console.log(' Checking time left for game:', url); const [p, isNew] = await sameOrNewPage(url); @@ -305,12 +305,12 @@ try { } const dueDateOrg = await dueDateLoc.first().innerText(); const dueDate = new Date(Date.parse(dueDateOrg + ' 17:00')); - const daysLeft = (dueDate.getTime() - Date.now())/1000/60/60/24; + const daysLeft = (dueDate.getTime() - Date.now()) / 1000 / 60 / 60 / 24; const availabilityText = await p.locator('.availability-date, [data-testid="availability-end-date"], [data-test-selector="availability-end-date"]').first().innerText().catch(() => dueDateOrg); console.log(' ', availabilityText, '->', daysLeft.toFixed(2)); if (isNew) await p.close(); return daysLeft > cfg.pg_timeLeft; - } + }; console.log('\nNumber of free unclaimed games (Prime Gaming):', internal.length); // claim games in internal store for (const card of internal) { @@ -353,12 +353,16 @@ try { try { await c.waitFor({ state: 'visible', timeout: 5000 }); if (!await c.isEnabled()) { - await c.evaluate(el => { el.disabled = false; el.removeAttribute('disabled'); el.click(); }); + await c.evaluate(el => { + el.disabled = false; + el.removeAttribute('disabled'); + el.click(); + }); } else { await c.click(); } return true; - } catch (_) { + } catch { // try next candidate } } @@ -375,7 +379,7 @@ try { continue; } await page.goto(url, { waitUntil: 'domcontentloaded' }); - await page.waitForSelector('[data-a-target="buy-box"]', { timeout: 10000 }).catch(_ => {}); + await page.waitForSelector('[data-a-target="buy-box"]', { timeout: 10000 }).catch(() => {}); if (cfg.debug) await page.pause(); let store = 'unknown'; const detailLoc = page.locator('[data-a-target="DescriptionItemDetails"], [data-testid="DescriptionItemDetails"]'); @@ -438,9 +442,9 @@ try { if (cfg.interactive && !await confirm()) continue; await clickCTA(page); await Promise.any([ - page.waitForSelector('.thank-you-title:has-text("Success")', { timeout: cfg.timeout }).catch(_ => {}), - page.waitForSelector('div:has-text("Link game account")', { timeout: cfg.timeout }).catch(_ => {}), - ]).catch(_ => {}); + page.waitForSelector('.thank-you-title:has-text("Success")', { timeout: cfg.timeout }).catch(() => {}), + page.waitForSelector('div:has-text("Link game account")', { timeout: cfg.timeout }).catch(() => {}), + ]).catch(() => {}); db.data[user][title] ||= { title, time: datetime(), url, store }; if (await page.locator('div:has-text("Link game account")').count() // TODO still needed? epic games store just has 'Link account' as the button text now. || await page.locator('div:has-text("Link account")').count()) { @@ -466,16 +470,16 @@ try { let code; try { // ensure CTA was clicked in case code is behind it - await clickCTA(page).catch(_ => {}); + await clickCTA(page).catch(() => {}); code = await Promise.any([ page.inputValue('input[type="text"]'), page.textContent('[data-a-target="ClaimStateClaimCodeContent"]').then(s => s.replace('Your code: ', '')), ]); - } catch (_) { + } catch { console.error(' Could not find claim code on page (timeout). Please check manually.'); db.data[user][title].status = 'claimed (code not found)'; notify_game.status = 'claimed (code not found)'; - await page.screenshot({ path: screenshot('external', `${filenamify(title)}_nocode.png`), fullPage: true }).catch(_ => {}); + await page.screenshot({ path: screenshot('external', `${filenamify(title)}_nocode.png`), fullPage: true }).catch(() => {}); continue; } console.log(' Code to redeem game:', chalk.blue(code)); @@ -563,7 +567,7 @@ try { if (j?.events?.cart.length && j.events.cart[0]?.data?.reason == 'UserAlreadyOwnsContent') { redeem_action = 'already redeemed'; console.error(' error: UserAlreadyOwnsContent'); - } else if (true) { // TODO what's returned on success? + } else { // TODO what's returned on success? redeem_action = 'redeemed'; db.data[user][title].status = 'claimed and redeemed?'; console.log(' Redeemed successfully? Please report if not in https://github.com/vogler/free-games-claimer/issues/5'); @@ -610,7 +614,7 @@ try { // await page.pause(); } await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); - page.click('button[data-type="Game"]').catch(_ => {}); + page.click('button[data-type="Game"]').catch(() => {}); if (notify_games.length && games) { // make screenshot of all games if something was claimed and list exists const p = screenshot(`${filenamify(datetime())}.png`); @@ -628,7 +632,7 @@ try { await loot.waitFor(); process.stdout.write('Loading all DLCs on page...'); - await scrollUntilStable(() => loot.locator('[data-a-target="item-card"]').count()) + await scrollUntilStable(() => loot.locator('[data-a-target="item-card"]').count()); console.log('\nNumber of already claimed DLC:', await loot.locator('p:has-text("Collected")').count()); @@ -657,7 +661,7 @@ try { // most games have a button 'Get in-game content' // epic-games: Fall Guys: Claim -> Continue -> Go to Epic Games (despite account linked and logged into epic-games) -> not tied to account but via some cookie? await Promise.any([page.click('.tw-button:has-text("Get in-game content")'), page.click('.tw-button:has-text("Claim your gift")'), page.click('.tw-button:has-text("Claim")').then(() => page.click('button:has-text("Continue")'))]); - page.click('button:has-text("Continue")').catch(_ => { }); + page.click('button:has-text("Continue")').catch(() => { }); const linkAccountButton = page.locator('[data-a-target="LinkAccountButton"]'); let unlinked_store; if (await linkAccountButton.count()) { @@ -674,7 +678,7 @@ try { dlc_unlinked[unlinked_store] ??= []; dlc_unlinked[unlinked_store].push(title); } else { - const code = await page.inputValue('input[type="text"]').catch(_ => undefined); + const code = await page.inputValue('input[type="text"]').catch(() => undefined); console.log(' Code to redeem game:', chalk.blue(code)); db.data[user][title].code = code; db.data[user][title].status = 'claimed'; diff --git a/src/util.js b/src/util.js index 49424f8..8ba63a3 100644 --- a/src/util.js +++ b/src/util.js @@ -96,21 +96,21 @@ const timeoutPlugin = timeout => enquirer => { // cancel prompt after timeout ms prompt.hint = () => 'timeout'; prompt.cancel(); }, timeout); - prompt.on('submit', _ => clearTimeout(t)); - prompt.on('cancel', _ => clearTimeout(t)); + prompt.on('submit', () => clearTimeout(t)); + prompt.on('cancel', () => clearTimeout(t)); }); }; enquirer.use(timeoutPlugin(cfg.login_timeout)); // TODO may not want to have this timeout for all prompts; better extend Prompt and add a timeout prompt option // single prompt that just returns the non-empty value instead of an object // @ts-ignore -export const prompt = o => enquirer.prompt({ name: 'name', type: 'input', message: 'Enter value', ...o }).then(r => r.name).catch(_ => {}); +export const prompt = o => enquirer.prompt({ name: 'name', type: 'input', message: 'Enter value', ...o }).then(r => r.name).catch(() => {}); export const confirm = o => prompt({ type: 'confirm', message: 'Continue?', ...o }); // notifications via apprise CLI import { execFile } from 'child_process'; import { cfg } from './config.js'; -export const notify = html => new Promise((resolve, reject) => { +export const notify = html => new Promise(resolve => { if (!cfg.notify) { if (cfg.debug) console.debug('notify: NOTIFY is not set!'); return resolve(); diff --git a/steam-games.js b/steam-games.js index 9b307fc..ed54253 100644 --- a/steam-games.js +++ b/steam-games.js @@ -12,8 +12,8 @@ import { FingerprintInjector } from 'fingerprint-injector'; import { FingerprintGenerator } from 'fingerprint-generator'; const { fingerprint, headers } = new FingerprintGenerator().getFingerprint({ - devices: ["desktop"], - operatingSystems: ["windows"], + devices: ['desktop'], + operatingSystems: ['windows'], }); const context = await firefox.launchPersistentContext(cfg.dir.browser, { @@ -22,11 +22,11 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL userAgent: fingerprint.navigator.userAgent, viewport: { - width: fingerprint.screen.width, - height: fingerprint.screen.height, + width: fingerprint.screen.width, + height: fingerprint.screen.height, }, extraHTTPHeaders: { - 'accept-language': headers['accept-language'], + 'accept-language': headers['accept-language'], }, }); // await stealth(context); diff --git a/test/sigint-enquirer-raw-keeps-running.js b/test/sigint-enquirer-raw-keeps-running.js index 23d9983..ccb3081 100644 --- a/test/sigint-enquirer-raw-keeps-running.js +++ b/test/sigint-enquirer-raw-keeps-running.js @@ -12,10 +12,10 @@ function onRawSIGINT(fn) { } }); } -console.log(1) +console.log(1); onRawSIGINT(() => { console.log('raw'); process.exit(1); }); -console.log(2) +console.log(2); // onRawSIGINT workaround for enquirer keeps the process from exiting here... diff --git a/test/sigint-enquirer-raw.js b/test/sigint-enquirer-raw.js index e6b538d..c85ee0d 100644 --- a/test/sigint-enquirer-raw.js +++ b/test/sigint-enquirer-raw.js @@ -7,19 +7,19 @@ import { prompt, handleSIGINT } from '../src/util.js'; // }); handleSIGINT(); -function onRawSIGINT(fn) { - const { stdin, stdout } = process; - stdin.setRawMode(true); - stdin.resume(); - stdin.on('data', data => { - const key = data.toString('utf-8'); - if (key === '\u0003') { // ctrl + c - fn(); - } else { - stdout.write(key); - } - }); -} +// function onRawSIGINT(fn) { +// const { stdin, stdout } = process; +// stdin.setRawMode(true); +// stdin.resume(); +// stdin.on('data', data => { +// const key = data.toString('utf-8'); +// if (key === '\u0003') { // ctrl + c +// fn(); +// } else { +// stdout.write(key); +// } +// }); +// } // onRawSIGINT(() => { // console.log('raw'); process.exit(1); // });