// ==UserScript== // @name SteamDB Free Packages Claimer // @namespace https://kurotaku.de // @version 1.0 // @description Automatically claims free packages on SteamDB with configurable filters and duplicate tab detection. // @author Kurotaku // @license CC BY-NC-SA 4.0 // @match https://steamdb.info/* // @icon https://steamdb.info/static/logos/vector_prefers_schema.svg // @updateURL https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/userscripts/SteamDB_Free_Packages_Claimer/script.user.js // @downloadURL https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/userscripts/SteamDB_Free_Packages_Claimer/script.user.js // @require https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/libraries/kuros_library.js // @require https://cdn.jsdelivr.net/npm/sweetalert2 // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_registerMenuCommand // @run-at document-body // ==/UserScript== (async function() { await init_gm_config(); // Verify current URL to ensure the script only executes on the specific free packages subpage if (window.location.pathname !== '/freepackages/') return; // Verify if the param is included (if set) if(!check_required_param()) return; // Execute cross-tab communication check to prevent multiple instances from running simultaneously await check_for_duplicate_tab(); // Retrieve the user-defined startup delay from the configuration const initial_wait = GM_config.get('wait_after_init'); print(`[SteamDB-FPC] Waiting ${initial_wait}s after init...`); await sleep_s(initial_wait); // Pause execution to allow the page to settle // Ensure the user is actually authenticated on SteamDB before proceeding if (!await check_if_signed_in()) return; // Perform an initial scan to see if there are any packages available for claiming if (await check_if_nothing_to_claim()) return; // Check if the current page filter matches the user's preferred filter (e.g., 'games only') await apply_filter(); // Re-verify availability after filtering, as the list might now be empty if (await check_if_nothing_to_claim()) return; // All checks passed; trigger the activation process if (await start_claiming()) { // Wait for the process to finish and get status const is_success = await wait_for_completion(); // If enabled, exclude packages that failed during the process if (GM_config.get('auto_exclude_failed')) await exclude_failed_packages(); // Handle the final tab exit based on success/error await handle_exit(is_success); } })(); async function init_gm_config() { const config_id = "configuration_steamdb_claimer"; GM_registerMenuCommand("Settings", () => GM_config.open()); GM_config.init({ id: config_id, title: 'SteamDB Free Packages Claimer', fields: { preferred_filter: { section: ['General Settings'], label: 'Default Filter', type: 'select', options: ['all', 'owned', 'all-no-demos', 'games', 'demos', 'others'], default: 'all', }, required_url_parameter: { label: 'Requir URL Parameter to start (optional)
For example "autoclaim"
Script only runs if the URL includes the param (e.g. "https://steamdb.info/freepackages/?autoclaim")', type: 'text', default: '', }, wait_after_init: { label: 'Wait after Start (5-60 sec)', type: 'int', min: 3, max: 60, default: 10, }, wait_after_filter: { label: 'Wait after Filter change (5-60 sec)', type: 'int', min: 3, max: 60, default: 10, }, auto_close_empty: { label: 'Auto-close tab if nothing to claim', type: 'checkbox', default: true, }, auto_exclude_failed: { label: 'Auto-exclude failed packages', type: 'checkbox', default: true, }, close_mode_success: { section: ['Automation - On Success'], label: 'Success Close Tab Action', type: 'select', options: ['Disabled', 'Instant', 'Time'], default: 'Time', }, close_time_success: { label: 'Wait time on Success (1-60 sec, only when Time is selected)', type: 'int', min: 1, max: 60, default: 5, }, close_mode_error: { section: ['Automation - On Error'], label: 'Error Close Tab Action', type: 'select', options: ['Disabled', 'Instant', 'Time'], default: 'Time', }, close_time_error: { label: 'Wait time on Error (1-60 sec, only when Time is selected)', type: 'int', min: 1, max: 60, default: 30, } }, events: { save: () => { location.reload(); } }, frame: create_configuration_container(), }); await wait_for_gm_config(); } async function check_if_signed_in() { // If this link exists, the user is not logged in const signed_in = document.querySelector("header a.header-login") === null; if (!signed_in) { print("[SteamDB-FPC] ERROR: You are not logged in! Script stopping."); await sleep_s(5); window.close(); return false; } return true; } function check_required_param() { const required_param = GM_config.get('required_url_parameter').trim(); if (required_param !== "") { const url_params = new URLSearchParams(window.location.search); if (!url_params.has(required_param)) { print(`[SteamDB-FPC] Required parameter "${required_param}" not found in URL. Script stopped.`); return false; } print(`[SteamDB-FPC] Parameter "${required_param}" detected. Starting automation...`); } return true; } async function check_for_duplicate_tab() { return new Promise((resolve) => { // Use a descriptive channel name for SteamDB free packages const channel = new BroadcastChannel('steamdb_freepackages_claimer'); const instance_id = Math.random().toString(36).substring(7); // Unique ID for this specific tab let duplicate_detected = false; channel.onmessage = async (event) => { // Destructure message for easier access const { msg, id } = event.data; if (id === instance_id) return; // Ignore our own messages switch (msg) { case 'check_for_active_instance': // If another tab asks, confirm we are active channel.postMessage({ msg: 'freepackages_active', id: instance_id }); break; case 'freepackages_active': // If we get a response, a duplicate exists duplicate_detected = true; channel.close(); if (typeof Swal !== 'undefined') { await Swal.fire({ title: 'Duplicate Detected', text: 'Another SteamDB Claimer tab is already active. Stopping this one...', icon: 'warning', timer: 3000, backdrop: true, theme: "dark", showConfirmButton: false, }); await sleep_s(3); } window.close(); // Resolve true so the main script knows to stop, even if window.close() fails resolve(true); break; } }; // Broadcast a message to see if any other tab already has the script running channel.postMessage({ msg: 'check_for_active_instance', id: instance_id }); // Wait 1000ms for responses; if none come, we are the primary tab setTimeout(() => { if (!duplicate_detected) resolve(false); }, 1000); }); } async function check_if_nothing_to_claim() { // Wait for the loading container to appear const loading_box = await wait_for_element("#loading"); // Check if the empty-state message is present in the container if (loading_box.innerText.includes("There are no free packages remaining for you.")) { print("[SteamDB-FPC] Nothing to claim."); // Close the tab if the specific auto-close setting is enabled if (GM_config.get("auto_close_empty")) window.close(); return true; } return false; } async function apply_filter() { // Retrieve the user-defined filter from settings const target = GM_config.get("preferred_filter"); const container = document.querySelector(".app-history-filters"); // If filter container is missing, assume we can proceed if (!container) return true; // Check if the current active button already matches our target filter const active_btn = container.querySelector(".btn-info"); if (active_btn?.getAttribute("data-filter") === target) return true; // Find and click the target filter button if it's not already active const target_btn = container.querySelector(`.js-filter[data-filter="${target}"]`); if (target_btn) { target_btn.click(); // If the filter was just changed, we need to wait for the page content to update const filter_wait = GM_config.get('wait_after_filter'); print(`[SteamDB-FPC] Filter mismatch, waiting ${filter_wait}s for update...`); await sleep_s(filter_wait); return false; // Return false to indicate that a refresh happened } return true; } async function start_claiming() { const timeout = 60; // Timeout duration in seconds print(`[SteamDB-FPC] Searching for activation button (Timeout: ${timeout}s)...`); // Race between your wait_for_element and a timeout timer const start_button = await Promise.race([ wait_for_element("#js-activate-now"), new Promise(resolve => setTimeout(() => resolve(null), timeout * 1000)) ]); // If the timeout won the race, start_button will be null if (!start_button) { print(`[SteamDB-FPC] ERROR: Activation button not found within ${timeout}s. Closing tab...`); if (typeof Swal !== 'undefined') await Swal.fire({ title: 'Timeout', text: `The activation button did not appear within ${timeout} seconds. Closing tab...`, icon: 'error', timer: 5000, backdrop: true, theme: "dark", showConfirmButton: false }); window.close(); return false; } // If the button exists but is disabled (e.g., already claimed or Steam error) if (start_button.classList.contains("disabled") || start_button.hasAttribute("disabled")) { print("[SteamDB-FPC] Button found, but it is currently disabled. Closing tab..."); window.close(); return false; } // If we reached this point, the button is found and clickable print("[SteamDB-FPC] Button found. Starting activation..."); start_button.click(); // Start monitoring the progress return true; } async function wait_for_completion() { // Wait for any h4 status message to appear in the loading container const status_element = await wait_for_element("#loading h4"); const status_text = status_element.innerText; // Check if the process finished successfully const is_success = status_text.includes("Script finished."); print(`[SteamDB-FPC] Process finished. Success: ${is_success}`); return is_success; } async function exclude_failed_packages() { print("[SteamDB-FPC] Scanning for failed packages to exclude..."); // List of error messages that trigger an automatic exclusion const failure_messages = [ "You do not own the required app", "Steam says this is an invalid package", "There was a problem adding this product to your account" ]; // Select all status rows within the loading container const rows = document.querySelectorAll("#loading .tabular-nums"); let excluded_count = 0; rows.forEach(row => { const text = row.innerText.toLowerCase(); // Check if the current row contains any of the failure messages const should_exclude = failure_messages.some(msg => text.includes(msg.toLowerCase())); if (should_exclude) { const btn_exclude = row.querySelector(".js-remove"); if (btn_exclude) { // Trigger the [Ignore] button click btn_exclude.click(); excluded_count++; // Visual feedback: dim and strike through the processed row row.style.opacity = "0.5"; row.style.textDecoration = "line-through"; } } }); if (excluded_count > 0) print(`[SteamDB-FPC] Automatically excluded ${excluded_count} failed packages.`); // Small delay to ensure SteamDB processes the exclusion requests before tab closes await sleep_s(2); } async function handle_exit(is_success) { // Wait for the final status message to be sure const status_element = document.querySelector("#loading h4"); const status_text = status_element ? status_element.innerText : "Unknown state"; // Determine the action and wait time based on whether it was a success or an error const mode = is_success ? GM_config.get('close_mode_success') : GM_config.get('close_mode_error'); const wait_time = is_success ? GM_config.get('close_time_success') : GM_config.get('close_time_error'); print(`[SteamDB-FPC] Exit status: "${status_text}"`); switch (mode) { case 'Disabled': print("[SteamDB-FPC] Auto-close is disabled for this state."); break; case 'Instant': window.close(); break; case 'Time': print(`[SteamDB-FPC] Closing in ${wait_time}s...`); await sleep_s(wait_time); window.close(); break; } }