import { chromium } from 'patchright'; import dotenv from 'dotenv'; import path from 'path'; import { existsSync, writeFileSync } from 'fs'; 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(); 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, handleSIGINT: false, args: [ '--hide-crash-restore-bubble', ], }); const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist 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 Promise.all([page.click('button[type=submit]'), page.waitForNavigation()]); } } async function claim() { 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 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); db.data[user] ||= {}; if (cfg.steam_json) { console.log('Starting to claim from Steam JSON'); await claimJson(user); console.log('Finished claiming from Steam JSON'); } if (cfg.steam_gamerpower) { 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(user) { 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, user); } } } async function claimGamerpower(user) { console.log("Claiming Gamerpower"); 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(); 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); } } } catch (error) { console.error(`Error in claimGamerpower:`, error.message); } } // 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! 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 { 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 } 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("/")); const 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) fs.writeFileSync(path.resolve(cfg.dir.browser, 'cookies.json'), JSON.stringify(await context.cookies())); await context.close();