From 16cb1942166f2afcb54a17d7a36c00f44966cba1 Mon Sep 17 00:00:00 2001 From: 4n4n4s Date: Fri, 22 Sep 2023 12:47:04 +0000 Subject: [PATCH 01/10] Added first version of Steam --- .vscode/launch.json | 22 +++++ README.md | 6 ++ config.js | 6 ++ steam-games.js | 201 ++++++++++++++++++++++++++++++++++++++++++++ steam-games.json | 8 ++ 5 files changed, 243 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 steam-games.js create mode 100644 steam-games.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6071636 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Steam", + "outputCapture": "std", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/steam-games.js", + "env": { + "STEAM_JSON": "1", + "STEAM_GAMERPOWER": "1" + } + } + ] +} diff --git a/README.md b/README.md index 5c9e90f..f339f7c 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,12 @@ Available options/variables and their default values: | GOG_EMAIL | | GOG email for login. Overrides EMAIL. | | GOG_PASSWORD | | GOG password for login. Overrides PASSWORD. | | GOG_NEWSLETTER | 0 | Do not unsubscribe from newsletter after claiming a game if 1. | +| STEAM_USERNAME | | Steam username for login. | +| STEAM_PASSWORD | | Steam password for login. Overrides PASSWORD. | +| STEAM_JSON | 0 | Claims steam games from json. STEAM_JSON_URL can be defined. | +| STEAM_JSON_URL | [steam-games.json](https://raw.githubusercontent.com/vogler/free-games-claimer/main/steam-games.json) | A list of steam urls in json format to claim the games. | +| STEAM_GAMERPOWER | 1 | Claims steam games using [gamerpower api](https://www.gamerpower.com/api/giveaways?platform=steam&type=game). | + See `config.js` for all options. diff --git a/config.js b/config.js index 0962159..4ffafb4 100644 --- a/config.js +++ b/config.js @@ -48,4 +48,10 @@ export const cfg = { // experimmental - likely to change pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content + + steam_username: process.env.STEAM_USERNAME, + steam_password: process.env.STEAM_PASSWORD || process.env.PASSWORD, + steam_json: false || process.env.STEAM_JSON == '1', + steam_json_url: process.env.STEAM_JSON_URL || 'https://raw.githubusercontent.com/vogler/free-games-claimer/main/steam-games.json', + steam_gamerpower: true || process.env.STEAM_GAMERPOWER == '1', }; diff --git a/steam-games.js b/steam-games.js new file mode 100644 index 0000000..ba68f8c --- /dev/null +++ b/steam-games.js @@ -0,0 +1,201 @@ +import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra +import { resolve, jsonDb, datetime, prompt, stealth, notify, html_game_list, handleSIGINT } from './util.js'; +import path from 'path'; +import { existsSync, writeFileSync } from 'fs'; +import { cfg } from './config.js'; +import { config } from 'dotenv'; + +const screenshot = (...a) => resolve(cfg.dir.screenshots, 'steam', ...a); + +const URL_CLAIM = 'https://store.steampowered.com/?l=english'; +const URL_LOGIN = 'https://store.steampowered.com/login/'; + +console.log(datetime(), 'started checking steam'); + +const db = await jsonDb('steam.json', {}); + +handleSIGINT(); + +// https://playwright.dev/docs/auth#multi-factor-authentication +const context = await firefox.launchPersistentContext(cfg.dir.browser, { + // chrome will not work in linux arm64, only chromium + // channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge + headless: false, + viewport: { width: cfg.width, height: cfg.height }, + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated? + // userAgent for firefox: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0 + locale: "en-US", // ignore OS locale to be sure to have english text for locators + recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 + recordHar: cfg.record ? { path: `data/record/eg-${datetime()}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools + args: [ // https://peter.sh/experiments/chromium-command-line-switches + // don't want to see bubble 'Restore pages? Chrome didn't shut down correctly.' + // '--restore-last-session', // does not apply for crash/killed + '--hide-crash-restore-bubble', + // `--disable-extensions-except=${ext}`, + // `--load-extension=${ext}`, + ], + // ignoreDefaultArgs: ['--enable-automation'], // remove default arg that shows the info bar with 'Chrome is being controlled by automated test software.'. Since Chromeium 106 this leads to show another info bar with 'You are using an unsupported command-line flag: --no-sandbox. Stability and security will suffer.'. +}); + +await stealth(context); + +if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); + +const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist +// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent)); + +const notify_games = []; +let user; + +async function doLogin(){ + await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever + if (cfg.steam_username && cfg.steam_password){ + console.info('Using username and password from environment.'); + } + else { + console.info('Press ESC to skip the prompts if you want to login in the browser (not possible in headless mode).'); + } + const username = cfg.steam_username || await prompt({message: 'Enter username'}); + const password = username && (cfg.steam_password || await prompt({type: 'password', message: 'Enter password'})); + if (username && password) { + await page.type('input[type=text]:visible', username); + await page.type('input[type=password]:visible', password); + await page.waitForTimeout(2000); + await page.click('button[type=submit]'); + await page.waitForTimeout(2000); + } + const auth = await page.getByText('You have a mobile authenticator protecting this account.').first(); + let isFirstCheck = true; + while (await auth.isVisible()) + { + if (isFirstCheck) + { + console.log("Steam requires confirmation from authenticator"); + notify(`Steam requires confirmation from authenticator`); + isFirstCheck = false; + } + await page.waitForTimeout(2000); + } +} + +async function claim(){ + await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever + await context.addCookies([{name: 'cookieSettings', value: '%7B%22version%22%3A1%2C%22preference_state%22%3A2%2C%22content_customization%22%3Anull%2C%22valve_analytics%22%3Anull%2C%22third_party_analytics%22%3Anull%2C%22third_party_content%22%3Anull%2C%22utm_enabled%22%3Atrue%7D', domain: 'store.steampowered.com', path: '/'}]); // Decline all cookies to get rid of banner to save space on screen. + + const signIn = page.locator('a:has-text("Sign In")').first(); + while (await signIn.isVisible()) { + console.error('Not signed in to steam.'); + + await doLogin(); + } + + user = await page.locator("#account_pulldown").first().innerText(); + console.error('You are logged in as ' + user); + db.data[user] ||= {}; + + if (cfg.steam_json) { + await claimJson(); + } + if (cfg.steam_gamerpower) { + await claimGamerpower(); + } +} + +async function claimJson(){ + console.log("Claiming JSON"); + const response = await page.goto(cfg.steam_json_url); + const items = await response.json(); + for (const item of items) { + if (!await isClaimedUrl(item.url)){ + console.log(item); + if (item.hasOwnProperty("startDate")) + { + const date = Date.parse(item.startDate); + if (date >= Date.now()) + { + console.log("game not available yet " + new Date(date)); + return; + } + } + await claimGame(item.url); + } + } +} + +async function claimGamerpower(){ + console.log("Claiming Gamerpower"); + const response = await page.goto("https://www.gamerpower.com/api/giveaways?platform=steam&type=game"); + const items = await response.json(); + for (const item of items) { + console.log(item.open_giveaway_url); + await page.goto(item.open_giveaway_url, { waitUntil: 'domcontentloaded' }); + + const url = page.url(); + if (url.includes("https://store.steampowered.com/app")){ + if (!await isClaimedUrl(url)){ + await claimGame(url); + } + } + else { + console.log("Game can be claimed outside of steam! " + url); + } + } +} + +async function claimGame(url){ + await page.goto(url, { waitUntil: 'domcontentloaded'}); + const title = await page.locator('#appHubAppName').first().innerText(); + const pattern = "/app/"; + let game_id = page.url().substring(page.url().indexOf(pattern)+pattern.length); + game_id = game_id.substring(0, game_id.indexOf("/")); + db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only! + + const notify_game = { title, url: url, status: 'failed' }; + notify_games.push(notify_game); // status is updated below + + const alreadyOwned = await page.locator('.game_area_already_owned').first(); + if (await alreadyOwned.isVisible()) + { + console.log("Game " + title + " already in library"); + db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed + } + else + { + await page.locator(('#freeGameBtn')).click(); + console.log("purchased"); + db.data[user][game_id].status = 'claimed'; + db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time + } + notify_game.status = db.data[user][game_id].status; // claimed or failed + const p = screenshot(`${game_id}.png`); + if (!existsSync(p)) await page.screenshot({ path: p, fullPage: false }); // fullPage is quite long... +} + +async function isClaimedUrl(url) { + try { + const pattern = "/app/"; + let game_id = url.substring(url.indexOf(pattern)+pattern.length); + game_id = game_id.substring(0, game_id.indexOf("/")); + var status = db.data[user][game_id]["status"]; + return status === "existed" || status === "claimed"; + } catch (error) { + return false; + } +} + +try { + await claim(); +} catch (error) { + console.error(error); // .toString()? + process.exitCode ||= 1; + if (error.message && process.exitCode != 130) + notify(`steam failed: ${error.message.split('\n')[0]}`); +} finally { + await db.write(); // write out json db + if (notify_games.filter(g => g.status != 'existed').length) { // don't notify if all were already claimed + notify(`steam (${user}):
${html_game_list(notify_games)}`); + } +} +if (cfg.debug) writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies())); +if (page.video()) console.log('Recorded video:', await page.video().path()); +await context.close(); diff --git a/steam-games.json b/steam-games.json new file mode 100644 index 0000000..3e58128 --- /dev/null +++ b/steam-games.json @@ -0,0 +1,8 @@ +[ + { + "url": "https://store.steampowered.com/app/2447060/Monster_Tiles_TD/" + }, + { + "url": "https://store.steampowered.com/app/2568880/The_Hardest_Game_Ever/" + } +] From 617229f65974f84f5509d849e9b1a7f578dd7d26 Mon Sep 17 00:00:00 2001 From: 4n4n4s Date: Fri, 10 Nov 2023 16:43:47 +0000 Subject: [PATCH 02/10] * Format and fix sonar issues --- README.md | 2 +- config.js | 4 +-- steam-games.js | 72 ++++++++++++++++++++++---------------------------- 3 files changed, 35 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index f339f7c..371617a 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Available options/variables and their default values: | STEAM_PASSWORD | | Steam password for login. Overrides PASSWORD. | | STEAM_JSON | 0 | Claims steam games from json. STEAM_JSON_URL can be defined. | | STEAM_JSON_URL | [steam-games.json](https://raw.githubusercontent.com/vogler/free-games-claimer/main/steam-games.json) | A list of steam urls in json format to claim the games. | -| STEAM_GAMERPOWER | 1 | Claims steam games using [gamerpower api](https://www.gamerpower.com/api/giveaways?platform=steam&type=game). | +| STEAM_GAMERPOWER | 0 | Claims steam games using [gamerpower api](https://www.gamerpower.com/api/giveaways?platform=steam&type=game). | See `config.js` for all options. diff --git a/config.js b/config.js index 4ffafb4..92c0675 100644 --- a/config.js +++ b/config.js @@ -51,7 +51,7 @@ export const cfg = { steam_username: process.env.STEAM_USERNAME, steam_password: process.env.STEAM_PASSWORD || process.env.PASSWORD, - steam_json: false || process.env.STEAM_JSON == '1', + steam_json: process.env.STEAM_JSON == '1', steam_json_url: process.env.STEAM_JSON_URL || 'https://raw.githubusercontent.com/vogler/free-games-claimer/main/steam-games.json', - steam_gamerpower: true || process.env.STEAM_GAMERPOWER == '1', + steam_gamerpower: process.env.STEAM_GAMERPOWER == '1', }; diff --git a/steam-games.js b/steam-games.js index ba68f8c..fef893f 100644 --- a/steam-games.js +++ b/steam-games.js @@ -3,7 +3,6 @@ import { resolve, jsonDb, datetime, prompt, stealth, notify, html_game_list, han import path from 'path'; import { existsSync, writeFileSync } from 'fs'; import { cfg } from './config.js'; -import { config } from 'dotenv'; const screenshot = (...a) => resolve(cfg.dir.screenshots, 'steam', ...a); @@ -22,11 +21,10 @@ const context = await firefox.launchPersistentContext(cfg.dir.browser, { // channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge headless: false, viewport: { width: cfg.width, height: cfg.height }, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO Windows UA enough to avoid 'device not supported'? update if browser is updated? - // userAgent for firefox: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0 + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', locale: "en-US", // ignore OS locale to be sure to have english text for locators - recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, // will record a .webm video for each page navigated; without size, video would be scaled down to fit 800x800 - recordHar: cfg.record ? { path: `data/record/eg-${datetime()}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools + recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, + recordHar: cfg.record ? { path: `data/record/eg-${datetime()}.har` } : undefined, args: [ // https://peter.sh/experiments/chromium-command-line-switches // don't want to see bubble 'Restore pages? Chrome didn't shut down correctly.' // '--restore-last-session', // does not apply for crash/killed @@ -47,16 +45,16 @@ const page = context.pages().length ? context.pages()[0] : await context.newPage const notify_games = []; let user; -async function doLogin(){ +async function doLogin() { await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever - if (cfg.steam_username && cfg.steam_password){ + if (cfg.steam_username && cfg.steam_password) { console.info('Using username and password from environment.'); } else { console.info('Press ESC to skip the prompts if you want to login in the browser (not possible in headless mode).'); } - const username = cfg.steam_username || await prompt({message: 'Enter username'}); - const password = username && (cfg.steam_password || await prompt({type: 'password', message: 'Enter password'})); + const username = cfg.steam_username || await prompt({ message: 'Enter username' }); + const password = username && (cfg.steam_password || await prompt({ type: 'password', message: 'Enter password' })); if (username && password) { await page.type('input[type=text]:visible', username); await page.type('input[type=password]:visible', password); @@ -66,10 +64,8 @@ async function doLogin(){ } const auth = await page.getByText('You have a mobile authenticator protecting this account.').first(); let isFirstCheck = true; - while (await auth.isVisible()) - { - if (isFirstCheck) - { + while (await auth.isVisible()) { + if (isFirstCheck) { console.log("Steam requires confirmation from authenticator"); notify(`Steam requires confirmation from authenticator`); isFirstCheck = false; @@ -78,9 +74,9 @@ async function doLogin(){ } } -async function claim(){ +async function claim() { await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever - await context.addCookies([{name: 'cookieSettings', value: '%7B%22version%22%3A1%2C%22preference_state%22%3A2%2C%22content_customization%22%3Anull%2C%22valve_analytics%22%3Anull%2C%22third_party_analytics%22%3Anull%2C%22third_party_content%22%3Anull%2C%22utm_enabled%22%3Atrue%7D', domain: 'store.steampowered.com', path: '/'}]); // Decline all cookies to get rid of banner to save space on screen. + await context.addCookies([{ name: 'cookieSettings', value: '%7B%22version%22%3A1%2C%22preference_state%22%3A2%2C%22content_customization%22%3Anull%2C%22valve_analytics%22%3Anull%2C%22third_party_analytics%22%3Anull%2C%22third_party_content%22%3Anull%2C%22utm_enabled%22%3Atrue%7D', domain: 'store.steampowered.com', path: '/' }]); // Decline all cookies to get rid of banner to save space on screen. const signIn = page.locator('a:has-text("Sign In")').first(); while (await signIn.isVisible()) { @@ -101,18 +97,16 @@ async function claim(){ } } -async function claimJson(){ +async function claimJson() { console.log("Claiming JSON"); const response = await page.goto(cfg.steam_json_url); const items = await response.json(); for (const item of items) { - if (!await isClaimedUrl(item.url)){ + if (!await isClaimedUrl(item.url)) { console.log(item); - if (item.hasOwnProperty("startDate")) - { + if (item.hasOwnProperty("startDate")) { const date = Date.parse(item.startDate); - if (date >= Date.now()) - { + if (date >= Date.now()) { console.log("game not available yet " + new Date(date)); return; } @@ -122,7 +116,7 @@ async function claimJson(){ } } -async function claimGamerpower(){ +async function claimGamerpower() { console.log("Claiming Gamerpower"); const response = await page.goto("https://www.gamerpower.com/api/giveaways?platform=steam&type=game"); const items = await response.json(); @@ -131,8 +125,8 @@ async function claimGamerpower(){ await page.goto(item.open_giveaway_url, { waitUntil: 'domcontentloaded' }); const url = page.url(); - if (url.includes("https://store.steampowered.com/app")){ - if (!await isClaimedUrl(url)){ + if (url.includes("https://store.steampowered.com/app")) { + if (!await isClaimedUrl(url)) { await claimGame(url); } } @@ -142,11 +136,11 @@ async function claimGamerpower(){ } } -async function claimGame(url){ - await page.goto(url, { waitUntil: 'domcontentloaded'}); +async function claimGame(url) { + await page.goto(url, { waitUntil: 'domcontentloaded' }); const title = await page.locator('#appHubAppName').first().innerText(); const pattern = "/app/"; - let game_id = page.url().substring(page.url().indexOf(pattern)+pattern.length); + let game_id = page.url().substring(page.url().indexOf(pattern) + pattern.length); game_id = game_id.substring(0, game_id.indexOf("/")); db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only! @@ -154,13 +148,11 @@ async function claimGame(url){ notify_games.push(notify_game); // status is updated below const alreadyOwned = await page.locator('.game_area_already_owned').first(); - if (await alreadyOwned.isVisible()) - { + if (await alreadyOwned.isVisible()) { console.log("Game " + title + " already in library"); db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed } - else - { + else { await page.locator(('#freeGameBtn')).click(); console.log("purchased"); db.data[user][game_id].status = 'claimed'; @@ -172,15 +164,15 @@ async function claimGame(url){ } async function isClaimedUrl(url) { - try { - const pattern = "/app/"; - let game_id = url.substring(url.indexOf(pattern)+pattern.length); - game_id = game_id.substring(0, game_id.indexOf("/")); - var status = db.data[user][game_id]["status"]; - return status === "existed" || status === "claimed"; - } catch (error) { - return false; - } + try { + const pattern = "/app/"; + let game_id = url.substring(url.indexOf(pattern) + pattern.length); + game_id = game_id.substring(0, game_id.indexOf("/")); + const status = db.data[user][game_id]["status"]; + return status === "existed" || status === "claimed"; + } catch (error) { + return false; + } } try { From 450191dffc7af8e5597cb9026f04aa04a7f5d2ae Mon Sep 17 00:00:00 2001 From: drklien Date: Thu, 17 Jul 2025 12:38:21 +1000 Subject: [PATCH 03/10] fixed claim procedure with age claims functioning and relocated config.js with updated variables --- config.js => src/config.js | 6 +- steam-games.js | 314 +++++++++++++++++++++++++++---------- 2 files changed, 239 insertions(+), 81 deletions(-) rename config.js => src/config.js (93%) diff --git a/config.js b/src/config.js similarity index 93% rename from config.js rename to src/config.js index 92c0675..724f97b 100644 --- a/config.js +++ b/src/config.js @@ -51,7 +51,9 @@ export const cfg = { steam_username: process.env.STEAM_USERNAME, steam_password: process.env.STEAM_PASSWORD || process.env.PASSWORD, - steam_json: process.env.STEAM_JSON == '1', + steam_json: process.env.STEAM_JSON == '0', steam_json_url: process.env.STEAM_JSON_URL || 'https://raw.githubusercontent.com/vogler/free-games-claimer/main/steam-games.json', - steam_gamerpower: process.env.STEAM_GAMERPOWER == '1', + steam_gamerpower: true, + steam_gamerpower_url: process.env.STEAM_GAMERPOWER_URL || 'https://www.gamerpower.com/api/giveaways?platform=steam&type=game', + }; diff --git a/steam-games.js b/steam-games.js index fef893f..f7bc09c 100644 --- a/steam-games.js +++ b/steam-games.js @@ -1,103 +1,82 @@ -import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra -import { resolve, jsonDb, datetime, prompt, stealth, notify, html_game_list, handleSIGINT } from './util.js'; +import { chromium } from 'patchright'; +import dotenv from 'dotenv'; import path from 'path'; import { existsSync, writeFileSync } from 'fs'; -import { cfg } from './config.js'; +import { resolve, jsonDb, datetime, prompt, notify, html_game_list, handleSIGINT } from './src/util.js'; +import { cfg } from './src/config.js'; + +const notify_games = []; const screenshot = (...a) => resolve(cfg.dir.screenshots, 'steam', ...a); - const URL_CLAIM = 'https://store.steampowered.com/?l=english'; const URL_LOGIN = 'https://store.steampowered.com/login/'; console.log(datetime(), 'started checking steam'); const db = await jsonDb('steam.json', {}); - handleSIGINT(); -// https://playwright.dev/docs/auth#multi-factor-authentication -const context = await firefox.launchPersistentContext(cfg.dir.browser, { - // chrome will not work in linux arm64, only chromium - // channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge - headless: false, - viewport: { width: cfg.width, height: cfg.height }, - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', - locale: "en-US", // ignore OS locale to be sure to have english text for locators +const context = await chromium.launchPersistentContext(cfg.dir.browser, { + headless: cfg.headless, + locale: "en-US", recordVideo: cfg.record ? { dir: 'data/record/', size: { width: cfg.width, height: cfg.height } } : undefined, recordHar: cfg.record ? { path: `data/record/eg-${datetime()}.har` } : undefined, - args: [ // https://peter.sh/experiments/chromium-command-line-switches - // don't want to see bubble 'Restore pages? Chrome didn't shut down correctly.' - // '--restore-last-session', // does not apply for crash/killed + handleSIGINT: false, + args: [ '--hide-crash-restore-bubble', - // `--disable-extensions-except=${ext}`, - // `--load-extension=${ext}`, ], - // ignoreDefaultArgs: ['--enable-automation'], // remove default arg that shows the info bar with 'Chrome is being controlled by automated test software.'. Since Chromeium 106 this leads to show another info bar with 'You are using an unsupported command-line flag: --no-sandbox. Stability and security will suffer.'. }); -await stealth(context); - -if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); - const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist -// console.debug('userAgent:', await page.evaluate(() => navigator.userAgent)); -const notify_games = []; -let user; - -async function doLogin() { - await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever - if (cfg.steam_username && cfg.steam_password) { - console.info('Using username and password from environment.'); - } - else { - console.info('Press ESC to skip the prompts if you want to login in the browser (not possible in headless mode).'); - } +async function doLogin(page) { const username = cfg.steam_username || await prompt({ message: 'Enter username' }); const password = username && (cfg.steam_password || await prompt({ type: 'password', message: 'Enter password' })); if (username && password) { await page.type('input[type=text]:visible', username); await page.type('input[type=password]:visible', password); - await page.waitForTimeout(2000); - await page.click('button[type=submit]'); - await page.waitForTimeout(2000); - } - const auth = await page.getByText('You have a mobile authenticator protecting this account.').first(); - let isFirstCheck = true; - while (await auth.isVisible()) { - if (isFirstCheck) { - console.log("Steam requires confirmation from authenticator"); - notify(`Steam requires confirmation from authenticator`); - isFirstCheck = false; - } - await page.waitForTimeout(2000); + await Promise.all([page.click('button[type=submit]'), page.waitForNavigation()]); } } + async function claim() { - await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever + await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); + console.log('Navigated to Steam store page'); + await context.addCookies([{ name: 'cookieSettings', value: '%7B%22version%22%3A1%2C%22preference_state%22%3A2%2C%22content_customization%22%3Anull%2C%22valve_analytics%22%3Anull%2C%22third_party_analytics%22%3Anull%2C%22third_party_content%22%3Anull%2C%22utm_enabled%22%3Atrue%7D', domain: 'store.steampowered.com', path: '/' }]); // Decline all cookies to get rid of banner to save space on screen. + console.log('Cookies added'); - const signIn = page.locator('a:has-text("Sign In")').first(); - while (await signIn.isVisible()) { + const loginText = await page.textContent('a.global_action_link'); + const user = await page.locator("#account_pulldown").first().innerText(); + const result = await Promise.race([loginText, user]); + while (await result.includes('Log In')) { console.error('Not signed in to steam.'); - await doLogin(); + loginText = await page.textContent('a.global_action_link'); + user = await page.locator("#account_pulldown").first().innerText(); + result = await Promise.race([loginText, user]); } + console.log('You are logged in as ' + user); - user = await page.locator("#account_pulldown").first().innerText(); - console.error('You are logged in as ' + user); db.data[user] ||= {}; - if (cfg.steam_json) { - await claimJson(); + console.log('Starting to claim from Steam JSON'); + await claimJson(user); + console.log('Finished claiming from Steam JSON'); } if (cfg.steam_gamerpower) { - await claimGamerpower(); + console.log('Starting to claim from GamerPower'); + await claimGamerpower(user); + console.log('Finished claiming from GamerPower'); } + + // Write db.data[user] to a file + writeFileSync(`data/${user}.json`, JSON.stringify(db.data[user], null, 2)); + console.log('Data written to file for user:', user); } -async function claimJson() { +async function claimJson(user) { console.log("Claiming JSON"); const response = await page.goto(cfg.steam_json_url); const items = await response.json(); @@ -111,49 +90,224 @@ async function claimJson() { return; } } - await claimGame(item.url); + await claimGame(item.url, user); } } } -async function claimGamerpower() { +async function claimGamerpower(user) { console.log("Claiming Gamerpower"); - const response = await page.goto("https://www.gamerpower.com/api/giveaways?platform=steam&type=game"); - const items = await response.json(); - for (const item of items) { - console.log(item.open_giveaway_url); - await page.goto(item.open_giveaway_url, { waitUntil: 'domcontentloaded' }); + try { + const response = await page.goto(cfg.steam_gamerpower_url); + if (!response.ok) { + throw new Error(`Failed to fetch GamerPower data: ${response.statusText}`); + } + const items = await response.json(); - const url = page.url(); - if (url.includes("https://store.steampowered.com/app")) { - if (!await isClaimedUrl(url)) { - await claimGame(url); + for (const item of items) { + console.log(item.open_giveaway_url); + try { + await page.goto(item.open_giveaway_url, { waitUntil: 'domcontentloaded' }); + const url = page.url(); + if (url.includes("https://store.steampowered.com/app")) { + if (!await isClaimedUrl(url)) { + await claimGame(url, user); + } + } else if (url.includes("https://store.steampowered.com/agecheck/app")) { + if (!await isClaimedUrl(url)) { + await handleAgeGate(page, 21, 1, 1989); + await claimGame(url, user); + } + } else { + console.log("Game can be claimed outside of Steam! " + url); + } + } catch (error) { + console.error(`Failed to claim game from ${item.open_giveaway_url}:`, error.message); } } - else { - console.log("Game can be claimed outside of steam! " + url); - } + } catch (error) { + console.error(`Error in claimGamerpower:`, error.message); } } -async function claimGame(url) { +// async function claimAgedRestrictedGame(url, user) { +// await page.goto(url, { waitUntil: 'domcontentloaded' }); +// try { +// await page.waitForSelector('#ageDay', { timeout: 5000 }); +// // Select a random day between 1 and 31 +// const dayOptions = document.querySelectorAll('#ageDay option'); +// const dayIndex = Math.floor(Math.random() * 20) + 1; +// await dayOptions[dayIndex].setAttribute('selected', 'true'); + +// // Select a random month between January and December +// const monthOptions = document.querySelectorAll('#ageMonth option'); +// await page.selectOption('#ageMonth', { value: monthOptions[monthIndex] }); + +// // Select a year between 1900 and the current year +// const yearOptions = await page.$$eval('#ageYear option', options => options.map(option => option.value)); +// const currentDate = new Date(); +// const yearIndex = currentDate.getFullYear() - 25; +// await page.selectOption('#ageYear', { value: yearOptions[yearIndex] }); + +// await Promise.all([ +// page.click('#view_product_page_btn'), +// page.waitForNavigation({ waitUntil: 'networkidle2' }) +// ]); +// } catch (error) { +// console.error("Age gate not found or failed to handle"); +// } +// try { +// await claimGame(url, user); +// } catch (error) { +// console.error("Failed to handle age gate or claim game:", error); +// } +// } + +async function handleAgeGate(page, day, month, year, timeout = 30000) { + try { + // 1. Check if age_gate element is visible + console.log('Looking for age gate...'); + const ageGate = page.locator('.age_gate'); + + // Try to wait a little on the element to appear + await page.waitForTimeout(1000); + + const isVisible = await ageGate.isVisible().catch(() => false); + + if (!isVisible) { + console.log('Age gate not found or not visible'); + return false; + } + + console.log('Age gate found, filling in dates...'); + + // 2. Set day + const daySelect = page.locator('#ageDay'); + const isdaySelect = await daySelect.isVisible().catch(() => false); + + if (!isdaySelect) { + console.log('Day select not found. Attempting to click product page button...') + } else { + + await daySelect.waitFor({ timeout }); + await daySelect.selectOption(day.toString()); + console.log(`Day set to: ${day}`); + + // 3. Set month (convert to English month name with capital first letter) + const monthNames = { + 1: 'January', 2: 'February', 3: 'March', 4: 'April', + 5: 'May', 6: 'June', 7: 'July', 8: 'August', + 9: 'September', 10: 'October', 11: 'November', 12: 'December' + }; + + const monthNumber = parseInt(month); + const monthName = monthNames[monthNumber]; + + if (!monthName) { + throw new Error(`Invalid Month: ${month}. Must be between 1-12`); + } + + const monthSelect = page.locator('#ageMonth'); + await monthSelect.waitFor({ timeout }); + await monthSelect.selectOption(monthName); + console.log(`Month set to: ${monthName}`); + + // 4. Set year + const yearSelect = page.locator('#ageYear'); + await yearSelect.waitFor({ timeout }); + await yearSelect.selectOption(year.toString()); + console.log(`Year set to: ${year}`); + } + + // 5. Click on view_product_page_btn + console.log('Clicking on the view product button...'); + const viewProductBtn = page.locator('#view_product_page_btn'); + await viewProductBtn.waitFor({ timeout }); + await viewProductBtn.click(); + + // 6. Wait until the page has loaded + console.log('Waiting for page to load...'); + await page.waitForLoadState('networkidle', { timeout }); + + console.log('Age gate completed'); + return true; + + } catch (error) { + console.error('Error during age gate process:', error.message); + return false; + } +} + + +async function claimGame(url, user) { await page.goto(url, { waitUntil: 'domcontentloaded' }); const title = await page.locator('#appHubAppName').first().innerText(); const pattern = "/app/"; let game_id = page.url().substring(page.url().indexOf(pattern) + pattern.length); game_id = game_id.substring(0, game_id.indexOf("/")); - db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only! + db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only! const notify_game = { title, url: url, status: 'failed' }; notify_games.push(notify_game); // status is updated below - const alreadyOwned = await page.locator('.game_area_already_owned').first(); if (await alreadyOwned.isVisible()) { console.log("Game " + title + " already in library"); db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed } else { - await page.locator(('#freeGameBtn')).click(); + if (url.includes("https://store.steampowered.com/agecheck/app")) { + try { + await page.waitForSelector('#agegate_birthday_desc', { timeout: 5000 }); + // Select a random day between 1 and 31 + const dayIndex = Math.floor(Math.random() * 31) + 1; + await page.selectOption('#ageDay', { value: dayIndex.toString() }); + + // Select a random month between January and December + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const monthIndex = Math.floor(Math.random() * months.length); + await page.selectOption('#ageMonth', { value: months[monthIndex] }); + + // Select a year between 1900 and the current year + const currentDate = new Date(); + const yearIndex = Math.floor(Math.random() * (currentDate.getFullYear() - 1900 + 1)) + 1900; + await page.fill('#ageYear', yearIndex.toString()); + + await Promise.all([ + page.click('#view_product_page_btn'), + page.waitForNavigation({ waitUntil: 'networkidle2' }) + ]); + } catch (error) { + console.error("Age gate not found or failed to handle"); + } + } + try { + await page.waitForSelector('#freeGameBtn', { timeout: 5000 }); // Wait for the free game button to appear + await page.click('#freeGameBtn'); + console.log("purchased (using #freeGameBtn)"); + } catch (error) { + try { + const button = await page.locator('.btn_green_steamui.btn_medium[data-action="add_to_account"]'); + if ((await button.textContent()) === "Add to Account") { + await button.click(); + console.log("purchased (using .btn_green_steamui.btn_medium with text 'Add to Account' and data-action='add_to_account')"); + } else { + console.error(`Button found but text is not 'Add to Account': ${await button.textContent()}`); + } + } catch (error) { + try { + const button = await page.locator('.btn_green_steamui.btn_medium'); + if ((await button.textContent()) === "Add to Account") { + await button.click(); + console.log("purchased (using .btn_green_steamui.btn_medium with text 'Add to Account')"); + } else { + console.error(`Button found but text is not 'Add to Account': ${await button.textContent()}`); + } + } catch (error) { + console.error(`Failed to claim game: Button not found`); + } + } + } + console.log("purchased"); db.data[user][game_id].status = 'claimed'; db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time @@ -175,6 +329,9 @@ async function isClaimedUrl(url) { } } + + + try { await claim(); } catch (error) { @@ -188,6 +345,5 @@ try { notify(`steam (${user}):
${html_game_list(notify_games)}`); } } -if (cfg.debug) writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies())); -if (page.video()) console.log('Recorded video:', await page.video().path()); -await context.close(); +if (cfg.debug) fs.writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies())); +await context.close(); \ No newline at end of file From c071abb62c4f4eb88d6bc7bb19ddcbaf7386196d Mon Sep 17 00:00:00 2001 From: drklien Date: Thu, 17 Jul 2025 12:39:35 +1000 Subject: [PATCH 04/10] updating gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7983ad4..fe8e92a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ data/ *.env +.vscode +.github From 39634da1d5730e9a5290ee2d4e228ccaa65eba5d Mon Sep 17 00:00:00 2001 From: drklien Date: Thu, 17 Jul 2025 12:40:26 +1000 Subject: [PATCH 05/10] updating gitignore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index fe8e92a..831f62f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules/ data/ *.env -.vscode -.github +.vscode/ +.github/ From 3c943828fde7e7cecc359492c43d698ca724914c Mon Sep 17 00:00:00 2001 From: drklien Date: Thu, 17 Jul 2025 12:42:45 +1000 Subject: [PATCH 06/10] Delete .vscode directory --- .vscode/launch.json | 22 ---------------------- .vscode/settings.json | 9 --------- 2 files changed, 31 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 6071636..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Steam", - "outputCapture": "std", - "skipFiles": [ - "/**" - ], - "program": "${workspaceFolder}/steam-games.js", - "env": { - "STEAM_JSON": "1", - "STEAM_GAMERPOWER": "1" - } - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c2cd6b0..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - // https://eslint.style/guide/faq#vs-code - "editor.formatOnSave": true, - "editor.formatOnSaveMode": "modifications", - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - }, - "eslint.experimental.useFlatConfig": true, -} From ae64c9f53d6ff604cd306ef0e810fb3dabf4a574 Mon Sep 17 00:00:00 2001 From: drklien Date: Thu, 17 Jul 2025 12:44:26 +1000 Subject: [PATCH 07/10] updating gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 831f62f..72e8fca 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ node_modules/ data/ *.env .vscode/ -.github/ From dc120ea55bca4972504b775a3ec2decaddca8135 Mon Sep 17 00:00:00 2001 From: drklien Date: Thu, 17 Jul 2025 12:45:22 +1000 Subject: [PATCH 08/10] remvoing old age gate --- steam-games.js | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/steam-games.js b/steam-games.js index f7bc09c..101d53c 100644 --- a/steam-games.js +++ b/steam-games.js @@ -130,39 +130,6 @@ async function claimGamerpower(user) { } } -// async function claimAgedRestrictedGame(url, user) { -// await page.goto(url, { waitUntil: 'domcontentloaded' }); -// try { -// await page.waitForSelector('#ageDay', { timeout: 5000 }); -// // Select a random day between 1 and 31 -// const dayOptions = document.querySelectorAll('#ageDay option'); -// const dayIndex = Math.floor(Math.random() * 20) + 1; -// await dayOptions[dayIndex].setAttribute('selected', 'true'); - -// // Select a random month between January and December -// const monthOptions = document.querySelectorAll('#ageMonth option'); -// await page.selectOption('#ageMonth', { value: monthOptions[monthIndex] }); - -// // Select a year between 1900 and the current year -// const yearOptions = await page.$$eval('#ageYear option', options => options.map(option => option.value)); -// const currentDate = new Date(); -// const yearIndex = currentDate.getFullYear() - 25; -// await page.selectOption('#ageYear', { value: yearOptions[yearIndex] }); - -// await Promise.all([ -// page.click('#view_product_page_btn'), -// page.waitForNavigation({ waitUntil: 'networkidle2' }) -// ]); -// } catch (error) { -// console.error("Age gate not found or failed to handle"); -// } -// try { -// await claimGame(url, user); -// } catch (error) { -// console.error("Failed to handle age gate or claim game:", error); -// } -// } - async function handleAgeGate(page, day, month, year, timeout = 30000) { try { // 1. Check if age_gate element is visible From c42e15b8c29d6dd14584c95d9bdd14b1f3165054 Mon Sep 17 00:00:00 2001 From: drklien Date: Thu, 17 Jul 2025 12:51:41 +1000 Subject: [PATCH 09/10] fixes to db writing --- steam-games.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/steam-games.js b/steam-games.js index 101d53c..7246f91 100644 --- a/steam-games.js +++ b/steam-games.js @@ -72,7 +72,7 @@ async function claim() { } // Write db.data[user] to a file - writeFileSync(`data/${user}.json`, JSON.stringify(db.data[user], null, 2)); + writeFileSync(`data/steam.json`, JSON.stringify(db.data[user], null, 2)); console.log('Data written to file for user:', user); } From f54d399dab4d366c183796a8a89a16bb6460b621 Mon Sep 17 00:00:00 2001 From: drklien Date: Thu, 24 Jul 2025 14:33:30 +1000 Subject: [PATCH 10/10] removing steam-games and reverting vscode folder based on feedback --- .gitignore | 1 - .vscode/launch.json | 22 ++++++++++++++++++++++ .vscode/settings.json | 9 +++++++++ steam-games.json | 8 -------- 4 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json delete mode 100644 steam-games.json diff --git a/.gitignore b/.gitignore index 72e8fca..7983ad4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ node_modules/ data/ *.env -.vscode/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3630f0d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Steam", + "outputCapture": "std", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/steam-games.js", + "env": { + "STEAM_JSON": "1", + "STEAM_GAMERPOWER": "1" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b85ca10 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + // https://eslint.style/guide/faq#vs-code + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "modifications", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "eslint.experimental.useFlatConfig": true, +} \ No newline at end of file diff --git a/steam-games.json b/steam-games.json deleted file mode 100644 index 3e58128..0000000 --- a/steam-games.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "url": "https://store.steampowered.com/app/2447060/Monster_Tiles_TD/" - }, - { - "url": "https://store.steampowered.com/app/2568880/The_Hardest_Game_Ever/" - } -]