Compare commits

..

518 commits

Author SHA1 Message Date
nocci
67afeead60 chore: chown /fgc/data on start so fgc can write bind-mount
All checks were successful
build-and-push / lint (push) Successful in 5s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m13s
2025-12-30 17:36:31 +00:00
nocci
8ebb57a706 Add missing Playwright deps and clear Firefox locks on start
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m10s
2025-12-30 17:27:28 +00:00
nocci
949206dbf2 Ensure /tmp/.X11-unix exists with sane perms
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m16s
2025-12-30 17:21:53 +00:00
nocci
cfc1e0ee12 Harden entrypoint: skip user.js on RO volume and auto-install browsers
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 13s
build-and-push / docker (push) Successful in 1m13s
2025-12-30 17:10:53 +00:00
nocci
4d835013cc Skip browser prefs when volume is read-only
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m12s
2025-12-30 17:05:01 +00:00
nocci
6f2e1e5b22 Make browser prefs creation non-fatal when unwritable
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 11s
build-and-push / docker (push) Successful in 1m11s
2025-12-30 16:58:22 +00:00
nocci
e3e6a6f36c Handle unwritable browser volume in entrypoint
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m11s
2025-12-30 16:54:42 +00:00
nocci
a477bb3327 Fix lint: guard browser globals and normalize indentation
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m12s
2025-12-30 16:47:46 +00:00
nocci
7ffc454e47 Clean up Sonar issues and lint warnings
Some checks failed
build-and-push / lint (push) Failing after 5s
build-and-push / sonar (push) Has been skipped
build-and-push / docker (push) Has been skipped
2025-12-30 16:45:17 +00:00
nocci
b9aa6e0073 Use sonar.token instead of deprecated sonar.login
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 11s
build-and-push / docker (push) Successful in 1m14s
2025-12-30 16:19:53 +00:00
nocci
bab4359977 Fix indentation in unrealengine eslint
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m11s
2025-12-30 16:16:29 +00:00
nocci
52895fd991 Fix eslint no-unused-vars in async handlers
Some checks failed
build-and-push / lint (push) Failing after 4s
build-and-push / sonar (push) Has been skipped
build-and-push / docker (push) Has been skipped
2025-12-30 16:15:14 +00:00
nocci
3fd861f134 Fix eslint no-empty and clean tests
Some checks failed
build-and-push / lint (push) Failing after 4s
build-and-push / sonar (push) Has been skipped
build-and-push / docker (push) Has been skipped
2025-12-30 16:13:15 +00:00
nocci
5f919039ab Clean Sonar issues in store scripts
Some checks failed
build-and-push / lint (push) Failing after 4s
build-and-push / sonar (push) Has been skipped
build-and-push / docker (push) Has been skipped
2025-12-30 16:08:59 +00:00
nocci
397871b012 Further clean Sonar: merge base RUN, strip comments, node imports
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m13s
2025-12-30 15:47:28 +00:00
nocci
405e801851 Clean Sonar findings: merge RUNs, drop commented code, update node imports
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 11s
build-and-push / docker (push) Successful in 1m13s
2025-12-30 15:39:11 +00:00
nocci
37ffd09545 Refactor prompt timeout plugin to reduce nesting
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 11s
build-and-push / docker (push) Successful in 1m11s
2025-12-30 14:56:50 +00:00
nocci
69282c63d5 Address Sonar warnings and harden runtime
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m13s
2025-12-30 14:54:14 +00:00
nocci
9e2bc89ff2 chore: clean up util notify/prompt lint findings
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 12s
build-and-push / docker (push) Successful in 1m11s
2025-12-30 14:36:41 +00:00
nocci
e4b1f60a66 chore: use execFile for git commands in version check
All checks were successful
build-and-push / lint (push) Successful in 5s
build-and-push / sonar (push) Successful in 11s
build-and-push / docker (push) Successful in 1m11s
2025-12-30 14:31:17 +00:00
nocci
f82c158a6b ci: run sonar-scanner locally instead of docker
All checks were successful
build-and-push / lint (push) Successful in 5s
build-and-push / sonar (push) Successful in 16s
build-and-push / docker (push) Successful in 1m10s
2025-12-30 14:27:12 +00:00
nocci
b5e9111039 ci: mount workspace explicitly for sonar scanner
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / docker (push) Successful in 1m12s
build-and-push / sonar (push) Successful in 7s
2025-12-30 14:23:43 +00:00
nocci
94be980c95 ci: debug sonar scanner mount and remove extra inclusions
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 10s
build-and-push / docker (push) Successful in 1m10s
2025-12-30 14:22:07 +00:00
nocci
00b36a65b1 ci: quote sonar inclusions to avoid shell glob
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 7s
build-and-push / docker (push) Successful in 1m8s
2025-12-30 13:25:17 +00:00
nocci
2a4653062a ci: make sonar scan pick up sources
All checks were successful
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Successful in 6s
build-and-push / docker (push) Successful in 1m10s
2025-12-30 13:22:54 +00:00
nocci
25fb2d9838 chore: remove github workflows and configs
All checks were successful
build-and-push / lint (push) Successful in 5s
build-and-push / sonar (push) Successful in 7s
build-and-push / docker (push) Successful in 1m13s
2025-12-30 13:19:36 +00:00
nocci
6c4eb948fc alles jut 2025-12-30 13:08:44 +00:00
nocci
405660e0f2 🐛 fix(ci): adjust sonar scm configuration
All checks were successful
build-and-push / lint (push) Successful in 5s
build-and-push / sonar (push) Successful in 6s
build-and-push / docker (push) Successful in 1m11s
- change sonar.scm.provider to sonar.scm.disabled for compatibility
2025-12-30 13:06:49 +00:00
nocci
02972a04d4 🐛 fix(ci): handle missing sonar project key gracefully
Some checks failed
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Failing after 6s
build-and-push / docker (push) Has been skipped
- set default empty value for PROJECT_KEY variable
- add fallback to read sonar-project.properties if secret is not provided
- exit with error if PROJECT_KEY is still unset after checks
2025-12-30 13:05:40 +00:00
nocci
608b6b87cd 👷 ci(build): simplify sonar project key retrieval
Some checks failed
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Failing after 6s
build-and-push / docker (push) Has been skipped
- remove redundant conditional logic for project key extraction
- enforce mandatory SONAR_PROJECT_KEY secret for consistency
- change sonar.scm.provider to 'none' for improved configuration
2025-12-30 13:02:52 +00:00
nocci
e11d40bdda 👷 ci(build): enforce sonar host url secret presence
Some checks failed
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Failing after 6s
build-and-push / docker (push) Has been skipped
- update HOST_URL to require SONAR_HOST_URL secret for enhanced security configuration
- improve error handling by ensuring critical secrets are set
2025-12-30 13:01:14 +00:00
nocci
8626fa5a0f ♻️ refactor(build): enhance sonar scanner configuration
- set default host url for sonar scanner to ensure consistency
- add sonar.sources and sonar.scm.provider for improved analysis
2025-12-30 12:57:44 +00:00
nocci
56ca1f63d4 👷 ci(build): enhance sonar project key configuration
Some checks failed
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Failing after 6s
build-and-push / docker (push) Has been skipped
- add SONAR_PROJECT_KEY environment variable for flexibility
- fallback to default or file-based project key if not set
2025-12-30 12:42:36 +00:00
nocci
5b1d966c6e 👷 ci(build): enhance sonar scanner configuration
Some checks failed
build-and-push / lint (push) Successful in 4s
build-and-push / sonar (push) Failing after 7s
build-and-push / docker (push) Has been skipped
- extract project key from properties file for dynamic setup
- add projectBaseDir to sonar scanner command for accurate analysis
2025-12-30 12:40:06 +00:00
nocci
d40a577f47 👷 ci(build): add SonarQube scan to build workflow
Some checks failed
build-and-push / lint (push) Successful in 5s
build-and-push / sonar (push) Failing after 12s
build-and-push / docker (push) Has been skipped
- introduce SonarQube scanning step for code quality analysis
- update workflow dependencies and execution order

🐛 fix(auth): improve error handling and code formatting

- remove unused imports and fix code indentation
- enhance error handling with improved catch blocks

💄 style(general): standardize code formatting and style consistency

- update various files to ensure consistent code style
- adjust indentation and whitespace for readability
2025-12-30 12:38:03 +00:00
nocci
0e5303da62 👷 ci(workflow): add lint job to build workflow
Some checks failed
build-and-push / lint (push) Failing after 32s
build-and-push / docker (push) Has been skipped
- introduce lint job in build.yml for code quality checks
- ensure lint job runs before docker job
- setup Node.js and install dependencies for ESLint
2025-12-30 11:03:08 +00:00
e39cca93c2 better README.md
All checks were successful
build-and-push / docker (push) Successful in 1m10s
2025-12-29 17:19:39 +01:00
nocci
9d79f9ac78 📝 docs(README): update configuration and remove CI section
All checks were successful
build-and-push / docker (push) Successful in 1m8s
- remove outdated CI/build instructions
- add new environment variable options for configuration

🐛 fix(util): handle notification errors gracefully

- resolve promise instead of rejecting on notification errors
- prevent whole run from failing due to notification issues
2025-12-29 15:54:41 +00:00
nocci
0a729d0cbf 🔧 chore(workflow): simplify docker image tagging and pushing
All checks were successful
build-and-push / docker (push) Successful in 1m10s
- remove specific sha tag from build and push steps
- streamline workflow by focusing on latest tag
2025-12-29 15:08:00 +00:00
nocci
eb5b9bbb6e 👷 ci(build): enhance docker build process with buildx
All checks were successful
build-and-push / docker (push) Successful in 1m12s
- add Docker Buildx setup for advanced build capabilities
- update build step to use buildx for multi-platform support
2025-12-29 15:01:55 +00:00
nocci
eba07721ca 📝 docs(README): update instructions and clarify configurations
Some checks failed
build-and-push / docker (push) Failing after 1m55s
- update registry image references to point to `git.sky-net.it/nocci/free-games-claimer`
- improve clarity on Docker and Docker Compose examples
- translate German sections into English for wider accessibility
- add detailed explanations for environment variables and configurations
- enhance quickstart and CI instructions for better understanding
2025-12-29 14:38:54 +00:00
nocci
bde0f34a80 📝 docs(README): simplify and update documentation
Some checks failed
build-and-push / docker (push) Failing after 0s
- remove detailed setup instructions and history
- add quick-start guide for Docker and Docker Compose
- update CI and configuration sections
- streamline language and formatting for clarity
2025-12-29 14:36:18 +00:00
nocci
6f778d71ac 📝 docs(README): add instructions for building images in Forgejo
Some checks failed
build-and-push / docker (push) Failing after 0s
- include steps for building and pushing Docker images using Forgejo
- provide details on setting Forgejo secrets and using self-hosted runners
2025-12-29 14:33:14 +00:00
nocci
a21dced86e 📦 build(ci): add build-and-push workflow for docker images
Some checks failed
build-and-push / docker (push) Failing after 4s
- create a new CI workflow to automate image building and pushing
- trigger workflow on push to main branch
- include steps for checkout, login, build, and push docker images
2025-12-29 14:31:25 +00:00
nocci
76f578e2e6 feat(auth): enhance automatic login and MFA handling
Some checks failed
Build and push Docker image (amd64, arm64 to hub.docker.com and ghcr.io) / docker (push) Has been cancelled
Lint / Lint (push) Has been cancelled
Sonar / sonarcloud (push) Has been cancelled
- add handleMFA function for improved two-step verification
- implement direct login page handling with automatic sign-in

🐛 fix(claim): improve game claim process and duplicate prevention

- normalize claim URLs and deduplicate by URL
- fix various selectors for claim buttons and handle different languages
- prevent duplicate game claims by checking existing records

♻️ refactor(utils): improve code readability and maintainability

- extract normalizeClaimUrl function for URL handling
- restructure logic for claim and notification processes

🌐 i18n(claim): add support for game claim text in German

- handle German text for claim buttons and status checks
2025-12-29 14:15:22 +00:00
nocci
faf22aafb1 🐛 fix(prime-gaming): update URL and selectors for claims
Some checks are pending
Build and push Docker image (amd64, arm64 to hub.docker.com and ghcr.io) / docker (push) Waiting to run
Lint / Lint (push) Waiting to run
Sonar / sonarcloud (push) Waiting to run
- change URL_CLAIM to point to new Luna claims home
- update selectors for sign-in and user verification
- improve handling of cookies acceptance

 feat(prime-gaming): enhance game claiming logic

- add support for new layout and game list detection
- implement flexible scrolling for loading all game cards
- refine logic for internal and external game claims
- improve store identification for external claims

♻️ refactor(prime-gaming): modularize game tab and list location

- extract functions for opening games tab and locating games list
- enhance code readability and maintainability

🐛 fix(prime-gaming): handle dynamic selectors for availability dates

- support multiple selectors for availability date detection
- improve error handling and logging for missing elements
2025-12-29 11:41:09 +00:00
Ralf Vogler
99c1f05302 Merge branch 'dev' 2025-05-16 00:15:45 +02:00
Ralf Vogler
991e0a1449 docker: only build branches main, dev
Otherwise branches will accumulate in 'Recent tagged image version'
https://github.com/vogler/free-games-claimer/pkgs/container/free-games-claimer
2025-05-16 00:14:10 +02:00
Ralf Vogler
cea5b11c95 mv renovate.json .github/ 2025-05-16 00:06:39 +02:00
Ralf Vogler
6adc7e529a disable Renovate for now 2025-05-16 00:05:48 +02:00
Ralf Vogler
158caa6164
Merge pull request #469 from vogler/renovate/super-linter-super-linter-7.x 2025-05-16 00:01:31 +02:00
Ralf Vogler
0a4cac3571 ncu -u 2025-05-15 23:51:49 +02:00
Ralf Vogler
bf0c9032cc need 'packages: write' permission for ghcr.io 2025-05-14 01:59:37 +02:00
Ralf Vogler
4ce0d44b79 can't check secrets in if expr... 2025-05-14 01:44:14 +02:00
Ralf Vogler
7c8682ac91 build docker image for each branch/PR? 2025-05-14 01:31:04 +02:00
Ralf Vogler
a5ce5ec816 fix CodeQL alerts 5-7
https://github.com/vogler/free-games-claimer/security/code-scanning/5
https://github.com/vogler/free-games-claimer/security/code-scanning/6
https://github.com/vogler/free-games-claimer/security/code-scanning/7
2025-05-14 00:46:37 +02:00
Ralf Vogler
6aea18836d enquirer: esc is fine, but after first prompt SIGINT will be ignore; onRawSIGINT keeps process running -> switch to inquirer 2025-05-14 00:30:39 +02:00
Ralf Vogler
97ef14f514 uncommited steam-games.js from 2024-09-24 - WIP just saves all games and their stats 2025-05-14 00:18:22 +02:00
Ralf Vogler
5cfe57870a note on git describe for version 2025-05-13 23:43:29 +02:00
renovate[bot]
3e13e9fba8
chore(deps): update super-linter/super-linter action to v7.4.0 2025-05-13 20:29:21 +00:00
Ralf Vogler
dee47d2218 pg: redeem gog: detect captcha 2025-04-15 17:27:21 +02:00
Ralf Vogler
726db4527b Current free game title in blue 2025-04-15 17:26:27 +02:00
Ralf Vogler
04d1b7ea9e pg: fix auto-login; rememberMe was removed 2025-04-15 17:22:21 +02:00
Ralf Vogler
0fefcc47b7 pg: skipBasedOnTime if days left > PG_TIMELEFT 2025-04-15 17:08:34 +02:00
Ralf Vogler
92cb32ff2d ae: try fingerprint-injector 2025-04-03 15:33:00 +02:00
Ralf Vogler
90e3400072
readme: logos: use GitHub asset upload 2025-03-27 17:11:30 +01:00
Ralf Vogler
041a77d9f6
readme: logos: align: middle - CSS is stripped 2025-03-27 17:01:18 +01:00
Ralf Vogler
787522473f readme: logos: vertical-align: middle 2025-03-27 16:53:41 +01:00
Ralf Vogler
cfe42db805 pg: fix logo 2025-03-27 16:53:27 +01:00
Ralf Vogler
8b7018f54c fix actionlint errors (docker.yml)
Double quote to prevent globbing and word splitting
2025-03-27 16:26:40 +01:00
Ralf Vogler
ea69c76b50 eg: fix #449: only notify once if 'Game already in library' 2025-03-11 10:14:42 +01:00
Ralf Vogler
3626fc1762 fixup #417 2025-03-05 16:17:48 +01:00
Ralf Vogler
d2d85dee8c
Merge pull request #417 from NeoMod/patch-1
Fixed missing game link in notification for epic-games on captcha halt
2025-03-05 16:11:37 +01:00
Ralf Vogler
8d538290a8
Merge pull request #387 from srounce/patch-1
fix: docker-entrypoint.sh uses safe shebang
2025-03-05 15:57:54 +01:00
Ralf Vogler
daef12b562
Merge pull request #441 from vogler/renovate/docker-build-push-action-6.x
chore(deps): update docker/build-push-action action to v6
2025-03-05 15:54:29 +01:00
Ralf Vogler
acaf6691d4
Merge pull request #440 from vogler/renovate/major-eslint-stylistic-monorepo
chore(deps): update dependency @stylistic/eslint-plugin-js to v4
2025-03-05 15:54:12 +01:00
renovate[bot]
e75975b273
chore(deps): update docker/build-push-action action to v6 2025-03-05 14:54:01 +00:00
renovate[bot]
cb22abfb2f
chore(deps): update dependency @stylistic/eslint-plugin-js to v4 2025-03-05 14:53:57 +00:00
Ralf Vogler
67058850d5
Merge pull request #438 from vogler/renovate/chalk-5.x-lockfile
fix(deps): update dependency chalk to v5.4.1
2025-03-05 15:47:47 +01:00
Ralf Vogler
3ac05a5593
Merge pull request #434 from vogler/renovate/eslint-stylistic-monorepo
chore(deps): update dependency @stylistic/eslint-plugin-js to v2.13.0
2025-03-05 15:47:30 +01:00
renovate[bot]
d620ee2731
chore(deps): update dependency @stylistic/eslint-plugin-js to v2.13.0 2025-03-05 14:46:12 +00:00
Ralf Vogler
f47ffe0e9f
Merge pull request #436 from vogler/renovate/eslint-monorepo
chore(deps): update dependency eslint to v9.21.0
2025-03-05 15:45:33 +01:00
Ralf Vogler
253a3f7253
Merge pull request #433 from vogler/renovate/dotenv-16.x-lockfile
fix(deps): update dependency dotenv to v16.4.7
2025-03-05 15:44:52 +01:00
Ralf Vogler
5e5c257b83
Merge pull request #437 from vogler/super-linter
try super-linter
2025-03-05 15:41:35 +01:00
Ralf Vogler
6aa0f9b817 super-linter: problem matchers included? 2025-03-05 15:34:03 +01:00
Ralf Vogler
bf2216f731 super-linter: TODO fix-lint-issues 2025-03-05 15:27:00 +01:00
Ralf Vogler
d6666aed03 super-linter: use slim version 2025-03-05 15:17:34 +01:00
renovate[bot]
041605dcf7
fix(deps): update dependency chalk to v5.4.1 2025-03-05 14:08:35 +00:00
renovate[bot]
d76c03ed6a
chore(deps): update dependency eslint to v9.21.0 2025-03-05 14:08:28 +00:00
Ralf Vogler
fed03428f3 try super-linter
https://github.com/marketplace/actions/super-linter#get-started
2025-03-05 15:07:37 +01:00
renovate[bot]
4a018c20e9
fix(deps): update dependency dotenv to v16.4.7 2025-03-05 13:22:40 +00:00
Ralf Vogler
ceb873ea0b
Merge pull request #432 from vogler/renovate/configure
chore: Configure Renovate
2025-03-05 14:22:06 +01:00
renovate[bot]
9e87ce58ac
Add renovate.json 2025-03-05 13:17:13 +00:00
NeoMod
b3ab8f7830
Fixed missing game link in notification for epic-games on captcha halt
This fixes the "game link" missing from notification when checking Epic Games Store and encountering a captcha, as per #259 should have been but instead wasn't. Issue was identified in #402 comment by "vttc08" (https://github.com/vogler/free-games-claimer/issues/402#issuecomment-2510818082)

I also added the missing notification for "Game Already in Library" for Epic Games Store, since I felt it is useful because it provides an easy way to know if the script is working or not during scheduled usage.

This also provided the opportunity to check if the game url link was handled properly outside "Discord": I have added a commented option where one could activate the "Already in Library" notification that will also provide the game url link for easy verification.
2025-01-01 15:31:04 +01:00
Ralf Vogler
33d2493336
Merge pull request #411 from AgentTechnoman/main
Fix gog error per @jordyamc
2024-12-30 04:29:09 +01:00
AgentTechnoman
d5072a62c0
Fix gog error per @jordyamc
https://github.com/vogler/free-games-claimer/issues/398#issuecomment-2487274414
2024-12-19 16:46:18 -07:00
Samuel Rounce
c5a7a10ca3
fix: docker-entrypoint.sh uses safe shebang
Shebangs should use /usr/bin/env to locate the interpreter. Setting a path directly to the interpreter itself tends to be brittle and prone to breaking.
2024-09-28 18:34:39 +01:00
Ralf Vogler
c8cf7362fa eg: wait for purchaseBtn to have some text, fixes #375 2024-09-01 01:20:45 +02:00
Ralf Vogler
fef5b97e3b eg: wait for purchaseBtn, without it didn't detect bundle as already claimed 2024-08-29 20:23:43 +02:00
Ralf Vogler
292aadae3c eg: list games included in a bundle 2024-08-29 20:06:59 +02:00
Ralf Vogler
7279ba06e8 eg: change back to purchase-cta-button from 'aside button', fixes #374 2024-08-29 19:48:28 +02:00
Ralf Vogler
1f4af79e0e pg: fix loading all games by 2*PageDown + checking height instead of End + checking count..., #357 2024-08-29 13:23:45 +02:00
Ralf Vogler
c8e0640490 eg: #371: dump HTML in case of EULA popup to find new locator
https://github.com/vogler/free-games-claimer/issues/371
2024-08-29 12:46:28 +02:00
Ralf Vogler
eafb2316f7 pg: login now has Continue button after email 2024-08-28 02:31:47 +02:00
Ralf Vogler
c87deb393e pg: DLC: forgot to await scrollUntilStable, closes #356 2024-08-02 14:02:33 +02:00
Ralf Vogler
23a611a2f1 ci/docker: fix: tag main as latest 2024-08-02 13:19:39 +02:00
Ralf Vogler
eac11e8949 ci/docker: force tag main as latest since many people pull it 2024-08-02 13:06:28 +02:00
Ralf Vogler
8605b70377 pg: fix internal/external locators, closes #355, waitUntilStable 2s -> 5s 2024-08-02 12:57:37 +02:00
Ralf Vogler
d9e91d22c9 eg: fix changed button locators 2024-08-01 15:46:41 +02:00
Ralf Vogler
a39d737999 aliexpress: collect daily coins via desktop website
All load in webview in android app.
Other games have no desktop version and mobile version uses canvas
and only refers to download app with both stealth and fingerprint-injector.
Also stuck on loading screen when using firefox instead of chrome.
2024-07-19 14:35:40 +02:00
Ralf Vogler
b8f7068a87 pg: scrollUntilStable to wait in loop to load all games since one scroll may not be enough 2024-07-10 13:27:48 +02:00
Ralf Vogler
17f1ee41c9 eg: don't exit on login error since it may be 'Incorrect response' for captcha
https://github.com/vogler/free-games-claimer/issues/183#issuecomment-2195691372
2024-06-27 23:32:15 +02:00
Ralf Vogler
aee72327ea eg: disable webgl since it leaks running virtualized, #183 2024-06-27 15:41:58 +02:00
Ralf Vogler
00275d825b eg: forgot to catch timeout for captcha detection 2024-06-27 15:40:45 +02:00
Ralf Vogler
e154b74e4d eg: TODO handle base game for add-ons via function 2024-06-27 14:09:37 +02:00
Ralf Vogler
d85cd8d20c rm xbox.js, no more games with gold, closes #286, closes #339
TODO integrate login code for microsoft games in prime-gaming
2024-06-27 10:54:46 +02:00
Ralf Vogler
9af36e902c eg: fix login: password now together with email again, detect login error, closes #338, closes #337 2024-06-26 17:16:38 +02:00
Ralf Vogler
920fc4d186
Merge pull request #318 from Targunitoth/main
Escape the apprise -b parameter
2024-06-25 18:52:13 +02:00
Ralf Vogler
c4be7ece21 xbox: fixup #307 2024-06-25 18:45:21 +02:00
Ralf Vogler
b8071d442c
Merge pull request #307 from Rexexe/main
Resolve error with no default data
2024-06-25 18:44:25 +02:00
Ralf Vogler
9cf5d2f7f2 pg: include code in redeem_url for gog, closes #330 2024-06-25 15:50:02 +02:00
Ralf Vogler
55226933c0 filenamify datetime recordHar for Windows : -> ., fix #336 2024-06-25 15:30:02 +02:00
Ralf Vogler
357a780aa9
Merge pull request #325 from Tymec/main
Ability to use a different email for Legacy Games
2024-06-25 13:31:06 +02:00
Ralf Vogler
afceb0aad9
Merge branch 'main' into main 2024-06-25 13:30:42 +02:00
Ralf Vogler
c0eb6dbb0b ncu -u: playwright 1.44.1 -> 1.45.0 2024-06-25 13:24:55 +02:00
Ralf Vogler
076738e301 gog: fail if WIDTH<1280 due to hidden username, closes #335 2024-06-25 13:09:10 +02:00
Ralf Vogler
601e893714 README: link example how to set env vars on Windows, closes #314 2024-06-23 20:50:00 +02:00
Ralf Vogler
43bef9b23c pg: redeem microsoft store and xbox, WIP, #315, #5 2024-06-23 20:44:03 +02:00
Ralf Vogler
f920aa26d0 pg: checkTimeLeft via PG_TIMELEFT=1 2024-06-23 19:18:30 +02:00
Ralf Vogler
30b1835bdb pg: reverse games: oldest/bottom to newest/top 2024-06-23 19:16:01 +02:00
Ralf Vogler
f0f142733e ncu -u: lowdb 6 -> 7 dropped support for Node 16 2024-06-23 19:14:13 +02:00
Ralf Vogler
cf59da5d9e gog: new locators, fixes #326, #334 2024-06-23 18:03:38 +02:00
Ralf Vogler
2d81ed2145
Merge pull request #329 from nett00n/patch-1
Update README.md
2024-06-21 18:12:04 +02:00
Vladimir Budylnikov
f7c23569c7
Update README.md
I've made i howto for windows users
2024-06-02 13:33:51 +04:00
Ralf Vogler
9ef9798626 eg: detect 'Failed to challenge captcha, please try again later.' 2024-05-23 15:55:07 +02:00
Ralf Vogler
e49fa930e0 eg: claim base game before add-on 2024-05-23 15:54:23 +02:00
Tymec
c4ae3505b3
Update README.md 2024-05-18 19:50:13 +02:00
Tymec
8c535f48fa
Update prime-gaming.js 2024-05-18 19:47:18 +02:00
Tymec
075106da11
Update config.js 2024-05-18 19:45:59 +02:00
Ralf Vogler
3338d08a1d eg: fix title for Bundles, TODO got stuck after
happened for 'LISA: The Definitive Edition' - https://store.epicgames.com/en-US/bundles/lisa-the-definitive-edition
2024-05-02 16:58:31 +02:00
Targunitoth
da6964c19a
Escape the apprise -b parameter 2024-04-25 13:51:40 +02:00
Ralf Vogler
6f06fccd46 eslint: prefer-const 2024-03-07 13:47:58 +01:00
Rex
61fdf566c9 Resolve error with no default data 2024-03-01 20:57:00 +08:00
Ralf Vogler
285c7a44fd README.md: comment alternative starchart.cc 2024-02-29 16:35:02 +01:00
Ralf Vogler
2ea0c611e4 pg: fix claiming external games, detect store early in description, delete old code 2024-02-13 23:48:48 +01:00
Ralf Vogler
e4e4ce703d pg: also get and store url for internal games 2024-02-13 23:45:36 +01:00
Ralf Vogler
fc810af80e
Update README.md: add Repobeats insights 2024-01-11 15:58:56 +01:00
Ralf Vogler
28bcceb285 eg: enter date of birth if age confirmation pops up, #275 2023-12-31 16:58:56 +01:00
Ralf Vogler
0ec24ef062 fix import paths for test/ 2023-12-30 13:37:04 +01:00
Ralf Vogler
c3657f05ac workaround for cropped viewport (Playwright regression), fixes #277 2023-12-28 17:56:09 +01:00
Ralf Vogler
7ca79bd413 eg: debug window.screen 2023-12-28 17:31:23 +01:00
Ralf Vogler
56cfab6e21
Update README.md 2023-12-28 11:28:23 +01:00
Ralf Vogler
fb8f38706a
mention pipx, #276 2023-12-27 10:22:54 +01:00
Ralf Vogler
a9e20928ee pg: fix util.js -> src/util.js
64676795d1 (r135743209)
2023-12-26 18:56:36 +01:00
Ralf Vogler
b68021b8e8 docker: firefox: overwrite user.js instead of appending on every run, #261 2023-12-26 18:01:32 +01:00
Ralf Vogler
baeaaa64f8 docker: firefox: privacy.resistFingerprinting fixes #261
https://github.com/vogler/free-games-claimer/issues/261#issuecomment-1868385830

Docker container will ask/fill email, challenge captcha, ask/fill password, challenge captcha again.
2023-12-26 17:52:18 +01:00
Ralf Vogler
105f6f414a eg: only ask for password after email submit -> notice captcha before 2023-12-26 17:49:51 +01:00
Ralf Vogler
64676795d1 mv {config,migrate,util,version}.js src/ 2023-12-26 17:09:14 +01:00
Ralf Vogler
6b9420804b test to show enquirer's sigint issue 2023-12-26 16:58:14 +01:00
Ralf Vogler
5a4f07ce70 mv notify-test.js test/notify.js 2023-12-26 16:57:47 +01:00
Ralf Vogler
09a364c67d extract chrome-specific comments/args into util.js/launchChromium 2023-12-26 16:48:25 +01:00
Ralf Vogler
4518914bfd
Update FUNDING.yml 2023-12-24 17:04:37 +01:00
Ralf Vogler
377ee736cb
Update FUNDING.yml 2023-12-24 16:29:05 +01:00
Ralf Vogler
2dec0080d9
Update FUNDING.yml 2023-12-24 16:23:01 +01:00
Ralf Vogler
a2af1ccd09
Update FUNDING.yml 2023-12-24 16:22:35 +01:00
Ralf Vogler
e66d10574d
Update FUNDING.yml 2023-12-24 16:21:15 +01:00
Ralf Vogler
68b66444ca eg: set cookie to void 'please provide your date of birth', closes #275 2023-12-24 15:52:59 +01:00
Ralf Vogler
2a19100601
Update FUNDING.yml 2023-12-24 15:39:38 +01:00
Ralf Vogler
5343535429
Create FUNDING.yml 2023-12-24 01:10:17 +01:00
Ralf Vogler
ace4ef8303 pg: DLC: ignore timeout if there is no code
https://github.com/vogler/free-games-claimer/issues/208#issuecomment-1868317276
2023-12-23 18:16:30 +01:00
Ralf Vogler
e078a1540a
Merge pull request #274 from Primajin/#208-prime-gaming
Use `.tw-button` css class over `button` html element selector
2023-12-23 15:23:33 +01:00
Jannis Hell
093ed813c6
Update prime-gaming.js
account for more than one account link button
2023-12-23 11:55:27 +01:00
Jannis Hell
e06ad3a27f
use tw-button css class over button html element selector 2023-12-22 16:40:02 +01:00
Ralf Vogler
9f97805517 debug navigator.{userAgent, platform} 2023-12-21 16:38:12 +01:00
Ralf Vogler
30957d63d0 DEBUG_NETWORK=1 to log network requests and responses instead of DEBUG && RECORD 2023-12-21 16:16:48 +01:00
Ralf Vogler
19e9bb9cfc eg: changed: move password fill and fix captcha locators, #260 2023-12-21 01:55:54 +01:00
Ralf Vogler
bdc305aa83 reorder package.json 2023-12-21 01:53:47 +01:00
Ralf Vogler
e18de7e640
Merge pull request #249 from Primajin/patch-1
prime-gaming: 'Claim now' -> 'Claim' for external/DLC
2023-12-21 01:19:20 +01:00
Jannis Hell
f4270e176f Update prime-gaming.js
The button was renamed to "Claim" instead of "Claim now" this causes / fixes #208
2023-12-20 20:55:14 +01:00
Ralf Vogler
8bf36a2158 specify engines.node >=15 for ||=, #264 2023-12-19 12:27:55 +01:00
Ralf Vogler
3c15252d8b pg: use chalk to color game codes blue, closes #250 2023-12-19 12:06:15 +01:00
Ralf Vogler
76597f4315 eg: include link to game in captcha notification, closes #259
TODO use purchaseURL from https://github.com/vogler/free-games-claimer/pull/130
2023-12-19 11:52:35 +01:00
Ralf Vogler
9c89bf06a4 npm run lint = npx eslint . 2023-12-19 11:34:00 +01:00
Ralf Vogler
36fe60a2b1 vscode update changed config 2023-12-19 11:33:38 +01:00
Ralf Vogler
0f549285b9
Merge pull request #253 from vogler/dependabot/github_actions/actions/setup-node-4
build(deps): bump actions/setup-node from 3 to 4
2023-12-19 11:30:59 +01:00
Ralf Vogler
544eff8a23 ncu -u: playwright-firefox 1.40.0 -> 1.40.1, eslint 2023-12-19 11:25:21 +01:00
Ralf Vogler
5919d37efa ncu -u: playwright-firefox 1.39.0 -> 1.40.0, eslint 2023-11-23 16:16:26 +01:00
dependabot[bot]
324f6bffe7
build(deps): bump actions/setup-node from 3 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 06:01:59 +00:00
Ralf Vogler
4231b7dd28 gog: don't wait for screenshot to not be loading, closes #240 2023-11-09 02:43:43 +01:00
Ralf Vogler
3ddf1720bb notify: use execFile with arg array instead of exec to avoid shell-escape, fixes #239
Also proper fix for https://github.com/vogler/free-games-claimer/pull/167

https://www.npmjs.com/package/shell-escape
https://stackoverflow.com/questions/1779858/how-do-i-escape-a-string-for-a-shell-command-in-node
https://nodejs.org/api/child_process.html#child_processexecfilefile-args-options-callback
2023-11-09 02:34:17 +01:00
Ralf Vogler
a7cc68b6db ue: login: fix #248, similar to #236
Commits for #236:
d73a523fe7
a374d48345
2023-11-09 01:41:01 +01:00
Ralf Vogler
739fb0dbfe vscode workspace settings for formatOnSave with eslint --fix using flat config
https://eslint.style/guide/faq#vs-code
2023-11-08 02:30:21 +01:00
Ralf Vogler
110b2ea4c6 eslint: migrate to new flat config (.eslintrc.cjs -> eslint.config.js)
https://eslint.org/docs/latest/use/configure/configuration-files-new
https://eslint.org/docs/latest/use/configure/migration-guide
2023-11-08 02:28:34 +01:00
Ralf Vogler
c5f75e06c8 gha: sonar: disable shallow clone (irrelevant) to get rid of warning 2023-11-08 01:36:35 +01:00
Ralf Vogler
d4685ff370 gha: sonar: install dev deps for eslint plugins 2023-11-08 01:32:20 +01:00
Ralf Vogler
0832ae57f5 run eslint --fix . 2023-11-08 01:16:49 +01:00
Ralf Vogler
011eddf97a eslint: enable most stylistic rules
https://eslint.style/packages/js
2023-11-08 01:02:43 +01:00
Ralf Vogler
efe4faab3e
add sonarcloud badge for code smells 2023-11-07 18:24:32 +01:00
Ralf Vogler
e4e4f2eafa fix eslint errors 2023-11-07 18:10:22 +01:00
Ralf Vogler
6a34cb541d format sonar.yml 2023-11-07 18:03:54 +01:00
Ralf Vogler
5083ea408d ncu -u: eslint 8.50.0 -> 8.53.0 2023-11-07 17:58:36 +01:00
Ralf Vogler
2eaf6f0598 .eslintignore -> .gitignore, no-unused-vars: ignore args starting with _
https://eslint.org/docs/latest/rules/no-unused-vars#argsignorepattern
https://eslint.org/docs/latest/rules/no-undef
2023-11-07 17:57:10 +01:00
Ralf Vogler
dec0b519a5
Merge pull request #228 from 4n4n4s/ci
Sonarqube support with ESLint
2023-11-07 17:36:20 +01:00
Ralf Vogler
6113ea4b8d
Merge branch 'main' into ci 2023-11-07 17:32:00 +01:00
Ralf Vogler
798b130c92 fix vscode problem in jsconfig with module vs. moduleResolution
Option 'module' must be set to 'NodeNext' when option 'moduleResolution' is set to 'NodeNext'.ts
Specify what module code is generated.

See more: https://www.typescriptlang.org/tsconfig#module
2023-11-07 16:45:17 +01:00
Ralf Vogler
e5935faa13 sonarcloud fixes 2023-11-07 15:20:58 +01:00
Ralf Vogler
b5ef699f4f sort .dockerignore 2023-11-07 12:03:15 +01:00
Ralf Vogler
a6b9ec9609 sonarcloud fix reject(error)
https://sonarcloud.io/project/issues?cleanCodeAttributeCategories=CONSISTENT&resolved=false&id=vogler_free-games-claimer&open=AYupZi4O_aoWVkCdISRb&tab=code
2023-11-07 12:02:33 +01:00
Ralf Vogler
a62aa8c0c8 sonarcloud fix docker: Remove cache after installing packages
https://sonarcloud.io/project/issues?resolved=false&types=CODE_SMELL&id=vogler_free-games-claimer&open=AYupZi3__aoWVkCdISRI
https://sonarcloud.io/organizations/vogler/rules?open=docker%3AS6587&rule_key=docker%3AS6587&tab=how_to_fix
https://askubuntu.com/questions/3167/what-is-difference-between-the-options-autoclean-autoremove-and-clean
2023-11-07 11:53:29 +01:00
Ralf Vogler
e192365b48 can't have comment in yml list? 2023-11-07 11:39:08 +01:00
Ralf Vogler
92ce3d405d run docker workflow if its defition changed 2023-11-07 11:36:17 +01:00
Ralf Vogler
584130f5d1 edits for #229, build image for PRs from forks? 2023-11-07 11:31:14 +01:00
Ralf Vogler
5fda138d8d
Merge pull request #229 from 4n4n4s/docker
Allow forks to publish docker images
2023-11-07 11:16:24 +01:00
Ralf Vogler
11db31ea46 Merge branch 'xbox-gold'
https://github.com/vogler/free-games-claimer/pull/160
2023-11-06 19:40:18 +01:00
Ralf Vogler
0ab9935fb5 eg: catch timeout in case there are no free games available, #210 2023-11-06 19:22:43 +01:00
Ralf Vogler
28a1e42cc4 npm package*.json license MIT -> AGPL-3.0-only 2023-11-06 19:15:09 +01:00
Ralf Vogler
32cd0d8990
Create LICENSE - AGPL-3.0
https://choosealicense.com/licenses/agpl-3.0/
2023-11-06 19:12:16 +01:00
Ralf Vogler
b99a154267 Revert "workaround for recordVideo broken in Playwright 1.36"
This reverts commit 13b2917dd0.

Fine to do after upgrade to 1.39 in 75f7d77445.
which included https://github.com/microsoft/playwright/issues/27086
2023-11-02 16:26:45 +01:00
Ralf Vogler
75f7d77445 upgrade playwright-firefox 1.38.1 -> 1.39.0 2023-11-02 16:23:46 +01:00
Ralf Vogler
bf870919a6 upgrade to lowdb 6.1.1 and use JSONPreset
See example in https://github.com/typicode/lowdb/releases/tag/v6.1.0
2023-11-02 16:18:48 +01:00
Ralf Vogler
280ab70975
README.md: recommend to run without docker until #183 is fixed 2023-11-02 16:01:08 +01:00
4n4n4s
1dbe2f1457 Allow forks to create builds and fix failing build 2023-10-27 11:37:32 +00:00
Ralf Vogler
4137bb5569 DEBUG=1 as alternative to PWDEBUG=1 (also shows Playwright debugger) 2023-10-26 15:01:04 +02:00
Ralf Vogler
a374d48345 eg: fix login (email/password split), closes #236 2023-10-25 19:48:58 +02:00
Ralf Vogler
d73a523fe7 eg: fix sign in, user displayname, #236 2023-10-25 19:44:52 +02:00
Ralf Vogler
04787909c7 eg: waitFor order confirmation to be attached instead of visible, #233 2023-10-13 00:03:11 +02:00
Ralf Vogler
ad2301c3fd pg: eg: fix detecting successful claim 2023-10-10 22:42:38 +02:00
Ralf Vogler
a8ab989a7f pg: external: check for 'Link account' besides 'Link game account' 2023-10-10 22:12:59 +02:00
Ralf Vogler
9fc68b881f pg: legacygames: don't wait for response, just text 2023-10-10 22:01:23 +02:00
4n4n4s
0040d9d96c Sonarqube support with ESLint 2023-09-26 19:12:09 +00:00
Ralf Vogler
d318a57be1 ncu -u: playwright-firefox ^1.38.0 → ^1.38.1 2023-09-25 09:35:30 +02:00
Ralf Vogler
9ebd15f204 fix log text if $VNC_PASSWORD is set, fixes #223 2023-09-23 11:02:08 +02:00
Ralf Vogler
41f1f95509 PW: replace deprecated type() with pressSequentially() for OTPs 2023-09-18 14:02:15 +02:00
Ralf Vogler
840d35c2d2 ncu -u: playwright-firefox ^1.37.1 → ^1.38.0 2023-09-18 13:54:40 +02:00
Ralf Vogler
13b2917dd0 workaround for recordVideo broken in Playwright 1.36
Recording videos with relative path as in docs was broken for Firefox.
Issue: https://github.com/microsoft/playwright/issues/27086
Can be reverted after PR is available in release: https://github.com/microsoft/playwright/pull/27099
2023-09-18 13:03:04 +02:00
Ralf Vogler
b748460b73 disable Playwright's handleSIGINT and close context ourselves to save recordings also on SIGINT 2023-09-18 12:58:20 +02:00
Ralf Vogler
1fbabbc0f2 log path of recorded video 2023-09-18 12:49:23 +02:00
dependabot[bot]
2a4da3f49c
build(deps): bump docker/build-push-action from 4 to 5 (#212)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 09:51:53 +02:00
dependabot[bot]
0ba693eeb6
build(deps): bump docker/login-action from 2 to 3 (#214)
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 09:51:23 +02:00
dependabot[bot]
6cdef56c09
build(deps): bump docker/setup-buildx-action from 2 to 3 (#213)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 09:50:04 +02:00
dependabot[bot]
30db49d9b9
build(deps): bump docker/setup-qemu-action from 2 to 3 (#215)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 09:49:00 +02:00
dependabot[bot]
44d712e333
build(deps): bump actions/checkout from 3 to 4 (#207)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 09:41:42 +02:00
Ralf Vogler
d51f7310d9 gog: fix #200, uncheck 'Marketing communications through Trusted Partners' 2023-09-07 13:56:37 +02:00
Ralf Vogler
e30216c9b8 eg: default resolution 1280x1280 -> 1920x1080, #183 2023-09-07 13:30:49 +02:00
Ralf Vogler
c79f4cc40f docker: migrate to new nodesource repo; node 19 -> 20
old node setup script had a warning to migrate with 60s pause during docker build

https://github.com/nodesource/distributions/wiki/How-to-migrate-to-the-new-repository
2023-09-07 13:27:18 +02:00
Ralf Vogler
97882c76c3 eg: debug: window.screen 2023-09-07 13:25:14 +02:00
Ralf Vogler
b449ee4d7f pg: redeem: legacygames success/error 2023-09-05 16:12:20 +02:00
Ralf Vogler
d4ebdd091a pg: log current game in claim loop, not data loop 2023-09-05 15:59:17 +02:00
Ralf Vogler
d8e2093a0d pg: INTERACTIVE=1 to confirm each claim or skip it 2023-09-05 15:56:24 +02:00
Ralf Vogler
38975e811b eg: error: Incorrect repsonse for captcha! 2023-08-31 23:08:29 +02:00
Ralf Vogler
59004cd4bb README: no docker updates: git pull; npm install 2023-08-31 23:04:42 +02:00
Ralf Vogler
5ae4ce5666 eg: option TIME=1 to log duration of steps, #183 2023-08-31 18:59:10 +02:00
Ralf Vogler
93d082e700 eg: comment userAgent firefox (docker) 2023-08-31 17:05:12 +02:00
Ralf Vogler
621032e459 eg: notify about captcha before claim, closes #191 2023-08-30 19:21:24 +02:00
KevinMatt
6bb1dca934
epic-games: captcha on login: await notify (#195)
Adding async handle in captcha notify
2023-08-28 09:16:30 +02:00
Ralf Vogler
9261be690c '/.dockerenv' did not exist in container... 2023-08-28 01:25:09 +02:00
Ralf Vogler
6560afa2b5 version.js to check if running the latest version 2023-08-28 01:17:57 +02:00
Ralf Vogler
d4d7ee3281 fix indent 2023-08-28 00:47:52 +02:00
Ralf Vogler
c211472d0c docker: try GitHub Actions cache
https://docs.docker.com/build/ci/github-actions/cache/#github-cache
2023-08-27 23:59:30 +02:00
Ralf Vogler
f528fbfd30 docker: more human-readable format for date $NOW 2023-08-27 23:41:19 +02:00
Ralf Vogler
d7949fb9dd can't use secrets in if of workflow step? 2023-08-27 23:39:52 +02:00
Ralf Vogler
8a010dbcc7 docker: pass in build-args, add $NOW 2023-08-27 23:32:56 +02:00
Ralf Vogler
2eb17a1419 docker: show version via $COMMIT_SHA (and $BRANCH if not "main") 2023-08-27 18:33:27 +02:00
Ralf Vogler
24a9f23211
Merge pull request #193 from 4n4n4s/main-1
Update Readme for restart with sleep in docker compose and Apprise with Telegram
2023-08-27 15:44:38 +02:00
Ralf Vogler
682c651224 docker: use OCI labels
https://github.com/opencontainers/image-spec/blob/main/annotations.md
https://docs.docker.com/engine/reference/builder/#label

Before:
```console
$ docker image inspect --format='{{json .Config.Labels}}' ghcr.io/vogler/free-games-claimer | jq
{
  "org.opencontainers.image.ref.name": "ubuntu",
  "org.opencontainers.image.version": "22.04"
}
```

After:
```console
$ docker image inspect --format='{{json .Config.Labels}}' ghcr.io/vogler/free-games-claimer | jq
{
  "org.opencontainers.image.base.name": "ubuntu:jammy",
  "org.opencontainers.image.description": "Automatically claims free games on the Epic Games Store, Amazon Prime Gaming and GOG",
  "org.opencontainers.image.name": "free-games-claimer",
  "org.opencontainers.image.ref.name": "",
  "org.opencontainers.image.revision": "",
  "org.opencontainers.image.source": "https://github.com/vogler/free-games-claimer",
  "org.opencontainers.image.title": "free-games-claimer",
  "org.opencontainers.image.url": "https://github.com/vogler/free-games-claimer",
  "org.opencontainers.image.version": "latest"
}
```
2023-08-26 10:47:18 +02:00
Ralf Vogler
c09d20766c dependabot daily -> weekly, may want to customize commit-message 2023-08-25 21:50:17 +02:00
Ralf Vogler
70b5182bed same as before, just ignore .github/** and then include docker.yml 2023-08-25 21:43:57 +02:00
Ralf Vogler
a7d045f0e2 ignore dependabot.yml in workflows/docker.yml 2023-08-25 21:40:28 +02:00
Ralf Vogler
56d8c2d188
Merge pull request #194 from 4n4n4s/main
Create dependabot.yml
2023-08-25 21:32:06 +02:00
4n4n4s
a3adaae99f Fixed README.md 2023-08-25 16:30:04 +00:00
4n4n4s
66c12b8ea2
Create dependabot.yml 2023-08-25 18:06:31 +02:00
4n4n4s
da23bc2f92
Update README.md
Update documentation
2023-08-25 17:42:52 +02:00
Ralf Vogler
6a7cca31a4 update dependencies via ncu -u, lowdb: pass defaultData
dotenv                          ^16.0.3  →  ^16.3.1
  enquirer                         ^2.3.6  →   ^2.4.1
  lowdb                            ^5.1.0  →   ^6.0.1
  playwright-firefox              ^1.31.0  →  ^1.37.1
  puppeteer-extra-plugin-stealth  ^2.11.1  →  ^2.11.2

https://github.com/typicode/lowdb/releases/tag/v6.0.0
> Require defaultData parameter for Low and LowSync constructors to improve TypeScript experience
2023-08-24 13:14:39 +02:00
Ralf Vogler
3d1168f653 pg: external: split loop for URLs to avoid issue with changed elementHandles (Page.adoptNode) 2023-08-23 00:51:19 +02:00
Ralf Vogler
40bcf1c8a2 pg: status 'claimed' (not just 'claimed and redeemed') for external stores 2023-08-23 00:09:27 +02:00
Ralf Vogler
7551a6ca96 pg: fixup e2e28301e6? 2023-08-19 17:04:33 +02:00
Ralf Vogler
e2e28301e6 pg: fix #185? 2023-08-19 16:35:58 +02:00
Ralf Vogler
e794a3306f pg: dlc: grouped list of dlcs per unlinked store, #180 2023-08-17 17:02:47 +02:00
Ralf Vogler
5c8f8fc3fd pg: dlc: log url of dlc if account not linked, #180 2023-08-17 16:44:59 +02:00
Ralf Vogler
86832e7fe9 pg: dlc: log url of dlc if account not linked, #180 2023-08-17 16:43:47 +02:00
Ralf Vogler
d0761dea26 pg: dlc: account linking: mention store, close #180 2023-08-17 16:37:05 +02:00
Ralf Vogler
490c4c0788 don't ignore .github, only issue templates 2023-08-17 16:31:43 +02:00
Ralf Vogler
13def8ba18 build on every push or PR to main 2023-08-17 16:30:12 +02:00
Ralf Vogler
08fc59520c update docker/build-push-action v3 -> v4 2023-08-17 16:29:08 +02:00
Ralf Vogler
f89b4a6a36 ue: mention new assets to claim every first Tuesday of a month 2023-08-10 14:37:00 +02:00
Ralf Vogler
b5ead8ea21 SCREENSHOTS_DIR=0 to disable screenshots, fixes #172 2023-07-20 16:10:50 +02:00
Ralf Vogler
22f673282b pg: fix a90f90afc0 2023-07-13 11:30:48 +02:00
Ralf Vogler
a90f90afc0 pg: PG_REDEEM: only pause if DEBUG=1
Fixes https://github.com/vogler/free-games-claimer/issues/5#issuecomment-1527063819
2023-07-13 11:28:18 +02:00
Ralf Vogler
2e8a731573 pg: update selectors for internal/external games, fixes #164 2023-07-13 10:56:12 +02:00
Ralf Vogler
04bd56cd77 pg: legacy games: status = claimed and redeemed 2023-07-06 16:48:07 +02:00
Omair
8c6ff57054 xbox: update readme and dockerfile with xbox info/scripts 2023-06-26 15:49:27 -04:00
Omair
a7dcfe72cc xbox: add implementation for xbox games with gold 2023-06-26 15:48:58 -04:00
Omair
acc337a4f9 xbox: add config values 2023-06-26 15:48:21 -04:00
Ralf Vogler
f0e15b5c7c pg: fix #158 - page for gog (and ?) changed (button, store, code)
The claim page for Legacy Games is still unchanged and works with the
old code.
2023-06-23 01:24:06 +02:00
Ralf Vogler
85513031f2 pg: external: fix lost elementHandle error due to navigation
elementHandle.$: Protocol error (Page.adoptNode)
2023-06-23 01:21:42 +02:00
Ralf Vogler
0b9d5d0b63 pg: comments 2023-06-22 17:16:35 +02:00
Ralf Vogler
7e4770b846 ue: notify on 'Price is not 0!' + ask to report, #44 2023-06-02 00:52:11 +02:00
Ralf Vogler
d3d22b1582 ue: wait 2s before checking cart, #44 2023-06-02 00:47:48 +02:00
Ralf Vogler
2ceccdae6b
ue: add as experimental to README.md, #44 2023-06-02 00:30:31 +02:00
Ralf Vogler
f906bef2a8 ue: fix 'Sign In'/user detection, #44 2023-06-02 00:14:21 +02:00
Ralf Vogler
00e213cf50 ue: support RECORD=1 2023-06-02 00:11:10 +02:00
Ralf Vogler
feadfc5acf pg: redeem: gog: status = 'claimed and redeemed' 2023-05-18 18:02:11 +02:00
Ralf Vogler
7fb872608d pg: dlc: try/catch for each dlc, #55 2023-05-18 17:32:02 +02:00
Ralf Vogler
0d074d0397 pg: dlc: scroll to end of page until all are loaded, #55 2023-05-18 16:03:24 +02:00
Ralf Vogler
368229be09 pg: screenshot prep only if needed 2023-05-17 11:35:10 +02:00
Ralf Vogler
8ee63c7a35 pg: support RECORD=1 2023-05-17 11:29:45 +02:00
Ralf Vogler
819e4cc57e pg: PG_REDEEM tested legacy games successfully, #5 2023-05-17 11:28:31 +02:00
Ralf Vogler
e4ebae3744 pg: PG_REDEEM, fixes for gog, #5 2023-05-17 11:15:07 +02:00
Ralf Vogler
240e64ae4c pg: also use elementHandles() for external 2023-05-17 10:20:17 +02:00
Ralf Vogler
98e4145708 Merge branch 'main' into refactor/prime-gaming 2023-05-17 00:59:44 +02:00
Ralf Vogler
60acf747d0 pg: scroll to bottom to load all games
There may be so many unclaimed games that not all of them are loaded
initially.
Also relevant to show the correct number of collected games.
2023-05-17 00:55:58 +02:00
Ralf Vogler
c4eb1e03ce pg: need elementHandles() instead of all() for internal since it changes 2023-05-17 00:41:37 +02:00
Ralf Vogler
590b01aba2 pg: refactor: use more locators & all() instead of $ 2023-05-17 00:11:35 +02:00
Ralf Vogler
290fe289d4 pg: fix #142: PG_CLAIMDLC locator 'a' resolved to 2 elements 2023-05-16 13:31:53 +02:00
Ralf Vogler
491ee899a5 pg: #126 change selector to handle potential button text 'Claim' (instead of 'Claim game') for internal games 2023-05-11 19:00:43 +02:00
Ralf Vogler
af935d4889 gog: no quotes around user, as for {eg, pg} 2023-05-11 18:50:54 +02:00
Ralf Vogler
6b13287b61 eg: click 'Yes, buy now' if 'This edition contains something you already have. Still interested?'
Happened for add-on https://store.epicgames.com/en-US/p/the-sims-4--the-daring-lifestyle-bundle
2023-05-11 18:38:44 +02:00
Ralf Vogler
8f174c4bf0 eg: RECORD=1 to recordVideo & recordHar; fixed: recordVideo can't be false
Strangely `recordHar` can be false instead of undefined, but made it symmetric.
2023-05-08 17:29:14 +02:00
Ralf Vogler
7fc0fbc69c Revert "eg: RECORD=1 to recordVideo & recordHar"
This reverts commit 066a99c77c.
2023-05-05 13:36:51 +02:00
Ralf Vogler
066a99c77c eg: RECORD=1 to recordVideo & recordHar 2023-05-05 09:54:21 +02:00
Ralf Vogler
ce51c269f8 eg: only notify for status 'claimed' or 'failed'; DRYRUN -> 'skipped' 2023-05-05 09:30:37 +02:00
Ralf Vogler
8fdf930415
Merge pull request #132 from vogler/unrealengine
Claim free asset packs from unrealengine.com (epic-games)
2023-04-29 11:05:03 +02:00
Ralf Vogler
631197371f ue: claimed successfully; set status, detect owned, better log 2023-04-29 11:01:16 +02:00
Ralf Vogler
5809b0963a ue: unrealengine: add assets to cart & checkout, rest same as for epic-games, #44 2023-04-28 00:59:27 +02:00
Ralf Vogler
5214bea488 cp epic-games.js unrealengine.js 2023-04-28 00:26:09 +02:00
Ralf Vogler
bb51fd8065 use local time instead of UTC, migrate.js, closes #131
Run `node migrate.js localtime data/*.json` to convert
existing `time` entries from UTC to your local timezone.
2023-04-28 00:11:29 +02:00
Ralf Vogler
bc8a89f365 log 'DRYRUN=1 -> Skip order!' 2023-04-27 19:14:48 +02:00
Ralf Vogler
a235ce7915
add star-history 2023-04-25 09:48:20 +02:00
Ralf Vogler
2e0cc4d377
Merge pull request #128 from l-skyalker/fix-epic-claim
fix: update epic claim confirmation text
2023-04-21 14:06:34 +02:00
l-skywalker
670262c118 fix: update epic confirm text 2023-04-20 20:12:49 +02:00
Ralf Vogler
351bf00c7b pg: redeem gog: fix for undefined reason in response, #5 2023-04-14 11:59:33 +02:00
Ralf Vogler
b2d69b4d26 pg: redeem legacy games, #5 2023-04-14 11:53:22 +02:00
Ralf Vogler
28146c7d69 eg: 2s delay after click Continue, #120 2023-04-07 10:23:19 +02:00
Ralf Vogler
e22bb22a53 eg: try click delay for #120 2023-04-07 09:51:18 +02:00
Ralf Vogler
6cbd9fe5e3
link RPi 64-bit OS issue 2023-03-30 17:59:34 +02:00
Ralf Vogler
04f503fea7 eg: mention required base game for free Add-Ons, #106 2023-03-30 16:53:43 +02:00
Ralf Vogler
db5a98eb88 gog: set GOG_NEWSLETTER=1 to not unsubscribe newsletter, closes #109 2023-03-30 14:17:26 +02:00
Ralf Vogler
9285af2544 eg: notify_games filter status 'requires base game', fixes #112
fix 352ee753c9
2023-03-30 14:06:57 +02:00
Ralf Vogler
352ee753c9 eg: skip if 'requires base game', fix #106 2023-03-23 19:57:10 +01:00
Ralf Vogler
b7d5d43078 eg: wait for "Place Order" button to not be loading, fixes #84 2023-03-17 20:59:55 +01:00
Ralf Vogler
b75f538d8a eg: add EG_PARENTALPIN to enter Parental Controls PIN, #98 2023-03-17 11:19:38 +01:00
Ralf Vogler
9435ff6edb fix #97: NOTIFY_TITLE in quotes, otherwise fails on spaces 2023-03-17 10:19:34 +01:00
Ralf Vogler
4f1ca53d1b eg: fix waiting for captcha for non-EU accounts
https://github.com/vogler/free-games-claimer/issues/84#issuecomment-1462993079
2023-03-16 15:56:00 +01:00
Ralf Vogler
9df80fa6d8 eg: wait 2s before 'Place Order', fix #84 for everyone? 2023-03-10 09:16:54 +01:00
Ralf Vogler
0cfb9d29aa eg: Accept End User License Agreement (only needed once per account) 2023-03-09 16:21:19 +01:00
Ralf Vogler
c48c80717a pg: only screenshot if claimed, incr. height to fit all games, fixes #82 2023-03-08 17:46:03 +01:00
Ralf Vogler
7520bf1976 gog: screenshot: wait for not is-loading 2023-03-08 16:54:10 +01:00
Ralf Vogler
74bd2f538a gog: newsletter is not subscribed again if game already existed 2023-03-08 16:41:31 +01:00
Ralf Vogler
9ab44bd7aa gog: fix indent 2023-03-08 16:41:06 +01:00
Ralf Vogler
07ce17f8e3 link issues for experimental options PG_REDEEM, PG_CLAIMDLC 2023-03-06 22:35:37 +01:00
Ralf Vogler
b51547a376 pg: experimental PG_CLAIMDLC, WIP #55 2023-03-06 22:32:08 +01:00
Ralf Vogler
a5f42a9609 sample command: and environment: in docker-compose.yml, #85 2023-03-06 22:13:58 +01:00
Ralf Vogler
cbdea1b5d0 add username to notification of claimed games, closes #88 2023-03-06 21:15:42 +01:00
Ralf Vogler
e45c3a5dca eg: no captcha -> no special timeout for solving one 2023-03-06 21:08:57 +01:00
Ralf Vogler
a8fa80cd05 eg: fix #84 like #75 with click delay >0ms 2023-03-06 21:08:24 +01:00
Ralf Vogler
944cca6715 eg: add click delay, fixes #75
Playwright triggered the click such that the purchase frame opened, but
did not resolve the promise. Had to move mouse into the browser for it
to continue. Adding a click delay of 1ms also worked (default is no
delay between mouse down and up). Went for 11ms. A typical click is
probably 100-200ms.
2023-02-24 20:56:25 +01:00
Ralf Vogler
0d2ff0c8a9 pg: add status to db for external games: claimed, claimed and redeemed, failed 2023-02-24 19:57:57 +01:00
Ralf Vogler
862fd20d5b mention open issue with Enquirer: must cancel prompt with Escape instead of Ctrl-C 2023-02-24 13:21:55 +01:00
Ralf Vogler
cc39b4b3d1 pg: skip if user has no Prime membership, closes #76? 2023-02-24 12:31:42 +01:00
Ralf Vogler
dc28c30e7b pg: make DRYRUN=1 terminate for external games, TODO only shows first game 2023-02-24 12:28:56 +01:00
Ralf Vogler
73a7cffd47 await notify before process.exit, #69 2023-02-24 00:08:35 +01:00
Ralf Vogler
114631da4d add NOTIFY_TITLE - Optional title for notifications, e.g. Pushover, #69 2023-02-23 23:35:47 +01:00
Ralf Vogler
e73d3d47d7 pg: PG_REDEEM == '1' like for other boolean options 2023-02-23 23:10:35 +01:00
Ralf Vogler
792d2859b3
mention how to run several scripts in docker via bash -c, #73 2023-02-23 19:33:08 +01:00
Ralf Vogler
08b9df7cc4 pg: microsoft games: click next, #5 2023-02-23 12:51:36 +01:00
Ralf Vogler
fc3f0a6333 pg: PG_REDEEM for external stores, post your response in #5
Known responses (missing unused key):
- GOG: `Invalid or no captcha`, `code_used`, `code_not_found`
- microsoft games: `NotFound`
2023-02-23 12:38:26 +01:00
Ralf Vogler
38c5402df0 exitCode should be 130 instead of 1 on SIGINT
https://unix.stackexchange.com/questions/386836/why-is-doing-an-exit-130-is-not-the-same-as-dying-of-sigint
2023-02-22 10:26:32 +01:00
Ralf Vogler
21ed2794f4 change default TIMEOUT from 20s to 60s, #62 2023-02-22 01:10:34 +01:00
Ralf Vogler
a3892eaafb update description & version in package.json 2023-02-22 00:59:22 +01:00
Ralf Vogler
9df4836221 gitignore *.env for safety :) 2023-02-22 00:52:25 +01:00
Ralf Vogler
18c27ba881 use playwright-firefox such that npm install does not download other browsers 2023-02-22 00:49:58 +01:00
Ralf Vogler
97f6735833 ncu -u updated playwright 2023-02-22 00:31:11 +01:00
Ralf Vogler
518008584f gog: actually wait for login instead of reentering the loop 2023-02-22 00:26:48 +01:00
Ralf Vogler
b0f662479d eg: exit like others if headless and no login data given 2023-02-22 00:16:19 +01:00
Ralf Vogler
66694d65e5 SIGINT handler to not notify about error on Ctrl-C 2023-02-22 00:15:03 +01:00
Ralf Vogler
c1cd2db721
Merge pull request #71 from vogler/login_timeout
add `LOGIN_TIMEOUT` with default 180s
2023-02-21 23:33:15 +01:00
Ralf Vogler
efeccf9493 use enquirer instead of prompts, use plugin for cancel after timeout 2023-02-21 23:25:18 +01:00
Ralf Vogler
8c2ac3b6d0 add LOGIN_TIMEOUT (180s) for PW, but prompts still wait forever 2023-02-21 21:21:55 +01:00
Ralf Vogler
704c4b01e1 set process.exitCode = 1 on error; don't want to rethrow 2023-02-21 21:00:26 +01:00
Ralf Vogler
1b319cacf7 docker: manually install-deps firefox: 1.12GB -> 932MB 2023-02-20 18:37:45 +01:00
Ralf Vogler
3158c5515f docker: no need for --no-install-suggests 2023-02-20 18:35:39 +01:00
Ralf Vogler
5f6d9ca830 docker: --no-install-recommends for nodejs 2023-02-20 18:32:13 +01:00
Ralf Vogler
992fe4fc8b notify-test.js 2023-02-20 15:31:30 +01:00
Ralf Vogler
957ba6d7ca
better explain 'How to set options' 2023-02-17 14:48:48 +01:00
Ralf Vogler
95598887cc readme md table spaces 2023-02-16 16:53:21 +01:00
Ralf Vogler
e2b07dc1e6 BROWSER_DIR for multiple profiles or testing, SCREENSHOTS_DIR, closes #12 2023-02-16 16:25:46 +01:00
Ralf Vogler
a90062b631 gog: Claim (.*) and don't miss -> Claim (.*), fixes #60 2023-02-15 19:33:44 +01:00
Ralf Vogler
ec2d31ed79 remove unused NopeCHA extension 2023-02-15 10:06:52 +01:00
Ralf Vogler
c65f153090 remove not needed await 2023-02-15 10:06:52 +01:00
Ralf Vogler
c058bafcf3 remove migrateDb
comment: remove this after some time since it will run fine without and people can still use this commit to adjust their data/epic-games.json and data/prime-gaming.json
2023-02-15 10:06:52 +01:00
Ralf Vogler
af34113eaf
mention pm2 for restarting 2023-02-14 18:45:23 +01:00
Ralf Vogler
acbfa9156e pg: ignore empty login error message, fixes #58 2023-02-14 10:29:21 +01:00
Ralf Vogler
f873d95a89
Update README.md 2023-02-10 17:55:55 +01:00
Ralf Vogler
3ada6bbc23
Update README.md 2023-02-09 19:49:51 +01:00
Ralf Vogler
d3e4c58c80 gog: only unsubscribe from newsletter if a game was claimed? 2023-02-09 16:53:42 +01:00
Ralf Vogler
11a28f0c73 ncu -u updated lowdb, playwright 2023-02-09 16:48:23 +01:00
Ralf Vogler
dff712d998 skip prompt for password if email is missing 2023-02-09 16:28:27 +01:00
Ralf Vogler
65a389c3d9 docker: don't pin playwright, split: system deps + firefox after npm install
`npx playwright install --with-deps firefox` fails since package.json references 1.29 but current version is 1.30:
`browserType.launchPersistentContext: Executable doesn't exist at /root/.cache/ms-playwright/firefox-1369/firefox/firefox`

Need to either pin the version there or install the browser after `npm install` which will make it take into account the package.json version.
See https://github.com/microsoft/playwright/issues/13188, https://github.com/microsoft/playwright/issues/12835

Image size:

1. 1.12GB - `npx playwright install --with-deps firefox` -> fails due to wrong browser version
2. 1.12GB - `npx playwright@1.29 install --with-deps firefox` -> pinning version works, but need to also update Dockerfile when updating package.json
3. 920MB - `npx playwright install-deps firefox` -> fails, system deps only, no browser
4. 1.12GB - 3. + `npx install firefox` after `npm install` -> works, no pinning needed, system deps stuff is cleaned up with the rest of the apt installs; breaks if system deps change...
2023-02-05 23:33:45 +01:00
Ralf Vogler
b9e9abe546 page.waitForNavigation -> page.waitForURL 2023-02-04 21:37:04 +01:00
Ralf Vogler
eb17a49628 extract prompt into util.js 2023-02-03 16:03:01 +01:00
Ralf Vogler
21a4e06209 gog: check for reCAPTCHA on login; better info for eg 2023-01-29 20:27:05 +01:00
Ralf Vogler
0393a3998b info about escaping prompts only if needed 2023-01-29 19:52:18 +01:00
Ralf Vogler
d4bf4a7af0 gog: wait for username after login, barrier before, fixes #53 2023-01-29 19:40:44 +01:00
Ralf Vogler
c5b0065a50 TypeError: String includes not contains, #53; only first line of error 2023-01-29 19:28:02 +01:00
Ralf Vogler
d7e5cc4a3a
Update README.md 2023-01-28 20:36:33 +01:00
Ralf Vogler
c9cefcb7e1
Update README.md 2023-01-28 20:35:10 +01:00
Ralf Vogler
9e0effa8c0
Update README.md 2023-01-28 20:33:21 +01:00
Ralf Vogler
0c2834eb5e
readme: add --pull=always to docker run, #51 2023-01-28 19:55:22 +01:00
Ralf Vogler
88b4dcfcac also run node gog by default, closes #52 2023-01-28 13:14:42 +01:00
Ralf Vogler
98dff72888 eg: notify: set status existed correctly, fixes #50 2023-01-28 10:26:27 +01:00
Ralf Vogler
01acfc2311 docker: pin playwright version since we install it before npm i
https://github.com/microsoft/playwright/issues/13188
2023-01-27 10:11:26 +01:00
Ralf Vogler
72a61458fc notify: escapeHtml for titles 2023-01-25 20:56:35 +01:00
Ralf Vogler
f8932af2a1
Screenshot sample Telegram notifications 2023-01-25 20:44:30 +01:00
Ralf Vogler
9355ff3e01 notify: forgot to join list, gets rid of commas 2023-01-25 19:35:18 +01:00
Ralf Vogler
0913ca3da7 pg: exit on login error 2023-01-25 19:26:34 +01:00
Ralf Vogler
af46be3a52
Merge pull request #49 from vogler/notifications/apprise
Notifications via apprise
2023-01-25 19:03:12 +01:00
Ralf Vogler
13e6f05cd0 pg: notify about games and login 2023-01-25 18:54:23 +01:00
Ralf Vogler
95b703efb1 gog: notify about games and login 2023-01-25 18:29:06 +01:00
Ralf Vogler
109423925e eg: notify about games and login 2023-01-25 17:50:26 +01:00
Ralf Vogler
2f0961b1b3 NOTIFY to set notification services 2023-01-25 17:50:26 +01:00
Ralf Vogler
e57c2c4408 docker: pip install apprise; 1.09GB -> 1.11GB 2023-01-25 17:50:26 +01:00
Ralf Vogler
cecc54082f
eg: title now in span instead of div 2023-01-25 17:45:50 +01:00
Ralf Vogler
3b1b900d77 pg: TODO check for wrong credentials 2023-01-25 16:38:56 +01:00
Ralf Vogler
0e7e7b08f3
add some logos 2023-01-25 15:32:37 +01:00
Ralf Vogler
fafd1ad6bf comment: no gog_otp 2023-01-23 14:59:59 +01:00
Ralf Vogler
46a5928d01
Update README.md 2023-01-23 14:48:29 +01:00
Ralf Vogler
7196a26070
Merge pull request #48 from vogler/eg-unavailable-region
eg: skip if "This product is currently unavailable in your region"
2023-01-23 14:46:36 +01:00
Ralf Vogler
8139c0a78f gog: save metadata, screenshot, closes #18 2023-01-23 14:45:29 +01:00
Ralf Vogler
c09da8eec6 eg: simpler check if game is unavailable in region 2023-01-19 12:29:20 +01:00
Ralf Vogler
4055ec44c7 fix for #46: "This product is currently unavailable in your region" 2023-01-19 01:26:27 +01:00
Ralf Vogler
351670f426 eg: TODO locator for 2FA text (email or app?) 2023-01-19 00:57:56 +01:00
Ralf Vogler
5babbb6bc1
Update README.md 2023-01-13 18:03:29 +01:00
Ralf Vogler
f2df47f13e
Update README.md 2023-01-13 18:02:35 +01:00
Ralf Vogler
78a14569c8
Update README.md 2023-01-13 17:51:32 +01:00
Ralf Vogler
7922044a60
Update README.md 2023-01-12 21:58:59 +01:00
Ralf Vogler
db5b2e7607
Update README.md 2023-01-12 21:40:17 +01:00
Ralf Vogler
6aacab592e
mention supported Raspberry Pi OS (64-bit) 2023-01-12 21:19:09 +01:00
Ralf Vogler
1aeb35ac86
typo 2023-01-12 16:27:38 +01:00
Ralf Vogler
ba1b6fba95 use $VNC_PASSWORD if set 2023-01-10 00:37:38 +01:00
Ralf Vogler
1a3d90f795 add otplib to generate OTP from key for eg, pg; gog only has mail 2023-01-10 00:06:25 +01:00
Ralf Vogler
9e0d4434fe docker-entrypoint: bash strict mode 2023-01-09 21:03:05 +01:00
Ralf Vogler
72d8550c86 docker/entrypoint.sh -> docker-entrypoint.sh 2023-01-09 20:37:13 +01:00
Ralf Vogler
84d4b9b7bc docker compose comments, only noVNC by default, use fgc volume as in README 2023-01-09 20:21:29 +01:00
Ralf Vogler
6a7594fa32 use dotenv for loading env vars from data/config.env 2023-01-09 10:56:18 +01:00
Ralf Vogler
2168c40aa5 centralize env vars in config.js 2023-01-09 10:47:19 +01:00
Ralf Vogler
af0c9a6f2d lowdb: fix for 'Can't import JSONFile' 2023-01-09 10:26:10 +01:00
Ralf Vogler
792b4b3915 options: store-specific logins, drop SCREEN_ 2023-01-07 13:55:31 +01:00
Ralf Vogler
577fd84a7c update README, explain options via env vars, TODOs 2023-01-07 13:47:53 +01:00
Ralf Vogler
09c3e57a8a use SHOW for pg and eg, headless by default for both, but show inside Docker 2023-01-07 13:47:08 +01:00
Ralf Vogler
cc183a6303 eg: set cookie accept time to 5 days ago instead of a static value 2023-01-07 12:40:25 +01:00
Ralf Vogler
cf9c31e544 eg: cookie comment 2023-01-07 12:32:55 +01:00
Ralf Vogler
6305ffd15d gog: login, claim (waiting), unsubscribe newsletter 2023-01-07 12:31:23 +01:00
Ralf Vogler
593677ca19 cp prime-gaming.js gog.js 2022-12-30 21:36:34 +01:00
Ralf Vogler
21a2bc01a4
mention how to change default command with docker run, #42 2022-12-30 15:37:49 +01:00
Ralf Vogler
67ccf032e5 eg: catch pending optional promise to avoid time out 2022-12-29 15:38:17 +01:00
Ralf Vogler
a4d39b6a6e pg: fix for #41 2022-12-29 15:23:22 +01:00
Ralf Vogler
446c1c9346 pg: comment external microsoft 2022-12-29 15:22:55 +01:00
Ralf Vogler
3c6d7f4300 pg: index by user and title, migrateDB 2022-12-29 15:17:08 +01:00
Ralf Vogler
db7cf88c1b eg: don't await MFA 2022-12-29 14:55:04 +01:00
Ralf Vogler
a10c61379b pg: prompts for login & MFA 2022-12-29 14:54:13 +01:00
Ralf Vogler
4ed5aa7774 pg: no longer track runs in json 2022-12-29 12:58:18 +01:00
Ralf Vogler
8edc4727b3 pg: changes from eg 2022-12-29 12:54:45 +01:00
Ralf Vogler
bea048cc72 update README chromium -> firefox 2022-12-29 12:05:43 +01:00
Ralf Vogler
09b867158c viewport dimensions via env 2022-12-29 01:23:17 +01:00
Ralf Vogler
1c38f730ab docker: run both epic-games and prime-gaming by default 2022-12-24 15:20:45 +01:00
Ralf Vogler
67e622e6ec docker: move firefox install up to other apt deps; --with-deps needed!
Without --with-deps we get and error starting the browser:

node:internal/process/esm_loader:108
    internalBinding('errors').triggerUncaughtException(
                              ^

browserType.launchPersistentContext:
╔══════════════════════════════════════════════════════╗
║ Host system is missing dependencies to run browsers. ║
║ Please install them with the following command:      ║
║                                                      ║
║     npx playwright install-deps                      ║
║                                                      ║
║ Alternatively, use apt:                              ║
║     apt-get install libgtk-3-0\                      ║
║         libasound2\                                  ║
║         libxcomposite1\                              ║
║         libpangocairo-1.0-0\                         ║
║         libpango-1.0-0\                              ║
║         libatk1.0-0\                                 ║
║         libcairo-gobject2\                           ║
║         libcairo2\                                   ║
║         libgdk-pixbuf-2.0-0\                         ║
║         libdbus-glib-1-2\                            ║
║         libxcursor1                                  ║
║                                                      ║
║ <3 Playwright Team                                   ║
╚══════════════════════════════════════════════════════╝
    at async file:///fgc/prime-gaming.js:25:17 {
  name: 'Error'
}
2022-12-24 15:02:28 +01:00
Ralf Vogler
8ce6c2fdc9 docker: hadolint: pipefail
Can't do the recommended --no-install-recommends for curl because
otherwise it has problems with certificates:

  curl: (77) error setting certificate file: /etc/ssl/certs/ca-certificates.crt
2022-12-24 14:46:40 +01:00
Ralf Vogler
3c4f79a1ed docker: upgrade Ubuntu 20.04 (focal) -> 22.04 (jammy), +60MB 2022-12-24 14:37:02 +01:00
Ralf Vogler
0ad324b756 docker: upgrade node 16 -> 19, +60MB 2022-12-23 00:28:49 +01:00
Ralf Vogler
c44d1641ea cleanup Dockerfile, merged apt runs -> saved ~90MB
Image size from 1.04GB to 952MB.
2022-12-21 09:05:30 +01:00
Ralf Vogler
34393eec76 pg: also use firefox
Dropping chromium reduced image size by ~500MB from 1.55GB to 1.04GB.
2022-12-21 01:25:32 +01:00
Ralf Vogler
31fb97345e docker: nicer entrypoint log 2022-12-19 16:50:01 +01:00
Ralf Vogler
a6f7c6d7e4
Readme: Xbox Live Games with Gold - planned 2022-12-17 09:17:31 +01:00
Ralf Vogler
ba545632f5 docker: also install firefox 2022-12-17 00:27:15 +01:00
Ralf Vogler
d616de2096 eg: use firefox instead of chromium, fixes #34 2022-12-17 00:23:22 +01:00
Ralf Vogler
f5c5bc4852 update playwright 2022-12-17 00:14:14 +01:00
Ralf Vogler
89ba21d2cb update lowdb 2022-12-17 00:12:59 +01:00
Ralf Vogler
aa53075ab5
Merge pull request #36 from gladiopeace/patch-1
fix copy multiple files
2022-11-30 02:52:00 +01:00
gladiopeace
b49862060c
fix copy multiple files
when copying multiple files you need to point to a directory rather a single file "."
2022-11-30 03:16:51 +02:00
Ralf Vogler
e1cd3117b6 fix #33 2022-11-12 13:43:27 +01:00
Ralf Vogler
0df7bf35ba pg: redeem code for microsoft games 2022-11-01 22:21:47 +01:00
Ralf Vogler
7ffdd61c44 pg: indent logging per game 2022-11-01 22:18:33 +01:00
Ralf Vogler
569e690d9e eg: log indent 'This game contains mature content' 2022-10-27 14:09:43 +02:00
Ralf Vogler
aae396fa80 eg: more info around login 2022-10-21 17:21:23 +02:00
Ralf Vogler
4e88964fcd lint with trunk 2022-10-21 16:24:52 +02:00
Ralf Vogler
4d6d2f47d9 update readme and docker scripts 2022-10-21 12:38:32 +02:00
Ralf Vogler
d58ca2037e metadata in package.json 2022-10-21 01:48:34 +02:00
Ralf Vogler
11b1a96f3b no longer build for arm/v7 2022-10-21 01:19:19 +02:00
Ralf Vogler
746d226cf6 eg: fix page.type: text: expected string, got number 2022-10-21 00:47:36 +02:00
Ralf Vogler
e931892b76 need Checkout Action after all? 2022-10-21 00:05:58 +02:00
Ralf Vogler
b82507e0af Revert "Buildx failed to read dockerfile"
This reverts commit 43f390ddb5.
2022-10-21 00:05:34 +02:00
Ralf Vogler
43f390ddb5 Buildx failed to read dockerfile
https://github.com/vogler/free-games-claimer/actions/runs/3292559451/jobs/5429519946
2022-10-21 00:03:39 +02:00
Ralf Vogler
ab84346945 GitHub Action to build & push Docker image 2022-10-20 22:01:27 +02:00
Ralf Vogler
2de5b8e4ab eg: info depending on NOVNC_PORT 2022-10-20 21:21:48 +02:00
Ralf Vogler
4e4410f5d6 clean up entrypoint.sh, quiet noisy x11vnc, echo status 2022-10-20 21:21:14 +02:00
Ralf Vogler
fbc33ffcaf same resolution for docker as for browser 2022-10-20 21:20:15 +02:00
Ralf Vogler
a789034c51 clean up Dockerfile and move down config such that it does not trigger rebuild 2022-10-20 20:45:44 +02:00
Ralf Vogler
9f0e50afb6 remove VNC_PASSWORD
Usually behind firewall and process is short-lived, except for when no longer logged in.
2022-10-20 20:09:12 +02:00
Ralf Vogler
1dbe239e48 rm -f /tmp/.X1-lock, fixes #31 2022-10-20 19:56:24 +02:00
Ralf Vogler
ef72506431 docker: respect package-lock.json, not just package.json 2022-10-20 19:55:42 +02:00
Ralf Vogler
e3eb26d527 docekr compose: shorter names for image and container 2022-10-20 19:55:10 +02:00
Ralf Vogler
f450d29bc3 eg: login from CLI, prompts for email, password, OTP 2022-10-20 18:07:34 +02:00
Ralf Vogler
ac0ecc0f7a eg: ignoring --enable-automation now shows info bar about unsupported --no-sandbox 2022-10-20 16:43:41 +02:00
Ralf Vogler
1532726b2a update deps via ncu -u 2022-10-20 16:34:03 +02:00
Ralf Vogler
2ab4b8b841 remove unused @playwright/test 2022-10-20 16:29:01 +02:00
Ralf Vogler
fb23408522 remove unused scripts: login, codegen
"login": "npx playwright open --save-storage=auth.json https://www.epicgames.com/login",
    "codegen": "npx playwright codegen --load-storage=auth.json https://www.epicgames.com/store/en-US/free-games",
2022-10-20 16:28:10 +02:00
Ralf Vogler
d42b0b38ad add docker-compose.yml, #31
works for first run of a container but then fails with:

free-games-claimer  | 16/10/2022 15:29:57 passing arg to libvncserver: -rfbport
free-games-claimer  | 16/10/2022 15:29:57 passing arg to libvncserver: 5900
free-games-claimer  | 16/10/2022 15:29:57 passing arg to libvncserver: -passwd
free-games-claimer  | 16/10/2022 15:29:57 x11vnc version: 0.9.16 lastmod: 2019-01-05  pid: 8
free-games-claimer  | 16/10/2022 15:29:57 XOpenDisplay(":1.0") failed.
free-games-claimer  | 16/10/2022 15:29:57 Trying again with XAUTHLOCALHOSTNAME=localhost ...
free-games-claimer  |
free-games-claimer  | 16/10/2022 15:29:57 ***************************************
free-games-claimer  | 16/10/2022 15:29:57 *** XOpenDisplay failed (:1.0)
free-games-claimer  |
free-games-claimer  | *** x11vnc was unable to open the X DISPLAY: ":1.0", it cannot continue.
free-games-claimer  | *** There may be "Xlib:" error messages above with details about the failure.
...
free-games-claimer  | browserType.launchPersistentContext:
free-games-claimer  | ╔════════════════════════════════════════════════════════════════════════════════════════════════╗
free-games-claimer  | ║ Looks like you launched a headed browser without having a XServer running.                     ║
free-games-claimer  | ║ Set either 'headless: true' or use 'xvfb-run <your-playwright-app>' before running Playwright. ║
free-games-claimer  | ║                                                                                                ║
free-games-claimer  | ║ <3 Playwright Team                                                                             ║
free-games-claimer  | ╚════════════════════════════════════════════════════════════════════════════════════════════════╝
2022-10-16 17:30:23 +02:00
Ralf Vogler
2012c7e2f2 eg: set cookie instead of click 'Accept All Cookies' 2022-10-06 23:33:39 +02:00
Ralf Vogler
094662aefc eg: write data/browser/cookies.json for easy access 2022-10-06 23:27:27 +02:00
Ralf Vogler
ed5f2d8486
Update README.md 2022-10-06 21:07:39 +02:00
Ralf Vogler
c2cb284121
add store icons to readme 2022-10-06 21:04:10 +02:00
Ralf Vogler
d67fb59355 eg: use NopeCHA extension to solve hcaptcha challenges
20 credits refill every 24h, but should not be needed
even if there are several games to claim every day.
2022-10-06 20:24:23 +02:00
Ralf Vogler
f28c465f8f eg: check for hcaptcha challenge 2022-10-06 20:18:27 +02:00
Ralf Vogler
32d432deb6 epic-games: Windows userAgent avoids 'Device not supported'-Continue? 2022-10-06 17:02:05 +02:00
Ralf Vogler
ac758d39e4 epic-games: migrateDb: rm .runs, .claimed[] -> .[user][game_id], closes #27
If you'd like to keep the .runs data: `cp -a data/epic-games.{json, v1.json}`
Objects also have insertion order for non-number strings, so there's not need for a list:
https://stackoverflow.com/questions/5525795/does-javascript-guarantee-object-property-order
2022-10-06 16:31:46 +02:00
Ralf Vogler
61af4e35f6 util.datetimeLocal 2022-10-06 14:20:50 +02:00
Ralf Vogler
548ba67e10 indent logging per game 2022-09-30 17:13:31 +02:00
Ralf Vogler
777b00b3fb log signed in user 2022-09-30 17:02:41 +02:00
31 changed files with 7006 additions and 752 deletions

View file

@ -1,6 +1,10 @@
node_modules
data
node_modules/
data/
*.env
.gitignore
.github/
**Dockerfile**
.dockerignore

View file

@ -0,0 +1,87 @@
name: build-and-push
on:
push:
branches:
- main
jobs:
lint:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
sonar:
needs: lint
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Sonar Scanner (npm)
run: npm install -g sonarqube-scanner
- name: SonarQube Scan
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }}
run: |
WORKDIR=${GITHUB_WORKSPACE:-$PWD}
HOST_URL=${SONAR_HOST_URL:?SONAR_HOST_URL secret not set}
PROJECT_KEY=${SONAR_PROJECT_KEY:-}
if [ -z "$PROJECT_KEY" ] && [ -f sonar-project.properties ]; then
PROJECT_KEY=$(grep -E '^sonar.projectKey=' sonar-project.properties | cut -d= -f2 | tr -d '\r')
fi
if [ -z "$PROJECT_KEY" ]; then
echo "SONAR_PROJECT_KEY secret not set and no sonar-project.properties entry found" >&2
exit 1
fi
echo "Sonar project key: $PROJECT_KEY"
echo "Listing workspace:"
ls -la
echo "Sample files:"
find . -maxdepth 2 -type f | head -n 20
echo "Running local sonar-scanner..."
sonar-scanner \
-Dsonar.host.url="$HOST_URL" \
-Dsonar.token="$SONAR_TOKEN" \
-Dsonar.projectKey="$PROJECT_KEY" \
-Dsonar.sources=. \
-Dsonar.scm.disabled=true \
-Dsonar.projectBaseDir="$WORKDIR"
docker:
needs: [lint, sonar]
runs-on: self-hosted
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Checkout
uses: actions/checkout@v4
- name: Login to registry
run: echo "${{ secrets.REG_TOKEN }}" | docker login "${{ secrets.REGISTRY }}" -u "${{ secrets.REG_USER }}" --password-stdin
- name: Build image
run: |
docker buildx build --load \
-t "${{ secrets.REGISTRY_IMAGE }}:latest" .
- name: Push image
run: |
docker push "${{ secrets.REGISTRY_IMAGE }}:latest"

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
node_modules/
data/
*.env

10
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
// https://eslint.style/guide/faq#vs-code
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modifications",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.experimental.useFlatConfig": true,
"eslint.codeActionsOnSave.rules": null,
}

6
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,6 @@
# Contribute
## Building and publishing docker images
Setup the secrets for DOCKERHUB_USERNAME and [DOCKERHUB_TOKEN](https://hub.docker.com/settings/security) in https://github.com/YOUR_USERNAME/free-games-claimer/settings/secrets/actions to be able to run the docker.yml workflows.
Check if under Workflow Permissions in https://github.com/YOUR_USERNAME/free-games-claimer/settings/actions the radio button is set to "Read and write permissions". In case that's not set the push to ghcr.io will fail.

View file

@ -1,75 +1,110 @@
# FROM mcr.microsoft.com/playwright:v1.20.0
FROM ubuntu:focal
# Partially from https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.focal
FROM ubuntu:jammy
# Configuration variables are at the end!
# https://github.com/hadolint/hadolint/wiki/DL4006
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ARG DEBIAN_FRONTEND=noninteractive
# Configure Xvfb via environment variables:
ENV SCREEN_WIDTH 1440
ENV SCREEN_HEIGHT 900
ENV SCREEN_DEPTH 24
# Configure VNC via environment variables:
ENV VNC_PASSWORD secret
ENV VNC_PORT 5900
ENV NOVNC_PORT 6080
ENV NOVNC_HOME /usr/share/novnc
EXPOSE 5900
EXPOSE 6080
# Playwright
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD true
# === INSTALL Node.js ===
# Taken from https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.focal
RUN apt-get update && \
# Install node16
apt-get install -y curl wget && \
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
apt-get install -y nodejs && \
# Feature-parity with node.js base images.
apt-get install -y --no-install-recommends git openssh-client && \
npm install -g yarn && \
# clean apt cache
rm -rf /var/lib/apt/lists/* && \
# Create the pwuser
adduser pwuser
# === Install the base requirements to run and debug webdriver implementations ===
# Install up-to-date node & npm, deps for virtual screen & noVNC, firefox, pip for apprise.
RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \
xvfb \
ca-certificates \
x11vnc \
curl \
tini \
novnc websockify \
dos2unix \
&& apt-get install --no-install-recommends -y curl ca-certificates gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
nodejs \
xvfb \
x11vnc \
tini \
novnc websockify \
dos2unix \
python3-pip \
libgtk-3-0 \
libasound2 \
libxcomposite1 \
libpangocairo-1.0-0 \
libpango-1.0-0 \
libatk1.0-0 \
libcairo-gobject2 \
libcairo2 \
libgdk-pixbuf-2.0-0 \
libdbus-glib-1-2 \
libxcursor1 \
libnss3 \
libnspr4 \
libgbm1 \
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf \
/tmp/* \
/usr/share/doc/* \
/var/cache/* \
/var/lib/apt/lists/* \
/var/tmp/* \
&& ln -s $NOVNC_HOME/vnc_auto.html $NOVNC_HOME/index.html
/tmp/* \
/usr/share/doc/* \
/var/cache/* \
/var/lib/apt/lists/* \
/var/tmp/* \
&& useradd -ms /bin/bash fgc \
&& ln -s /usr/share/novnc/vnc_auto.html /usr/share/novnc/index.html \
&& pip install apprise
WORKDIR /fgc
COPY package.json .
# Install chromium & dependencies only
RUN npm install \
&& npx playwright install --with-deps chromium \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY package*.json ./
COPY . .
# Playwright installs patched firefox to ~/.cache/ms-playwright/firefox-*
# Requires some system deps to run (see inlined install-deps above).
RUN npm install
# Old: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD + install firefox (had to be done after `npm install` to get the correct version). Now: playwright-firefox as npm dep and `npm install` will only install that.
# From 1.38 Playwright will no longer install browser automatically for playwright, but apparently still for playwright-firefox: https://github.com/microsoft/playwright/releases/tag/v1.38.0
# RUN npx playwright install firefox
# Shell scripts
RUN dos2unix ./docker/*.sh
RUN mv ./docker/entrypoint.sh /usr/local/bin/entrypoint \
&& chmod +x /usr/local/bin/entrypoint
# Only copy the files we actually need in the image to avoid accidentally adding secrets.
COPY *.js ./
COPY eslint.config.js jsconfig.json sonar-project.properties ./
COPY src ./src
COPY test ./test
COPY docker-entrypoint.sh ./
ENTRYPOINT ["entrypoint"]
CMD ["node", "epic-games.js"]
# Shell scripts need Linux line endings. On Windows, git might be configured to check out dos/CRLF line endings, so we convert them for those people in case they want to build the image. They could also use --config core.autocrlf=input
RUN dos2unix ./*.sh && chmod +x ./*.sh && chown -R fgc:fgc /fgc
COPY docker-entrypoint.sh /usr/local/bin/
ARG COMMIT=""
ARG BRANCH=""
ARG NOW=""
ENV COMMIT=${COMMIT}
ENV BRANCH=${BRANCH}
ENV NOW=${NOW}
LABEL org.opencontainers.image.title="free-games-claimer" \
org.opencontainers.image.name="free-games-claimer" \
org.opencontainers.image.description="Automatically claims free games on the Epic Games Store, Amazon Prime Gaming and GOG" \
org.opencontainers.image.url="https://github.com/vogler/free-games-claimer" \
org.opencontainers.image.source="https://github.com/vogler/free-games-claimer" \
org.opencontainers.image.revision=${COMMIT} \
org.opencontainers.image.ref.name=${BRANCH} \
org.opencontainers.image.base.name="ubuntu:jammy" \
org.opencontainers.image.version="latest"
# Configure VNC via environment variables:
ENV VNC_PORT 5900
ENV NOVNC_PORT 6080
# Ports are not exposed by default; publish explicitly with -p when you really need GUI access.
# EXPOSE 5900
# EXPOSE 6080
# Configure Xvfb via environment variables:
ENV WIDTH 1920
ENV HEIGHT 1080
ENV DEPTH 24
# Show browser instead of running headless
ENV SHOW 1
USER fgc
# Script to setup display server & VNC is always executed.
ENTRYPOINT ["docker-entrypoint.sh"]
# Default command to run. This is replaced by appending own command, e.g. `docker run ... node prime-gaming` to only run this script.
CMD node epic-games; node prime-gaming; node gog

661
LICENSE Normal file
View file

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

151
README.md
View file

@ -1,83 +1,88 @@
# free-games-claimer
Claims free games on
- [Epic Games Store](https://www.epicgames.com/store/free-games)
- [Amazon Prime Gaming](https://gaming.amazon.com)
- PRs welcome :)
Free Games Claimer (Fork)
==========================
## Setup
... should be the same on Windows/macOS/Linux:
[![Quality Gate Status](https://sonata.cyber77.de/api/project_badges/measure?project=free-games-claimer&metric=alert_status&token=sqb_99c83edf82a1331f0c649f8a5b698b4ec8f9a965)](https://sonata.cyber77.de/dashboard?id=free-games-claimer)
1. [Install Node.js](https://nodejs.org/en/download)
2. Clone/download this repository and `cd` into it in a terminal
3. Run `npm install && npx playwright install chromium`
Automates claiming of free games for:
- Amazon Luna Gaming / Luna claims (including external stores like GOG, Epic Games, Legacy Games )
- GOG giveaways
- Optional extras: Steam stats, AliExpress dailies (not implemated yet)
This downloads Chromium (343 MB) to a cache in home ([doc](https://playwright.dev/docs/browsers#managing-browser-binaries)).
If you are missing some dependencies for the browser on your system, you can use `sudo npx playwright install chromium --with-deps`.
Requirements
------------
- Docker or Podman (recommended), or Node.js ≥ 20 for local runs
- Optional notifications: `pip install apprise`
- Playwright dependencies are baked into the container; locally, `npm install` downloads Firefox.
## Usage
Both scripts start an automated Chromium instance, either with the browser GUI shown or hidden (*headless mode*).
Quickstart (Docker Run)
-----------------------
```
docker run --rm -it \
-p 6080:6080 \
-v fgc:/fgc/data \
-e SHOW=1 \
git.sky-net.it/nocci/free-games-claimer:latest \
node prime-gaming.js
```
- Ports 6080/5900: noVNC/VNC (only needed with `SHOW=1`)
- Data/configs are stored in volume `fgc` under `/fgc/data`
Login has to be done in the browser. It's hard to automate since you usually need to enter some OTP (but you can select 'remember this device').
After login, the script will just continue, but you can also restart it.
Docker Compose Example
----------------------
```yaml
services:
fgc:
image: git.sky-net.it/nocci/free-games-claimer:latest
container_name: fgc
environment:
- SHOW=1 # show browser via VNC/noVNC
# - PG_EMAIL=...
# - PG_PASSWORD=...
# - PG_OTPKEY=...
volumes:
- fgc:/fgc/data
ports:
- "6080:6080" # noVNC
# - "5900:5900" # VNC optional
command: bash -c "node epic-games; node prime-gaming; node gog"
volumes:
fgc:
```
If something goes wrong, use `PWDEBUG=1 node ...` to [inspect](https://playwright.dev/docs/inspector).
Configuration (Environment Variables)
-------------------------------------
Common options:
- `SHOW=0/1` (0 = headless, 1 = UI)
- `WIDTH`, `HEIGHT` (browser size)
- `TIMEOUT`, `LOGIN_TIMEOUT` (seconds)
- Login: `EMAIL`, `PASSWORD` global; per store `EG_EMAIL`, `EG_PASSWORD`, `EG_OTPKEY`, `PG_EMAIL`, `PG_PASSWORD`, `PG_OTPKEY`, `GOG_EMAIL`, `GOG_PASSWORD`
- Prime Gaming: `PG_REDEEM=1` (auto-redeem keys, experimental), `PG_CLAIMDLC=1`, `PG_TIMELEFT=<days>` to skip long-remaining offers
- Screenshots: `SCREENSHOTS_DIR` (default `data/screenshots`)
- Notifications: `NOTIFY='...'` (Apprise URL), optional `NOTIFY_TITLE`
- Browser profile: `BROWSER_DIR` (default `data/browser`)
- Recording: `RECORD=1` to save videos/HAR to `data/record/`
- Debugging: `DEBUG=1` (opens Playwright inspector), `DEBUG_NETWORK=1` (logs requests), `TIME=1` (prints timings)
- Dry run / Interaction: `DRYRUN=1` (do not claim), `INTERACTIVE=1` (ask before claiming), `HEADLESS` is derived from `SHOW`/`DEBUG`
- Directories: `SCREENSHOTS_DIR`, `BROWSER_DIR`, `DATA_DIR` (prefix for data; default under `data/`)
- VNC/noVNC: `VNC_PASSWORD` (for Docker entrypoint), `NOVNC_PORT`/`VNC_PORT` (Docker)
- General timeouts: `TIMEOUT` (per action), `LOGIN_TIMEOUT` (extra time for login)
### Epic Games Store
Alternatives:
- Run `node epic-games` (browser window will open, [headless leads to captcha](https://github.com/vogler/free-games-claimer/issues/2))
- Run with Docker (browser is hidden inside -> headless for host):
- [Install Docker](https://docs.docker.com/get-docker/)
- `npm run docker:build`
- `npm run docker:epic-games`
- When you need to login, go to http://localhost:6080 with password `secret` (you can also connect with another VNC client)
You can place a `data/config.env`; it is loaded via dotenv and is overridden by explicitly set environment variables.
### Amazon Prime Gaming
Run `node prime-gaming`
Local Run Without Docker
------------------------
```
npm install
SHOW=0 PG_EMAIL=... PG_PASSWORD=... PG_OTPKEY=... node prime-gaming.js
```
- Playwright downloads Firefox to `~/.cache/ms-playwright`.
- Use `SHOW=1` for a visible browser (requires GUI/Xvfb).
Runs headless. Run `node prime-gaming show` to show the GUI (to login).
Persistence & Outputs
---------------------
- Data/status: `data/*.json` (per store)
- Browser profile: `data/browser`
- Screenshots: `data/screenshots/<store>/`
- Optional videos/HAR: `RECORD=1``data/record/`
Claiming the Amazon Games works, external Epic Games also work if the account is linked.
Keys for {Origin, GOG.com, Legacy Games} should be printed to the console and need to be redeemed manually at the URL printed to the terminal ([issue](https://github.com/vogler/free-games-claimer/issues/5)).
A screenshot of the page with the code is saved to `data/screenshots` as well.
### Run periodically
Epic Games releases one (sometimes more) free game *every week*, but around christmas every day.
Prime Gaming has new games *every month*.
It is save to run both scripts every day.
If you can't use Docker for quasi-headless mode, you could run in a virtual machine, on a server, or you wake your PC at night to avoid being interrupted.
- Linux/macOS: `crontab -e`
- macOS: [launchd](https://stackoverflow.com/questions/132955/how-do-i-set-a-task-to-run-every-so-often)
- Windows: [task scheduler](https://active-directory-wp.com/docs/Usage/How_to_add_a_cron_job_on_Windows/Scheduled_tasks_and_cron_jobs_on_Windows/index.html), [other options](https://stackoverflow.com/questions/132971/what-is-the-windows-version-of-cron)
## History/DevLog
<details>
<summary>Click to expand</summary>
Tried [epicgames-freebies-claimer](https://github.com/Revadike/epicgames-freebies-claimer), but does not work anymore since epicgames introduced hcaptcha (see [issue](https://github.com/Revadike/epicgames-freebies-claimer/issues/172)).
Played around with puppeteer before, now trying newer https://playwright.dev which is pretty similar.
Playwright Inspector and `codegen` to generate scripts are nice, but failed to generate the right code for clicking a button in an iframe.
Added [main.spec.ts](https://github.com/vogler/epicgames-claimer/commit/e5ce7916ab6329cfc7134677c4d89c2b3fa3ba97#diff-d18d03e9c407a20e05fbf03cbd6f9299857740544fb6b50d6a70b9c6fbc35831) which was the test script generated by `npx playwright codegen` with manual fix for clicking buttons in the created iframe. Can be executed by `npx playwright test`. The test runner has options `--debug` and `--timeout` and can execute typescript which is nice. However, this only worked up to the button 'I Agree', and then showed an hcaptcha.
Added [main.captcha.js](https://github.com/vogler/epicgames-claimer/commit/e5ce7916ab6329cfc7134677c4d89c2b3fa3ba97#diff-d18d03e9c407a20e05fbf03cbd6f9299857740544fb6b50d6a70b9c6fbc35831) which uses beta of `playwright-extra@next` and `@extra/recaptcha@next` (from [comment on puppeteer-extra](https://github.com/berstend/puppeteer-extra/pull/303#issuecomment-775277480)).
However, `playwright-extra` seems to be old and missing `:has-text` selector (fixed [here](https://github.com/vogler/epicgames-claimer/commit/ba97a0e840b65f4476cca18e28d8461b0c703420)) and `page.frameLocator`, so the script did not run without adjustments.
Also, solving via [2captcha](https://2captcha.com?from=13225256) is a paid service which takes time and may be unreliable.
<!-- Alternative: https://anti-captcha.com -->
Added [main.stealth.js](https://github.com/vogler/epicgames-claimer/commit/64d0ba8ce71baec3947d1b64acd567befcb39340#diff-f70d3bd29df4a343f11062a97063953173491ce30fe34f69a0fc52517adbf342) which uses the stealth plugin without `playwright-extra` wrapper but up-to-date `playwright` (from [comment](https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212)).
The listed evasions are enough to not show an hcaptcha. Script claimed game successfully in non-headless mode.
Removed `main.captcha.js`.
Using Playwright Test (`main.spec.ts`) instead of Library (`main.stealth.js`) has the advantage of free CLI like `--debug` and `--timeout`.
<!-- TODO: check if stealth plugin can be setup with `contextOptions` ([doc](https://playwright.dev/docs/test-configuration#more-browser-and-context-options)). -->
Button selectors should preferably use text in order to be more stable against changes in the DOM.
Renamed repository from epicgames-claimer to free-games-claimer since a script for Amazon Prime Gaming was also added. Removed all old scripts in favor of just `epic-games.js` and `prime-gaming.js`.
epic games: `headless` mode gets hcaptcha challenge. More details/references in [issue](https://github.com/vogler/free-games-claimer/issues/2).
</details>
Tip: For captchas or first-time login, run with `SHOW=1` and log in once; cookies stay in the profile. Notifications via `NOTIFY` help surface errors (e.g., captcha, login).

113
aliexpress.js Normal file
View file

@ -0,0 +1,113 @@
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
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...
// from https://github.com/apify/fingerprint-suite/issues/162
import { FingerprintInjector } from 'fingerprint-injector';
import { FingerprintGenerator } from 'fingerprint-generator';
const { fingerprint, headers } = new FingerprintGenerator().getFingerprint({
devices: ['mobile'],
operatingSystems: ['android'],
});
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL
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/aliexpress-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
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,
},
extraHTTPHeaders: {
'accept-language': headers['accept-language'],
},
});
handleSIGINT(context);
await new FingerprintInjector().attachFingerprintToPlaywright(context, { fingerprint, headers });
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 => {
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
await Promise.any([page.waitForURL(url => url.includes('login.aliexpress.com')).then(async () => {
console.error('Not logged in! Will wait for 120s for you to 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"]');
await emailInput.fill(email);
await emailInput.blur(); // otherwise Continue button stays disabled
const continueButton = login.locator('button:has-text("Continue")');
await continueButton.click({ force: true }); // normal click waits for button to no longer be covered by their suggestion menu, so we have to force click somewhere for the menu to close and then click
await continueButton.click();
const password = email && (cfg.ae_password || await prompt({ type: 'password', message: 'Enter password' }));
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()));
await page.waitForURL(url);
page.getByRole('button', { name: 'Accept cookies' }).click().then(() => console.log('Accepted cookies')).catch(() => { });
}), page.locator('#nav-user-account').waitFor()]).catch(() => {});
};
// copied URLs from AliExpress app on tablet which has menu for the used webview
const urls = {
// works with desktop view, but stuck at 100% loading in mobile view:
coins: 'https://www.aliexpress.com/p/coin-pc-index/index.html',
// only work with mobile view:
grow: 'https://m.aliexpress.com/p/ae_fruit/index.html', // firefox: stuck at 60% loading, chrome: loads, but canvas
gogo: 'https://m.aliexpress.com/p/gogo-match-cc/index.html', // closes firefox?!
// only show notification to install the app
euro: 'https://m.aliexpress.com/p/european-cup/index.html', // doesn't load
merge: 'https://m.aliexpress.com/p/merge-market/index.html',
};
/* eslint-disable no-unused-vars */
const coins = async () => {
await Promise.any([page.locator('.checkin-button').click(), page.locator('.addcoin').waitFor()]);
console.log('Coins:', await page.locator('.mycoin-content-right-money').innerText());
console.log('Streak:', await page.locator('.title-box').innerText());
console.log('Tomorrow:', await page.locator('.addcoin').innerText());
};
const grow = async () => {
await page.pause();
};
const gogo = async () => {
await page.pause();
};
const euro = async () => {
await page.pause();
};
const merge = async () => {
await page.pause();
};
/* eslint-enable no-unused-vars */
try {
await [
merge,
].reduce((a, f) => a.then(async () => {
await auth(urls[f.name]);
await f();
console.log();
}), Promise.resolve());
} catch (error) {
process.exitCode ||= 1;
console.error('--- Exception:');
console.error(error); // .toString()?
}
if (page.video()) console.log('Recorded video:', await page.video().path());
await context.close();

15
docker-compose.yml Normal file
View file

@ -0,0 +1,15 @@
# start with `docker compose up`
services:
free-games-claimer:
container_name: fgc # is printed in front of every output line
image: ghcr.io/vogler/free-games-claimer # otherwise image name will be free-games-claimer-free-games-claimer
build: .
ports:
# - "5900:5900" # VNC server
- "6080:6080" # noVNC (browser-based VNC client)
volumes:
- fgc:/fgc/data
# command: bash -c "node epic-games; node gog"
environment:
# - EMAIL=foo@bar.org
# - NOTIFY='tgram://...'

79
docker-entrypoint.sh Executable file
View file

@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -eo pipefail # exit on error, error on any fail in pipe (not just last cmd); add -x to print each cmd; see gist bash_strict_mode.md
echo "Version: https://github.com/vogler/free-games-claimer/tree/${COMMIT}"
[ ! -z $BRANCH ] && [ $BRANCH != "main" ] && echo "Branch: ${BRANCH}"
echo "Build: $NOW"
# Ensure writable data dir for fgc when host bind-mount is owned by root.
if [ "$(id -u)" -eq 0 ]; then
chown -R 1000:1000 /fgc/data 2>/dev/null || true
fi
# Remove chromium profile lock.
# When running in docker and then killing it, on the next run chromium displayed a dialog to unlock the profile which made the script time out.
# Maybe due to changed hostname of container or due to how the docker container kills playwright - didn't check.
# https://bugs.chromium.org/p/chromium/issues/detail?id=367048
rm -f /fgc/data/browser/SingletonLock 2>/dev/null || true
# Firefox preferences are stored in $BROWSER_DIR/pref.js and can be overridden by a file user.js
# Since this file has to be in the volume (data/browser), we can't do this in Dockerfile.
mkdir -p /fgc/data/browser
# clean up stale firefox locks that can trigger "already running"
rm -f /fgc/data/browser/parent.lock /fgc/data/browser/lock /fgc/data/browser/.parentlock 2>/dev/null || true
# fix for 'Incorrect response' after solving a captcha correctly - https://github.com/vogler/free-games-claimer/issues/261#issuecomment-1868385830
# Only write the prefs file when the volume is writable (container runs as non-root).
if [ -w /fgc/data/browser ] && { [ ! -e /fgc/data/browser/user.js ] || [ -w /fgc/data/browser/user.js ] || rm -f /fgc/data/browser/user.js 2>/dev/null; }; then
cat << 'EOT' > /fgc/data/browser/user.js
user_pref("privacy.resistFingerprinting", true);
// user_pref("privacy.resistFingerprinting.letterboxing", true);
// user_pref("browser.contentblocking.category", "strict");
// user_pref("webgl.disabled", true);
EOT
else
echo "Warning: /fgc/data/browser not writable; skipping user.js creation."
fi
# TODO disable session restore message?
# Remove X server display lock, fix for `docker compose up` which reuses container which made it fail after initial run, https://github.com/vogler/free-games-claimer/issues/31
# echo $DISPLAY
# ls -l /tmp/.X11-unix/
rm -f /tmp/.X1-lock
# Ensure X11 socket dir exists with sane ownership/permissions.
mkdir -p /tmp/.X11-unix
if [ "$(stat -c %U /tmp/.X11-unix 2>/dev/null)" != "root" ]; then
chown root:root /tmp/.X11-unix 2>/dev/null || chmod 1777 /tmp/.X11-unix
fi
# 6000+SERVERNUM is the TCP port Xvfb is listening on:
# SERVERNUM=$(echo "$DISPLAY" | sed 's/:\([0-9][0-9]*\).*/\1/')
# Options passed directly to the Xvfb server:
# -ac disables host-based access control mechanisms
# screen NUM WxHxD creates the screen and sets its width, height, and depth
export DISPLAY=:1 # need to export this, otherwise playwright complains with 'Looks like you launched a headed browser without having a XServer running.'
Xvfb $DISPLAY -ac -screen 0 "${WIDTH}x${HEIGHT}x${DEPTH}" &
echo "Xvfb display server created screen with resolution ${WIDTH}x${HEIGHT}"
if [ -z "$VNC_PASSWORD" ]; then
pw="-nopw"
pwt="no password!"
else
pw="-passwd $VNC_PASSWORD"
pwt="with password"
fi
x11vnc -display $DISPLAY -forever -shared -rfbport $VNC_PORT -bg $pw 2>/dev/null 1>&2
echo "VNC is running on port $VNC_PORT ($pwt)"
websockify -D --web "/usr/share/novnc/" $NOVNC_PORT "localhost:$VNC_PORT" 2>/dev/null 1>&2 &
echo "noVNC (VNC via browser) is running on http://localhost:$NOVNC_PORT"
echo
# Ensure Playwright browsers are available (chromium + firefox).
if [ ! -x /home/fgc/.cache/ms-playwright/firefox-1482/firefox/firefox ] || ! ls /home/fgc/.cache/ms-playwright/chromium-*/*/chrome >/dev/null 2>&1; then
echo "Playwright browsers missing; installing..."
npx playwright install chromium firefox || echo "Warning: failed to install Playwright browsers" >&2
fi
exec tini -g -- "$@" # https://github.com/krallin/tini/issues/8 node/playwright respond to signals like ctrl-c, but unsure about zombie processes

View file

@ -1,21 +0,0 @@
#!/bin/sh
# Remove chromium profile lock.
# When running in docker and then killing it, on the next run chromium displayed a dialog to unlock the profile which made the script time out.
# Maybe due to changed hostname of container or due to how the docker container kills playwright - didn't check.
# https://bugs.chromium.org/p/chromium/issues/detail?id=367048
rm -f /fgc/data/browser/SingletonLock
# 6000+SERVERNUM is the TCP port Xvfb is listening on:
# SERVERNUM=$(echo "$DISPLAY" | sed 's/:\([0-9][0-9]*\).*/\1/')
# Options passed directly to the Xvfb server:
# -ac disables host-based access control mechanisms
# screen NUM WxHxD creates the screen and sets its width, height, and depth
Xvfb :1 -ac -screen 0 "${SCREEN_WIDTH}x${SCREEN_HEIGHT}x${SCREEN_DEPTH}" >/dev/null 2>&1 &
x11vnc -display :1.0 -forever -shared -rfbport "${VNC_PORT:-5900}" -passwd "${VNC_PASSWORD:-secret}" -bg
websockify -D --web "$NOVNC_HOME" "$NOVNC_PORT" "localhost:$VNC_PORT" &
DISPLAY=:1.0
export DISPLAY
exec tini -g -- "$@"

View file

@ -1,179 +1,360 @@
import { chromium } from 'playwright'; // stealth plugin needs no outdated playwright-extra
import path from 'path';
import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
import { authenticator } from 'otplib';
import chalk from 'chalk';
import path from 'node:path';
import { existsSync, writeFileSync, appendFileSync } from 'node:fs';
import { resolve, jsonDb, datetime, stealth, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js';
import { cfg } from './src/config.js';
const debug = process.env.PWDEBUG == '1'; // runs non-headless and opens https://playwright.dev/docs/inspector
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'epic-games', ...a);
const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games';
const URL_LOGIN = 'https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM;
const TIMEOUT = 20 * 1000; // 20s, default is 30s
const SCREEN_WIDTH = Number(process.env.SCREEN_WIDTH) - 80 || 1280;
const SCREEN_HEIGHT = Number(process.env.SCREEN_HEIGHT) || 1280;
const db = await jsonDb('epic-games.json');
db.data ||= { claimed: [], runs: [] };
const run = {
startTime: datetime(),
endTime: null,
n: null, // unclaimed games at beginning
c: 0, // claimed games at end
};
console.log(datetime(), 'started checking epic-games');
// check if there are new games to claim before launching browser? https://github.com/vogler/free-games-claimer/issues/29
// get current promotionalOffers
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // otherwise got UNABLE_TO_GET_ISSUER_CERT_LOCALLY
const promoJson = await (await fetch('https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US')).json();
const currentGames = promoJson.data.Catalog.searchStore.elements.filter(e => e.promotions?.promotionalOffers?.length);
console.log('Current games:', currentGames.map(e => e.title));
const db = await jsonDb('epic-games.json', {});
// Option 1: check order history using previously stored cookies
const cookiesPath = path.resolve(dirs.browser, 'epic-games-cookies.json');
if (existsSync(cookiesPath)) {
const cookieJson = JSON.parse(readFileSync(cookiesPath, 'utf8'));
let cookie = cookieJson.filter(e => e.domain == '.epicgames.com').map(e => `${e.name}=${e.value}`).join('; ');
// console.log(cookie);
// those cookies don't work but result in html to login as response instead of json - override with Dev Tools > Request Headers > cookie
if (existsSync('data/cookie')) {
cookie = readFileSync('data/cookie', 'utf8');
}
const orderHistory = await (await fetch('https://www.epicgames.com/account/v2/payment/ajaxGetOrderHistory?locale=en-US', {
headers: { cookie }
})).json();
const ordered = orderHistory.orders.flatMap(o => o.orderStatus == 'COMPLETED' ? o.items.map(i => i.namespace) : []);
// console.log(ordered);
const unclaimed = currentGames.filter(e => !(e.namespace in ordered));
console.log('Unclaimed games:', unclaimed.map(e => e.title));
if (cfg.time) console.time('startup');
const browserPrefs = path.join(cfg.dir.browser, 'prefs.js');
if (existsSync(browserPrefs)) {
console.log('Adding webgl.disabled to', browserPrefs);
appendFileSync(browserPrefs, 'user_pref("webgl.disabled", true);'); // apparently Firefox removes duplicates (and sorts), so no problem appending every time
} else {
console.log(browserPrefs, 'does not exist yet, will patch it on next run. Restart the script if you get a captcha.');
}
// Option 2: launch headless browser to check order history - should take care of handling cookies, but if there are games to claim we need to re-launch non-headless
// Option 3: ignore order history and check claimed games per account in data/epic-games.json
process.exit(0);
// https://playwright.dev/docs/auth#multi-factor-authentication
const context = await chromium.launchPersistentContext(dirs.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: SCREEN_WIDTH, height: SCREEN_HEIGHT },
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO update if browser is updated!
locale: "en-US", // ignore OS locale to be sure to have english text for locators
// recordVideo: { dir: 'data/videos/' }, // will record a .webm video for each page navigated
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',
],
ignoreDefaultArgs: ['--enable-automation'], // remove default arg that shows the info bar with 'Chrome is being controlled by automated test software.'
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0', // Windows UA avoids "device not supported"; update when browser version changes
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-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
// user settings for firefox have to be put in $BROWSER_DIR/user.js
args: [], // https://wiki.mozilla.org/Firefox/CommandLineOptions
});
handleSIGINT(context);
// Without stealth plugin, the website shows an hcaptcha on login with username/password and in the last step of claiming a game. It may have other heuristics like unsuccessful logins as well. After <6h (TBD) it resets to no captcha again. Getting a new IP also resets.
await stealth(context);
if (!debug) context.setDefaultTimeout(TIMEOUT);
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));
await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
// some debug info about the page (screen dimensions, user agent)
if (cfg.debug) {
/* global window, navigator */
const debugInfo = await page.evaluate(() => {
const { width, height, availWidth, availHeight } = window.screen;
return {
screen: { width, height, availWidth, availHeight },
userAgent: navigator.userAgent,
};
});
console.debug(debugInfo);
}
if (cfg.debug_network) {
// const filter = _ => true;
const filter = r => r.url().includes('store.epicgames.com');
page.on('request', request => filter(request) && console.log('>>', request.method(), request.url()));
page.on('response', response => filter(response) && console.log('<<', response.status(), response.url()));
}
const notify_games = [];
let user;
try {
await context.addCookies([
{ name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), domain: '.epicgames.com', path: '/' }, // Accept cookies to get rid of banner to save space on screen. Set accept time to 5 days ago.
{ name: 'HasAcceptedAgeGates', value: 'USK:9007199254740991,general:18,EPIC SUGGESTED RATING:18', domain: 'store.epicgames.com', path: '/' }, // gets rid of 'To continue, please provide your date of birth', https://github.com/vogler/free-games-claimer/issues/275, USK number doesn't seem to matter, cookie from 'Fallout 3: Game of the Year Edition'
]);
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // 'domcontentloaded' faster than default 'load' https://playwright.dev/docs/api/class-page#page-goto
// Accept cookies to get rid of banner to save space on screen. Will only appear for a fresh context, so we don't await, but let it time out if it does not exist and catch the exception. clickIfExists by checking selector's count > 0 did not work.
page.click('button:has-text("Accept All Cookies")').catch(_ => { }); // _ => console.info('Cookies already accepted')
if (cfg.time) console.timeEnd('startup');
if (cfg.time) console.time('login');
while (await page.locator('a[role="button"]:has-text("Sign In")').count() > 0) { // TODO also check alternative for signed-in state
console.error("Not signed in anymore. Please login and then navigate to the 'Free Games' page. If using docker, open http://localhost:6080");
context.setDefaultTimeout(0); // give user time to log in without timeout
while (await page.locator('egs-navigation').getAttribute('isloggedin') != 'true') {
console.error('Not signed in anymore. Please login in the browser or here in the terminal.');
if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`);
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log in
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' });
// after login it just reloads the login page...
await page.waitForNavigation({ url: URL_CLAIM });
context.setDefaultTimeout(TIMEOUT);
// process.exit(1);
if (cfg.eg_email && cfg.eg_password) console.info('Using email 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 notifyBrowserLogin = async () => {
console.log('Waiting for you to login in the browser.');
await notify('epic-games: no longer signed in and not enough options set for automatic login.');
if (cfg.headless) {
console.log('Run `SHOW=1 node epic-games` to login in the opened browser.');
await context.close(); // finishes potential recording
process.exit(1);
}
};
const email = cfg.eg_email || await prompt({ message: 'Enter email' });
if (email) {
const watchCaptchaChallenge = async () => {
try {
await page.waitForSelector('.h_captcha_challenge iframe', { timeout: 15000 });
console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.');
await notify('epic-games: got captcha during login. Please check.');
} catch {
return;
}
};
const watchCaptchaIncorrect = async () => {
try {
await page.waitForSelector('p:has-text("Incorrect response.")', { timeout: 15000 });
console.error('Incorrect response for captcha!');
} catch {
return;
}
};
watchCaptchaChallenge();
watchCaptchaIncorrect();
await page.fill('#email', email);
const password = cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' });
if (password) {
await page.fill('#password', password);
await page.click('button[type="submit"]');
} else await notifyBrowserLogin();
const error = page.locator('#form-error-message');
const watchLoginError = async () => {
try {
await error.waitFor({ timeout: 15000 });
console.error('Login error:', await error.innerText());
console.log('Please login in the browser!');
} catch {
return;
}
};
const watchMfaStep = async () => {
try {
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...');
const otp = cfg.eg_otpkey && authenticator.generate(cfg.eg_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 page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
await page.click('button[type="submit"]');
} catch {
return;
}
};
watchLoginError();
watchMfaStep();
} else await notifyBrowserLogin();
await page.waitForURL(URL_CLAIM);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
}
console.log('Signed in.');
user = await page.locator('egs-navigation').getAttribute('displayname'); // 'null' if !isloggedin
console.log(`Signed in as ${user}`);
db.data[user] ||= {};
if (cfg.time) console.timeEnd('login');
if (cfg.time) console.time('claim all games');
// Detect free games
const game_loc = await page.locator('a:has(span:text-is("Free Now"))');
await game_loc.last().waitFor();
const game_loc = page.locator('a:has(span:text-is("Free Now"))');
await game_loc.last().waitFor().catch(_ => {
// rarely there are no free games available -> catch Timeout
// waiting for timeout; alternative would be waiting for "coming soon"
// see https://github.com/vogler/free-games-claimer/issues/210#issuecomment-1727420943
console.error('Seems like currently there are no free games available in your region...');
// urls below should then be an empty list
});
// clicking on `game_sel` sometimes led to a 404, see https://github.com/vogler/free-games-claimer/issues/25
// debug showed that in those cases the href was still correct, so we `goto` the urls instead of clicking.
// Alternative: parse the json loaded to build the page https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions
// filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213
// i.e. filter data.Catalog.searchStore.elements for .promotions.promotionalOffers being set and build URL with .catalogNs.mappings[0].pageSlug or .urlSlug if not set to some wrong id like it was the case for spirit-of-the-north-f58a66 - this is also what's done here: https://github.com/claabs/epicgames-freegames-node/blob/938a9653ffd08b8284ea32cf01ac8727d25c5d4c/src/puppet/free-games.ts#L138-L213
const urlSlugs = await Promise.all((await game_loc.elementHandles()).map(a => a.getAttribute('href')));
const urls = urlSlugs.map(s => 'https://store.epicgames.com' + s);
const n = run.n = await game_loc.count();
// console.log('Number of free games:', n);
console.log('Free games:', urls);
for (const url of urls) {
await page.goto(url, { waitUntil: 'domcontentloaded' });
const btnText = await page.locator('//button[@data-testid="purchase-cta-button"][not(contains(.,"Loading"))]').first().innerText(); // barrier to block until page is loaded
if (cfg.time) console.time('claim game');
await page.goto(url); // , { waitUntil: 'domcontentloaded' });
const purchaseBtn = page.locator('button[data-testid="purchase-cta-button"] >> :has-text("e"), :has-text("i")').first(); // when loading, the button text is empty -> need to wait for some text {'get', 'in library', 'requires base game'} -> just wait for e or i to not be too specific; :text-matches("\w+") somehow didn't work - https://github.com/vogler/free-games-claimer/issues/375
await purchaseBtn.waitFor();
const btnText = (await purchaseBtn.innerText()).toLowerCase(); // barrier to block until page is loaded
// click Continue if 'This game contains mature content recommended only for ages 18+'
if (await page.locator('button:has-text("Continue")').count() > 0) {
console.log('This game contains mature content recommended only for ages 18+');
await page.click('button:has-text("Continue")');
console.log(' This game contains mature content recommended only for ages 18+');
if (await page.locator('[data-testid="AgeSelect"]').count()) {
console.error(' Got "To continue, please provide your date of birth" - This shouldn\'t happen due to cookie set above. Please report to https://github.com/vogler/free-games-claimer/issues/275');
await page.locator('#month_toggle').click();
await page.locator('#month_menu li:has-text("01")').click();
await page.locator('#day_toggle').click();
await page.locator('#day_menu li:has-text("01")').click();
await page.locator('#year_toggle').click();
await page.locator('#year_menu li:has-text("1987")').click();
}
await page.click('button:has-text("Continue")', { delay: 111 });
await page.waitForTimeout(2000);
}
const title = await page.locator('h1 div').first().innerText();
const title_url = page.url().split('/').pop();
console.log('Current free game:', title, title_url);
if (btnText.toLowerCase() == 'in library') {
console.log('Already in library! Nothing to claim.');
} else { // GET
console.log('Not in library yet! Click GET.');
await page.click('[data-testid="purchase-cta-button"]');
// click Continue if 'Device not supported. This product is not compatible with your current device.'
await Promise.any(['button:has-text("Continue")', '#webPurchaseContainer iframe'].map(s => page.waitForSelector(s))); // wait for Continue xor iframe
if (await page.locator('button:has-text("Continue")').count() > 0) {
// console.log('Device not supported. This product is not compatible with your current device.');
await page.click('button:has-text("Continue")');
let title;
let bundle_includes;
if (await page.locator('span:text-is("About Bundle")').count()) {
title = (await page.locator('span:has-text("Buy"):left-of([data-testid="purchase-cta-button"])').first().innerText()).replace('Buy ', '');
// h1 first didn't exist for bundles but now it does... However h1 would e.g. be 'Fallout® Classic Collection' instead of 'Fallout Classic Collection'
try {
bundle_includes = await Promise.all((await page.locator('.product-card-top-row h5').all()).map(b => b.innerText()));
} catch (e) {
console.error('Failed to get "Bundle Includes":', e);
}
} else {
title = await page.locator('h1').first().innerText();
}
const game_id = page.url().split('/').pop();
const existedInDb = db.data[user][game_id];
db.data[user][game_id] ||= { title, time: datetime(), url: page.url() }; // this will be set on the initial run only!
console.log('Current free game:', chalk.blue(title));
if (bundle_includes) console.log(' This bundle includes:', bundle_includes);
const notify_game = { title, url, status: 'failed' };
notify_games.push(notify_game); // status is updated below
if (process.env.DRYRUN) continue;
if (debug) await page.pause();
if (btnText == 'in library') {
console.log(' Already in library! Nothing to claim.');
if (!existedInDb) await notify(`Game already in library: ${url}`);
notify_game.status = 'existed';
db.data[user][game_id].status ||= 'existed'; // does not overwrite claimed or failed
if (db.data[user][game_id].status.startsWith('failed')) db.data[user][game_id].status = 'manual'; // was failed but now it's claimed
} else if (btnText == 'requires base game') {
console.log(' Requires base game! Nothing to claim.');
notify_game.status = 'requires base game';
db.data[user][game_id].status ||= 'failed:requires-base-game';
// if base game is free, add to queue as well
const baseUrl = 'https://store.epicgames.com' + await page.locator('a:has-text("Overview")').getAttribute('href');
console.log(' Base game:', baseUrl);
// await page.click('a:has-text("Overview")');
// re-add original add-on to queue after base game
urls.push(baseUrl, url); // add base game to the list of games to claim and re-add add-on itself
} else { // GET
console.log(' Not in library yet! Click', btnText);
await purchaseBtn.click({ delay: 11 }); // got stuck here without delay (or mouse move), see #75, 1ms was also enough
// Accept End User License Agreement (only needed once)
const acceptEulaIfShown = async () => {
try {
await page.locator(':has-text("end user license agreement")').waitFor({ timeout: 10000 });
console.log(' Accept End User License Agreement (only needed once)');
await page.locator('input#agree').check();
await page.locator('button:has-text("Accept")').click();
} catch {
return;
}
};
acceptEulaIfShown();
// it then creates an iframe for the purchase
await page.waitForSelector('#webPurchaseContainer iframe');
const iframe = page.frameLocator('#webPurchaseContainer iframe');
await iframe.locator('button:has-text("Place Order")').click();
// I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872
const btnAgree = iframe.locator('button:has-text("I Agree")');
try {
await Promise.any([btnAgree.waitFor().then(() => btnAgree.click()), page.waitForSelector('text=Thank you for buying').then(_ => { })]); // EU: wait for agree button, non-EU: potentially done
// TODO check for hcaptcha - the following is even true when no captcha challenge is shown...
// if (await iframe.frameLocator('#talon_frame_checkout_free_prod').locator('text=Please complete a security check to continue').count() > 0) {
// console.error('Encountered hcaptcha. Giving up :(');
// }
await page.waitForSelector('text=Thank you for buying'); // EU: wait, non-EU: wait again = no-op
db.data.claimed.push({ title, time: datetime(), url: page.url() });
run.c++;
console.log('Claimed successfully!');
} catch (e) {
console.log(e);
const p = path.resolve(dirs.screenshots, 'epic-games', 'captcha', `${filenamify(datetime())}.png`);
await page.screenshot({ path: p, fullPage: true });
console.info('Saved a screenshot of hcaptcha challenge to', p);
console.error('Got hcaptcha challenge. To avoid it, get a link from https://www.hcaptcha.com/accessibility'); // TODO save this link in config and visit it daily to set accessibility cookie to avoid captcha challenge?
// skip game if unavailable in region, https://github.com/vogler/free-games-claimer/issues/46
if (await iframe.locator(':has-text("unavailable in your region")').count() > 0) {
console.error(' This product is unavailable in your region!');
db.data[user][game_id].status = notify_game.status = 'unavailable-in-region';
if (cfg.time) console.timeEnd('claim game');
continue;
}
const p = path.resolve(dirs.screenshots, 'epic-games', `${title_url}.png`);
const enterParentalPinIfNeeded = async () => {
try {
await iframe.locator('.payment-pin-code').waitFor({ timeout: 10000 });
if (!cfg.eg_parentalpin) {
console.error(' EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.');
notify('epic-games: EG_PARENTALPIN not set. Need to enter Parental Control PIN manually.');
}
await iframe.locator('input.payment-pin-code__input').first().pressSequentially(cfg.eg_parentalpin);
await iframe.locator('button:has-text("Continue")').click({ delay: 11 });
} catch {
return;
}
};
enterParentalPinIfNeeded();
if (cfg.debug) await page.pause();
if (cfg.dryrun) {
console.log(' DRYRUN=1 -> Skip order!');
notify_game.status = 'skipped';
if (cfg.time) console.timeEnd('claim game');
continue;
}
// Playwright clicked before button was ready to handle event, https://github.com/vogler/free-games-claimer/issues/84#issuecomment-1474346591
await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 });
// I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872
const btnAgree = iframe.locator('button:has-text("I Accept")');
const acceptIfRequired = async () => {
try {
await btnAgree.waitFor({ timeout: 10000 });
await btnAgree.click();
} catch {
return;
}
}; // EU: wait for and click 'I Agree'
acceptIfRequired();
try {
// context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s?
const captcha = iframe.locator('#h_captcha_challenge_checkout_free_prod iframe');
const watchCaptchaChallenge = async () => {
try {
await captcha.waitFor({ timeout: 10000 });
console.error(' Got hcaptcha challenge! Lost trust due to too many login attempts? You can solve the captcha in the browser or get a new IP address.');
await notify(`epic-games: got captcha challenge for.\nGame link: ${url}`);
} catch {
return;
}
}; // may time out if not shown
const watchCaptchaFailure = async () => {
try {
await iframe.locator('.payment__errors:has-text("Failed to challenge captcha, please try again later.")').waitFor({ timeout: 10000 });
console.error(' Failed to challenge captcha, please try again later.');
await notify('epic-games: failed to challenge captcha. Please check.');
} catch {
return;
}
};
watchCaptchaChallenge();
watchCaptchaFailure();
await page.locator('text=Thanks for your order!').waitFor({ state: 'attached' });
db.data[user][game_id].status = 'claimed';
db.data[user][game_id].time = datetime(); // claimed time overwrites failed/dryrun time
console.log(' Claimed successfully!');
// context.setDefaultTimeout(cfg.timeout);
} catch (e) {
console.log(e);
// console.error(' Failed to claim! Try again if NopeCHA timed out. Click the extension to see if you ran out of credits (refill after 24h). To avoid captchas try to get a new IP or set a cookie from https://www.hcaptcha.com/accessibility');
console.error(' Failed to claim! To avoid captchas try to get a new IP address.');
const p = screenshot('failed', `${game_id}_${filenamify(datetime())}.png`);
await page.screenshot({ path: p, fullPage: true });
db.data[user][game_id].status = 'failed';
}
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...
}
if (cfg.time) console.timeEnd('claim game');
}
if (cfg.time) console.timeEnd('claim all games');
} catch (error) {
console.error(error);
run.error = error.toString();
process.exitCode ||= 1;
console.error('--- Exception:');
console.error(error); // .toString()?
if (error.message && process.exitCode != 130) notify(`epic-games failed: ${error.message.split('\n')[0]}`);
} finally {
// write out cookies (only for debugging)
await writeFileSync(cookiesPath, JSON.stringify(await context.cookies()));
// write out json db
run.endTime = datetime();
db.data.runs.push(run);
await db.write();
await db.write(); // write out json db
if (notify_games.filter(g => g.status == 'claimed' || g.status == 'failed').length) { // don't notify if all have status 'existed', 'manual', 'requires base game', 'unavailable-in-region', 'skipped'
notify(`epic-games (${user}):<br>${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();

76
eslint.config.js Normal file
View file

@ -0,0 +1,76 @@
// https://eslint.org/docs/latest/use/configure/configuration-files-new
// https://eslint.org/docs/latest/use/configure/migration-guide
import js from '@eslint/js';
import globals from 'globals';
import stylistic from '@stylistic/eslint-plugin-js';
export default [
// https://eslint.org/docs/latest/use/configure/configuration-files-new#globally-ignoring-files-with-ignores
// object with just `ignores` applies to all configuration objects
// had `ln -s .gitignore .eslintignore` before, but .eslintignore no longer supported
{
ignores: ['data/**'],
},
js.configs.recommended,
{
// files: ['*.js'],
languageOptions: {
globals: globals.node,
},
plugins: {
'@stylistic/js': stylistic,
},
// https://eslint.org/docs/latest/rules/
// https://eslint.style/packages/js
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'prefer-const': 'error',
'@stylistic/js/array-bracket-newline': ['error', 'consistent'],
'@stylistic/js/array-bracket-spacing': 'error',
'@stylistic/js/array-element-newline': ['error', 'consistent'],
'@stylistic/js/arrow-parens': ['error', 'as-needed'],
'@stylistic/js/arrow-spacing': 'error',
'@stylistic/js/block-spacing': 'error',
'@stylistic/js/brace-style': 'error',
'@stylistic/js/comma-dangle': ['error', 'always-multiline'],
'@stylistic/js/comma-spacing': 'error',
'@stylistic/js/comma-style': 'error',
'@stylistic/js/eol-last': 'error',
'@stylistic/js/func-call-spacing': 'error',
'@stylistic/js/function-paren-newline': ['error', 'consistent'],
'@stylistic/js/implicit-arrow-linebreak': 'error',
'@stylistic/js/indent': ['error', 2],
'@stylistic/js/key-spacing': 'error',
'@stylistic/js/keyword-spacing': 'error',
'@stylistic/js/linebreak-style': 'error',
'@stylistic/js/no-extra-parens': 'error',
'@stylistic/js/no-extra-semi': 'error',
'@stylistic/js/no-mixed-spaces-and-tabs': 'error',
'@stylistic/js/no-multi-spaces': 'error',
'@stylistic/js/no-multiple-empty-lines': 'error',
'@stylistic/js/no-tabs': 'error',
'@stylistic/js/no-trailing-spaces': 'error',
'@stylistic/js/no-whitespace-before-property': 'error',
'@stylistic/js/nonblock-statement-body-position': 'error',
'@stylistic/js/object-curly-newline': 'error',
'@stylistic/js/object-curly-spacing': ['error', 'always'],
'@stylistic/js/object-property-newline': ['error', { allowAllPropertiesOnSameLine: true }],
'@stylistic/js/quote-props': ['error', 'as-needed'],
'@stylistic/js/quotes': ['error', 'single'],
'@stylistic/js/rest-spread-spacing': 'error',
'@stylistic/js/semi': 'error',
'@stylistic/js/semi-spacing': 'error',
'@stylistic/js/semi-style': 'error',
'@stylistic/js/space-before-blocks': 'error',
'@stylistic/js/space-before-function-paren': ['error', { anonymous: 'never', named: 'never', asyncArrow: 'always' }],
'@stylistic/js/space-in-parens': 'error',
'@stylistic/js/space-infix-ops': 'error',
'@stylistic/js/space-unary-ops': 'error',
'@stylistic/js/spaced-comment': 'error',
'@stylistic/js/switch-colon-spacing': 'error',
'@stylistic/js/template-curly-spacing': 'error',
'@stylistic/js/template-tag-spacing': 'error',
'@stylistic/js/wrap-regex': 'error',
},
},
];

162
gog.js Normal file
View file

@ -0,0 +1,162 @@
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
import chalk from 'chalk';
import { resolve, jsonDb, datetime, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js';
import { cfg } from './src/config.js';
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'gog', ...a);
const URL_CLAIM = 'https://www.gog.com/en';
console.log(datetime(), 'started checking gog');
const db = await jsonDb('gog.json', {});
if (cfg.width < 1280) { // otherwise 'Sign in' and #menuUsername are hidden (but attached to DOM), see https://github.com/vogler/free-games-claimer/issues/335
console.error(`Window width is set to ${cfg.width} but needs to be at least 1280 for GOG!`);
process.exit(1);
}
// https://playwright.dev/docs/auth#multi-factor-authentication
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height },
locale: 'en-US', // ignore OS locale to be sure to have english text for locators -> done via /en in URL
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/gog-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
});
handleSIGINT(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
const notify_games = [];
let user;
try {
await context.addCookies([{ name: 'CookieConsent', value: '{stamp:%274oR8MJL+bxVlG6g+kl2we5+suMJ+Tv7I4C5d4k+YY4vrnhCD+P23RQ==%27%2Cnecessary:true%2Cpreferences:true%2Cstatistics:true%2Cmarketing:true%2Cmethod:%27explicit%27%2Cver:1%2Cutc:1672331618201%2Cregion:%27de%27}', domain: 'www.gog.com', path: '/' }]); // to not waste screen space when non-headless
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
const signIn = page.locator('a:has-text("Sign in")').first();
await Promise.any([signIn.waitFor(), page.waitForSelector('#menuUsername')]);
while (await signIn.isVisible()) {
console.error('Not signed in anymore.');
await signIn.click();
// it then creates an iframe for the login
await page.waitForSelector('#GalaxyAccountsFrameContainer iframe');
const iframe = page.frameLocator('#GalaxyAccountsFrameContainer iframe');
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log in
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
if (cfg.gog_email && cfg.gog_password) console.info('Using email 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 email = cfg.gog_email || await prompt({ message: 'Enter email' });
const password = email && (cfg.gog_password || await prompt({ type: 'password', message: 'Enter password' }));
if (email && password) {
iframe.locator('a[href="/logout"]').click().catch(_ => { }); // Click 'Change account' (email from previous login is set in some cookie)
await iframe.locator('#login_username').fill(email);
await iframe.locator('#login_password').fill(password);
await iframe.locator('#login_login').click();
const handleTwoFactor = async () => {
try {
await iframe.locator('form[name=second_step_authentication]').waitFor({ timeout: 15000 });
console.log('Two-Step Verification - Enter security code');
console.log(await iframe.locator('.form__description').innerText());
const otp = await prompt({ type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 4 || 'The code must be 4 digits!' }); // can't use type: 'number' since it strips away leading zeros and codes sometimes have them
await iframe.locator('#second_step_authentication_token_letter_1').pressSequentially(otp.toString(), { delay: 10 });
await iframe.locator('#second_step_authentication_send').click();
await page.waitForTimeout(1000);
} catch {
return;
}
};
const watchInvalidCaptcha = async () => {
try {
await iframe.locator('text=Invalid captcha').waitFor({ timeout: 15000 });
console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.');
notify('gog: got captcha during login. Please check.');
} catch {
return;
}
};
handleTwoFactor();
watchInvalidCaptcha();
await page.waitForSelector('#menuUsername');
} else {
console.log('Waiting for you to login in the browser.');
await notify('gog: no longer signed in and not enough options set for automatic login.');
if (cfg.headless) {
console.log('Run `SHOW=1 node gog` to login in the opened browser.');
await context.close();
process.exit(1);
}
}
await page.waitForSelector('#menuUsername');
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
}
user = await page.locator('#menuUsername').first().textContent(); // innerText is uppercase due to styling!
console.log(`Signed in as ${user}`);
db.data[user] ||= {};
const banner = page.locator('#giveaway');
const hasGiveaway = await banner.count();
if (!hasGiveaway) {
console.log('Currently no free giveaway!');
} else {
const text = await page.locator('.giveaway__content-header').innerText();
const match_all = text.match(/Claim (.*) and don't miss the|Success! (.*) was added to/);
const title = match_all[1] ? match_all[1] : match_all[2];
const url = await banner.locator('a').first().getAttribute('href');
console.log(`Current free game: ${chalk.blue(title)} - ${url}`);
db.data[user][title] ||= { title, time: datetime(), url };
if (cfg.dryrun) process.exit(1);
await banner.screenshot({ path: screenshot(`${filenamify(title)}.png`) }); // overwrites every time - only keep first?
// instead of clicking the button, visit the auto-claim URL which gives a JSON response
await page.goto('https://www.gog.com/giveaway/claim');
const response = await page.innerText('body');
// console.log(response);
// {} // when successfully claimed
// {"message":"Already claimed"}
// {"message":"Unauthorized"}
// {"message":"Giveaway has ended"}
let status;
if (response == '{}') {
status = 'claimed';
console.log(' Claimed successfully!');
} else {
const message = JSON.parse(response).message;
if (message == 'Already claimed') {
status = 'existed'; // same status text as for epic-games
console.log(' Already in library! Nothing to claim.');
} else {
console.log(response);
status = message;
}
}
db.data[user][title].status ||= status;
notify_games.push({ title, url, status });
if (status == 'claimed' && !cfg.gog_newsletter) {
console.log('Unsubscribe from \'Promotions and hot deals\' newsletter');
await page.goto('https://www.gog.com/en/account/settings/subscriptions');
await page.locator('li:has-text("Marketing communications through Trusted Partners") label').uncheck();
await page.locator('li:has-text("Promotions and hot deals") label').uncheck();
}
}
} catch (error) {
process.exitCode ||= 1;
console.error('--- Exception:');
console.error(error); // .toString()?
if (error.message && process.exitCode != 130) notify(`gog 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(`gog (${user}):<br>${html_game_list(notify_games)}`);
}
}
if (page.video()) console.log('Recorded video:', await page.video().path());
await context.close();

View file

@ -2,8 +2,8 @@
"compilerOptions": {
"checkJs": true,
"target": "es2021",
"module": "esnext",
"moduleResolution": "node"
"module": "NodeNext",
"moduleResolution": "NodeNext", // https://github.com/typicode/lowdb/issues/554
},
"exclude": ["node_modules", "**/node_modules"]
}

4160
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,37 @@
{
"name": "free-games-claimer",
"version": "1.4.0",
"description": "Automatically claims free games on the Epic Games Store, Amazon Prime Gaming and GOG.",
"homepage": "https://github.com/vogler/free-games-claimer",
"repository": {
"type": "git",
"url": "https://github.com/vogler/free-games-claimer.git"
},
"author": "Ralf Vogler",
"license": "AGPL-3.0-only",
"main": "index.js",
"scripts": {
"login": "npx playwright open --save-storage=auth.json https://www.epicgames.com/login",
"codegen": "npx playwright codegen --load-storage=auth.json https://www.epicgames.com/store/en-US/free-games",
"docker:build": "docker build --tag free-games-claimer .",
"docker:epic-games": "cross-env-shell docker run --rm -it -p 5900:5900 -p 6080:6080 -v \\\"$INIT_CWD/data\\\":/fgc/data --name free-games-claimer free-games-claimer"
"docker:build": "docker build . -t ghcr.io/vogler/free-games-claimer",
"docker": "cross-env-shell docker run --rm -it -p 5900:5900 -p 6080:6080 -v \\\"$INIT_CWD/data\\\":/fgc/data --name fgc ghcr.io/vogler/free-games-claimer",
"lint": "npx eslint ."
},
"type": "module",
"engines": {
"node": ">=17"
},
"dependencies": {
"chalk": "^5.4.1",
"cross-env": "^7.0.3",
"dotenv": "^16.5.0",
"enquirer": "^2.4.1",
"fingerprint-injector": "^2.1.66",
"lowdb": "^7.0.1",
"otplib": "^12.0.1",
"playwright-firefox": "^1.52.0",
"puppeteer-extra-plugin-stealth": "^2.11.2"
},
"devDependencies": {
"@playwright/test": "^1.25.1",
"cross-env": "^7.0.3",
"lowdb": "^3.0.0",
"playwright": "^1.25.1",
"puppeteer-extra-plugin-stealth": "^2.11.1"
},
"type": "module"
"@stylistic/eslint-plugin-js": "^4.2.0",
"eslint": "^9.26.0"
}
}

View file

@ -1,148 +1,724 @@
import { chromium } from 'playwright'; // stealth plugin needs no outdated playwright-extra
import path from 'path';
import { dirs, jsonDb, datetime, stealth, filenamify } from './util.js';
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
import { authenticator } from 'otplib';
import chalk from 'chalk';
import { resolve, jsonDb, datetime, stealth, filenamify, prompt, confirm, notify, html_game_list, handleSIGINT } from './src/util.js';
import { cfg } from './src/config.js';
const debug = process.env.PWDEBUG == '1'; // runs headful and opens https://playwright.dev/docs/inspector
const show = process.argv.includes('show', 2);
const headless = !debug && !show;
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'prime-gaming', ...a);
// const URL_LOGIN = 'https://www.amazon.de/ap/signin'; // wrong. needs some session args to be valid?
const URL_CLAIM = 'https://gaming.amazon.com/home';
const TIMEOUT = 20 * 1000; // 20s, default is 30s
const URL_CLAIM = 'https://luna.amazon.com/claims/home';
const db = await jsonDb('prime-gaming.json');
db.data ||= { claimed: [], runs: [] };
const run = {
startTime: datetime(),
endTime: null,
n_internal: null, // unclaimed games at beginning
c_internal: 0, // claimed games at end
n_external: null,
c_external: 0,
};
console.log(datetime(), 'started checking prime-gaming');
const db = await jsonDb('prime-gaming.json', {});
// https://playwright.dev/docs/auth#multi-factor-authentication
const context = await chromium.launchPersistentContext(dirs.browser, {
// channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge, chrome will not work on arm64 linux, only chromium which is the default
headless,
viewport: { width: 1280, height: 1280 },
locale: "en-US", // ignore OS locale to be sure to have english text for locators
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
viewport: { width: cfg.width, height: cfg.height },
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/pg-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
});
// TODO test if needed
handleSIGINT(context);
await stealth(context);
if (!debug) context.setDefaultTimeout(TIMEOUT);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
// const page = /* context.pages().length ? context.pages[0] : */ await context.newPage();
const page = context.pages()[0];
console.debug('userAgent:', await page.evaluate(() => navigator.userAgent));
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
const clickIfExists = async selector => {
if (await page.locator(selector).count() > 0 && await page.locator(selector).isVisible())
await page.click(selector);
const notify_games = [];
let user;
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(() => {});
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();
return true;
};
try {
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever
// need to wait for some elements to exist before checking if signed in or accepting cookies:
await Promise.any(['button:has-text("Sign in")', '[data-a-target="user-dropdown-first-name-text"]'].map(s => page.waitForSelector(s)));
await clickIfExists('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")'); // to not waste screen space in --debug
while (await page.locator('button:has-text("Sign in")').count() > 0) {
console.error('Not signed in anymore.');
if (headless) {
console.log('Please run `node prime-gaming show` to login in the opened browser.');
await context.close(); // not needed?
process.exit(1);
}
await page.click('button:has-text("Sign in")');
if (!debug) context.setDefaultTimeout(0); // give user time to log in without timeout
await page.waitForNavigation({ url: 'https://gaming.amazon.com/home?signedIn=true' });
if (!debug) context.setDefaultTimeout(TIMEOUT);
}
console.log('Signed in.');
await page.click('button[data-type="Game"]');
const games_sel = 'div[data-a-target="offer-list-FGWP_FULL"]';
await page.waitForSelector(games_sel);
console.log('Number of already claimed games (total):', await page.locator(`${games_sel} p:has-text("Collected")`).count());
const game_sel = `${games_sel} [data-a-target="item-card"]:has-text("Claim game")`;
run.n_internal = await page.locator(game_sel).count();
console.log('Number of free unclaimed games (Prime Gaming):', run.n_internal);
const games = await page.$$(game_sel);
// for (let i=1; i<=n; i++) {
for (const card of games) {
// const card = page.locator(`:nth-match(${game_sel}, ${i})`); // this will reevaluate after games are claimed and index will be wrong
// const title = await card.locator('h3').first().innerText();
const title = await (await card.$('.item-card-details__body__primary')).innerText();
console.log('Current free game:', title);
// const img = await (await card.$('img.tw-image')).getAttribute('src');
// console.log('Image:', img);
const p = path.resolve(dirs.screenshots, 'prime-gaming', 'internal', `${filenamify(title)}.png`);
await card.screenshot({ path: p });
await (await card.$('button:has-text("Claim game")')).click();
db.data.claimed.push({ title, time: datetime(), store: 'internal' });
run.c_internal++;
// await page.pause();
}
// claim games in external/linked stores. Linked: origin.com, epicgames.com; Redeem-key: gog.com, legacygames.com
let n;
const game_sel_ext = `${games_sel} [data-a-target="item-card"]:has(p:text-is("Claim"))`;
do {
n = await page.locator(game_sel_ext).count();
run.n_external ||= n;
console.log('Number of free unclaimed games (external stores):', n);
const card = await page.$(game_sel_ext);
if (!card) break;
const title = await (await card.$('.item-card-details__body__primary')).innerText();
console.log('Current free game:', title);
await (await card.$('text=Claim')).click();
// await page.waitForNavigation();
await Promise.any([page.click('button:has-text("Claim now")'), page.click('button:has-text("Complete Claim")')]); // waits for navigation
const store_text = await (await page.$('[data-a-target="hero-header-subtitle"]')).innerText();
// Full game for PC [and MAC] on: gog.com, Origin, Legacy Games, EPIC GAMES, Battle.net
// 3 Full PC Games on Legacy Games
const store = store_text.toLowerCase().replace(/.* on /, '');
console.log('External store:', store);
if (await page.locator('div:has-text("Link game account")').count()) {
console.error('Account linking is required to claim this offer!');
const handleDirectLoginPage = async () => {
if (!page.url().includes('/ap/signin')) return false;
console.log('On Amazon login page (redirect). Trying to sign in automatically.');
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout);
const email = cfg.pg_email || await prompt({ message: 'Enter email' });
const password = email && (cfg.pg_password || await prompt({ type: 'password', message: 'Enter password' }));
if (email && password) {
await page.fill('[name=email]', email);
await page.click('input[type="submit"]');
await page.fill('[name=password]', password);
await page.click('input[type="submit"]');
await handleMFA(page).catch(() => {});
try {
await page.waitForURL('**/ap/signin**');
const error = await page.locator('.a-alert-content').first().innerText();
if (error.trim().length) {
console.error('Login error:', error);
await notify(`prime-gaming: login: ${error}`);
await context.close(); // finishes potential recording
process.exit(1);
}
} catch {
// if navigation succeeded, continue
}
await page.waitForURL(/luna\.amazon\.com\/claims\/.*signedIn=true/);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
return true;
} else {
console.log('Waiting for manual login on redirect page.');
if (cfg.headless) {
console.log('Run `SHOW=1 node prime-gaming` to login in the opened browser.');
await context.close(); // finishes potential recording
process.exit(1);
}
return true;
}
};
await handleDirectLoginPage();
// need to wait for some elements to exist before checking if signed in or accepting cookies:
await Promise.any([
'button:has-text("Sign in")',
'button:has-text("Anmelden")',
'[data-a-target="user-dropdown-first-name-text"]',
'[data-testid="user-dropdown-first-name-text"]',
].map(s => page.waitForSelector(s)));
try {
await page.click('[aria-label="Cookies usage disclaimer banner"] button:has-text("Accept Cookies")'); // to not waste screen space when non-headless; could be flaky
} catch {
// ignore if banner not present
}
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")');
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log in
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
if (cfg.pg_email && cfg.pg_password) console.info('Using email 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 email = cfg.pg_email || await prompt({ message: 'Enter email' });
const password = email && (cfg.pg_password || await prompt({ type: 'password', message: 'Enter password' }));
if (email && password) {
await page.fill('[name=email]', email);
await page.click('input[type="submit"]');
await page.fill('[name=password]', password);
await page.click('input[type="submit"]');
try {
await page.waitForURL('**/ap/signin**');
const error = await page.locator('.a-alert-content').first().innerText();
if (error.trim().length) {
console.error('Login error:', error);
await notify(`prime-gaming: login: ${error}`);
await context.close(); // finishes potential recording
process.exit(1);
}
} catch {
// navigation ok
}
try {
await handleMFA(page);
} catch {
// ignore MFA watcher errors
}
} 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.');
if (cfg.headless) {
console.log('Run `SHOW=1 node prime-gaming` to login in the opened browser.');
await context.close(); // finishes potential recording
process.exit(1);
}
}
await page.waitForURL('https://gaming.amazon.com/home?signedIn=true');
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
}
user = await page.locator('[data-a-target="user-dropdown-first-name-text"], [data-testid="user-dropdown-first-name-text"]').first().innerText();
console.log(`Signed in as ${user}`);
db.data[user] ||= {};
if (await page.getByRole('button', { name: 'Try Prime' }).count()) {
console.error('User is currently not an Amazon Prime member, so no games to claim. Exit!');
await context.close();
process.exit(1);
}
const waitUntilStable = async (f, act) => {
let v;
while (true) {
const v2 = await f();
console.log('waitUntilStable', v2);
if (v == v2) break;
v = v2;
await act();
}
};
const scrollUntilStable = async f => await waitUntilStable(f, async () => {
// await page.keyboard.press('End'); // scroll to bottom to show all games
// loading all games became flaky; see https://github.com/vogler/free-games-claimer/issues/357
await page.keyboard.press('PageDown'); // scrolling to straight to the bottom started to skip loading some games
await page.waitForLoadState('networkidle'); // wait for all games to be loaded
await page.waitForTimeout(3000); // extra wait to load all already collected games
// do it again since once wasn't enough...
await page.keyboard.press('PageDown');
await page.waitForTimeout(3000);
});
const openGamesTab = async () => {
const selectors = [
'button[data-type="Game"]', // old layout
'button:has-text("Games")',
'button:has-text("Games einlösen")',
'[data-test-selector="category-picker"] button:has-text("Games")',
'[data-testid="category-picker"] button:has-text("Games")',
];
for (const sel of selectors) {
const btn = page.locator(sel).first();
if (await btn.count()) {
await btn.click();
return;
}
}
// New Luna claims home: try the filter/CTA button with embedded <p title="Games einlösen">
const gamesTitle = page.locator('p.offer-filters__button__title:has-text("Games"), p.offer-filters__button__title:has-text("einlösen")');
if (await gamesTitle.count()) {
const btn = gamesTitle.first().locator('xpath=ancestor::button[1]');
if (await btn.count()) {
await btn.click();
return;
}
}
// New Luna claims home already shows games list
};
await openGamesTab();
const locateGamesList = async () => {
const selectors = [
'div[data-a-target="offer-list-FGWP_FULL"]', // old layout
'[data-testid="offer-list"]',
'[data-test-selector="offer-list"]',
'section:has(h2:has-text("Games with Prime"))',
'section:has(h2:has-text("Games"))',
];
for (const sel of selectors) {
const loc = page.locator(sel).first();
if (await loc.count()) return loc;
}
return null;
};
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(() => globalThis.document?.scrollingElement?.scrollHeight ?? 0));
const normalizeClaimUrl = url => {
if (!url) return { url, key: null };
const m = url.match(/(https?:\/\/[^/]+)?(\/claims\/[^?#]+)/);
if (!m) return { url, key: url };
const path = m[2];
const slug = path.split('/')[2];
return { url: 'https://luna.amazon.com' + path, key: slug || path };
};
const cards = [];
// New layout: direct claim buttons on cards (FGWPOffer) with "Spiel aktivieren"/"Claim"
const fgwps = page.locator('[data-a-target="FGWPOffer"]');
if (await fgwps.count()) {
for (const handle of await fgwps.elementHandles()) {
const href = await handle.getAttribute('href');
const { url, key } = normalizeClaimUrl(href?.startsWith('/') ? href : href || '');
const title =
await handle.$eval('p[title], span[title]', el => el.getAttribute('title')).catch(() => null) ||
await handle.$eval('p, span', el => el.textContent).catch(() => null) ||
key ||
'Unknown title';
cards.push({ kind: 'external', title, url, key });
}
}
const anchorClaims = page.locator('a[href*="/claims/"][href*="amzn1.pg.item"]');
if (await anchorClaims.count()) {
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';
cards.push({ kind: 'external', title, url, key });
}
}
if (!cards.length && games) {
const cardLocator = games.locator([
'[data-testid="offer-card"]',
'[data-test-selector="offer-card"]',
'.item-card__action',
].join(','));
if (await cardLocator.count() === 0) {
console.log('No games found in list.');
} else {
for (const handle of await cardLocator.elementHandles()) {
const text = (await handle.textContent() || '').toLowerCase();
if (text.includes('collected')) continue; // skip already claimed
const title = await (await handle.$('h3, h4, [data-testid="item-card-title"], [data-test-selector="item-card-title"], .item-card-details__body__primary'))?.innerText() || 'Unknown title';
const linkEl = await handle.$('a[href]');
let url = linkEl && await linkEl.getAttribute('href');
if (url?.startsWith('/')) url = 'https://luna.amazon.com' + url;
const { url: normUrl, key } = normalizeClaimUrl(url);
const hasLinkClaim = await handle.$('a:has-text("Claim"), a:has-text("Get"), a:has-text("Details")');
const hasButtonClaim = await handle.$('button:has-text("Claim"), button:has-text("Get"), button:has-text("Get game"), button:has-text("Play")');
const hasGermanCTA = await handle.$(':is(button,p,a):has-text("Spiel aktivieren"), :is(button,p,a):has-text("Spiel holen")');
if (hasLinkClaim || hasGermanCTA) cards.push({ kind: 'external', title, url: normUrl, key });
else if (hasButtonClaim) cards.push({ kind: 'internal', title, url: normUrl, key, handle });
}
}
}
// dedup by URL to avoid duplicates from multiple selectors
const seenUrl = new Set();
const uniq = cards.filter(c => {
const key = c.key || c.url || c.title;
if (seenUrl.has(key)) return false;
seenUrl.add(key);
return true;
});
const internal = uniq.filter(c => c.kind == 'internal');
const external = uniq.filter(c => c.kind == 'external');
// bottom to top: oldest to newest games
internal.reverse();
external.reverse();
const sameOrNewPage = async url => {
const isNew = page.url() != url;
let p = page;
if (isNew) {
p = await context.newPage();
await p.goto(url, { waitUntil: 'domcontentloaded' });
}
return [p, isNew];
};
const skipBasedOnTime = async url => {
const [p, isNew] = await sameOrNewPage(url);
const dueDateLoc = p.locator('.availability-date .tw-bold, [data-testid="availability-end-date"], [data-test-selector="availability-end-date"]');
if (!await dueDateLoc.count()) {
if (isNew) await p.close();
return false;
}
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 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) {
await card.handle.scrollIntoViewIfNeeded();
const title = card.title;
const url = card.url;
console.log('Current free game:', chalk.blue(title));
if (cfg.pg_timeLeft && url && await skipBasedOnTime(url)) continue;
if (cfg.dryrun) continue;
if (cfg.interactive) {
const confirmed = await confirm();
if (!confirmed) continue;
}
await card.handle.locator('.tw-button:has-text("Claim"), .tw-button:has-text("Get"), button:has-text("Claim"), button:has-text("Get")').first().click();
db.data[user][title] ||= { title, time: datetime(), url, store: 'internal' };
notify_games.push({ title, status: 'claimed', url });
await card.handle.screenshot({ path: screenshot('internal', `${filenamify(title)}.png`) });
}
console.log('\nNumber of free unclaimed games (external stores):', external.length);
// claim games in external/linked stores. Linked: origin.com, epicgames.com; Redeem-key: gog.com, legacygames.com, microsoft
const external_info = [];
for (const card of external) { // need to get data incl. URLs in this loop and then navigate in another, otherwise .all() would update after coming back and .elementHandles() like above would lead to error due to page navigation: elementHandle.$: Protocol error (Page.adoptNode)
const title = card.title;
const url = card.url ? card.url.split('?')[0] : undefined;
if (!url) continue;
external_info.push({ title, url });
}
const clickCTA = async p => {
const candidates = [
p.locator('button[data-a-target="buy-box_call-to-action"]').first(),
p.locator('[data-a-target="buy-box_call-to-action"]').first(),
p.locator('[data-a-target="buy-box"] .tw-button:has-text("Get game")').first(),
p.locator('[data-a-target="buy-box"] .tw-button:has-text("Claim")').first(),
p.locator('.tw-button:has-text("Complete Claim")').first(),
p.locator('[data-a-target="buy-box_call-to-action-text"]').first().locator('xpath=ancestor::button[1]'),
p.locator('.tw-button:has-text("Spiel holen"), .tw-button:has-text("Spiel aktivieren")').first(),
p.locator('p:has-text("Spiel holen"), p:has-text("Spiel aktivieren")').first().locator('xpath=ancestor::button[1]'),
];
for (const c of candidates) {
if (await c.count()) {
try {
await c.waitFor({ state: 'visible', timeout: 5000 });
const enabled = await c.isEnabled();
if (enabled) await c.click();
else {
await c.evaluate(el => {
el.disabled = false;
el.removeAttribute('disabled');
el.click();
});
}
return true;
} catch {
// try next candidate
}
}
}
return false;
};
for (const { title, url } of external_info) {
console.log('Current free game:', chalk.blue(title)); // , url);
const existingStatus = db.data[user]?.[title]?.status;
if (existingStatus && !existingStatus.startsWith('failed')) {
console.log(` Already recorded as ${existingStatus}, skipping.`);
notify_games.push({ title, url, status: 'existed' });
continue;
}
await page.goto(url, { waitUntil: 'domcontentloaded' });
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"]');
if (await detailLoc.count()) {
const item_text = await detailLoc.first().innerText();
const lower = item_text.toLowerCase();
const onPos = lower.lastIndexOf(' on ');
if (onPos >= 0) store = lower.slice(onPos + 4).replace(/[.!]$/, '');
} else if (url.includes('/claims/')) {
const slug = url.split('/claims/')[1]?.split('/')[0] || '';
if (slug.includes('gog')) store = 'gog.com';
else if (slug.includes('epic')) store = 'epic-games';
else if (slug.includes('origin')) store = 'origin';
else if (slug.includes('xbox') || slug.includes('microsoft')) store = 'microsoft store';
else if (slug.includes('legacy')) store = 'legacy games';
const lunaPlay = await page.locator('[data-a-target="LunaOffer"], button[data-a-target="LunaOffer"], button:has-text("Spielen")').count();
if (store == 'unknown' && lunaPlay) store = 'luna';
}
console.log(' External store:', store);
const notify_game = { title, url };
notify_games.push(notify_game); // status is updated below
// Already collected? skip
const collectedLoc = page.locator('[data-a-target="ClaimStateQuantityAndDateContent"], [data-a-target="ClaimStateClaimCodeContent"]:has-text("Collected"), [data-a-target="ClaimStateVendorContent"], [data-a-target="ClaimStateViewDetailsAndInstructions"]');
const collectedText = page.getByText(/You collected this on/i);
const collectedEpic = page.getByText(/Sent to your Epic Games Store library/i);
const collectedBanner = page.locator('p.tw-c-text-alert-success:has-text("Collected"), p.tw-c-text-alert-success:has-text("Collected this")');
const collectedSuccessIcon = page.locator('[data-a-target="ItemCardDetailSuccessStatus"], .claim-state__success-icon');
const disabledCTA = page.locator('[data-a-target="buy-box_call-to-action"][disabled], button[disabled]:has-text("Get game")');
const collectedAny = await Promise.all([
collectedLoc.count(),
collectedBanner.count(),
collectedText.count(),
collectedEpic.count(),
collectedSuccessIcon.count(),
disabledCTA.count(),
]).then(([a, b, c, d, e, f]) => a + b + c + d + e + f > 0);
if (collectedAny) {
console.log(' Already collected, skipping.');
notify_game.status = 'existed';
db.data[user][title] ||= { title, time: datetime(), url, store, status: 'existed' };
continue;
}
// Disabled CTA (e.g., needs linking or not available)
if (await disabledCTA.count()) {
if (store !== 'epic-games') {
console.log(' CTA is disabled, skipping (likely needs linking/not available).');
notify_game.status = 'disabled';
db.data[user][title] ||= { title, time: datetime(), url, store, status: 'disabled' };
continue;
} else {
console.log(' CTA disabled for epic-games, will still try to link/claim.');
}
}
if (store == 'luna') {
console.log(' Luna cloud title detected, skipping code redemption.');
notify_game.status = 'luna (play)';
db.data[user][title] ||= { title, time: datetime(), url, store: 'luna', status: 'luna (play)' };
continue;
}
if (cfg.pg_timeLeft && await skipBasedOnTime(url)) continue;
if (cfg.dryrun) continue;
if (cfg.interactive) {
const confirmed = await confirm();
if (!confirmed) 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(() => {});
db.data[user][title] ||= { title, time: datetime(), url, store };
if (await page.locator('div:has-text("Link game account")').count() // epic games store also shows "Link account"
|| await page.locator('div:has-text("Link account")').count()) {
console.error(' Account linking is required to claim this offer!');
notify_game.status = `failed: need account linking for ${store}`;
db.data[user][title].status = 'failed: need account linking';
// await page.pause();
// await page.click('[data-a-target="LinkAccountModal"] [data-a-target="LinkAccountButton"]');
// login for epic games also needed if already logged in
// wait for https://www.epicgames.com/id/authorize?redirect_uri=https%3A%2F%2Fservice.link.amazon.gg...
// await page.click('button[aria-label="Allow"]');
} else {
db.data[user][title].status = 'claimed';
// print code if there is one
const redeem = {
// 'origin': 'https://www.origin.com/redeem', // TODO still needed or now only via account linking?
// 'origin': 'https://www.origin.com/redeem', // kept for legacy flows; current path uses account linking
'gog.com': 'https://www.gog.com/redeem',
'microsoft store': 'https://account.microsoft.com/billing/redeem',
xbox: 'https://account.microsoft.com/billing/redeem',
'legacy games': 'https://www.legacygames.com/primedeal',
};
let code;
if (store in redeem) { // did not work for linked origin: && !await page.locator('div:has-text("Successfully Claimed")').count()
code = await page.inputValue('input[type="text"]');
console.log('Code to redeem game:', code);
if (store == 'legacy games') { // may be different URL like https://legacygames.com/primeday/puzzleoftheyear/
redeem[store] = await (await page.$('li:has-text("Click here") a')).getAttribute('href');
let code;
try {
// ensure CTA was clicked in case code is behind it
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 {
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(() => {});
continue;
}
console.log('URL to redeem game:', redeem[store]);
console.log(' Code to redeem game:', chalk.blue(code));
if (store == 'legacy games') { // may be different URL like https://legacygames.com/primeday/puzzleoftheyear/
redeem[store] = await (await page.$('li:has-text("Click here") a')).getAttribute('href'); // full text: Click here to enter your redemption code.
}
let redeem_url = redeem[store];
if (store == 'gog.com') redeem_url += '/' + code; // to log and notify, but can't use for goto below (captcha)
console.log(' URL to redeem game:', redeem_url);
db.data[user][title].code = code;
let redeem_action = 'redeem';
if (cfg.pg_redeem) { // try to redeem keys on external stores
console.log(` Trying to redeem ${code} on ${store} (need to be logged in)!`);
const page2 = await context.newPage();
await page2.goto(redeem[store], { waitUntil: 'domcontentloaded' });
if (store == 'gog.com') {
await page2.fill('#codeInput', code);
const r1 = page2.waitForResponse(r => r.request().method() == 'GET' && r.url().startsWith('https://redeem.gog.com/'));
await page2.click('[type="submit"]'); // click Continue
const r1t = await (await r1).text();
const reason = JSON.parse(r1t).reason;
// {"reason":"Invalid or no captcha"}
// {"reason":"code_used"}
// {"reason":"code_not_found"}
if (reason?.includes('captcha')) {
redeem_action = 'redeem (got captcha)';
console.error(' Got captcha; could not redeem!');
} else if (reason == 'code_used') {
redeem_action = 'already redeemed';
console.log(' Code was already used!');
} else if (reason == 'code_not_found') {
redeem_action = 'redeem (not found)';
console.error(' Code was not found!');
} else { // unknown state; keep info log for later analysis
redeem_action = 'redeemed?';
console.debug(` Response 1: ${r1t}`);
const r2 = page2.waitForResponse(r => r.request().method() == 'POST' && r.url().startsWith('https://redeem.gog.com/'));
await page2.click('[type="submit"]'); // click Redeem
const r2t = await (await r2).text();
const reason2 = JSON.parse(r2t).reason;
if (r2t == '{}') {
redeem_action = 'redeemed';
console.log(' Redeemed successfully.');
db.data[user][title].status = 'claimed and redeemed';
} else if (reason2?.includes('captcha')) {
redeem_action = 'redeem (got captcha)';
console.error(' Got captcha; could not redeem!');
} else {
console.debug(` Response 2: ${r2t}`);
console.log(' Unknown Response 2 - please report in https://github.com/vogler/free-games-claimer/issues/5');
}
}
} else if (store == 'microsoft store' || store == 'xbox') {
console.error(` Redeem on ${store} is experimental!`);
if (page2.url().startsWith('https://login.')) {
console.error(' Not logged in! Please redeem the code above manually. You can now login in the browser for next time. Waiting for 60s.');
await page2.waitForTimeout(60 * 1000);
redeem_action = 'redeem (login)';
} else {
const iframe = page2.frameLocator('#redeem-iframe');
const input = iframe.locator('[name=tokenString]');
await input.waitFor();
await input.fill(code);
const r = page2.waitForResponse(r => r.url().startsWith('https://cart.production.store-web.dynamics.com/v1.0/Redeem/PrepareRedeem'));
const rt = await (await r).text();
const j = JSON.parse(rt);
const reason = j?.events?.cart.length && j.events.cart[0]?.data?.reason;
if (reason == 'TokenNotFound') {
redeem_action = 'redeem (not found)';
console.error(' Code was not found!');
} else if (j?.productInfos?.length && j.productInfos[0]?.redeemable) {
await iframe.locator('button:has-text("Next")').click();
await iframe.locator('button:has-text("Confirm")').click();
const r = page2.waitForResponse(r => r.url().startsWith('https://cart.production.store-web.dynamics.com/v1.0/Redeem/RedeemToken'));
const j = JSON.parse(await (await r).text());
if (j?.events?.cart.length && j.events.cart[0]?.data?.reason == 'UserAlreadyOwnsContent') {
redeem_action = 'already redeemed';
console.error(' error: UserAlreadyOwnsContent');
} else { // success path not seen yet; log below if needed
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');
}
} else { // other responses; keep info log for analysis
redeem_action = 'unknown';
console.debug(` Response: ${rt}`);
console.log(' Redeemed successfully? Please report your Response from above (if it is new) in https://github.com/vogler/free-games-claimer/issues/5');
}
}
} else if (store == 'legacy games') {
await page2.fill('[name=coupon_code]', code);
await page2.fill('[name=email]', cfg.lg_email);
await page2.fill('[name=email_validate]', cfg.lg_email);
await page2.uncheck('[name=newsletter_sub]');
await page2.click('[type="submit"]');
try {
await page2.waitForSelector('h2:has-text("Thanks for redeeming")');
redeem_action = 'redeemed';
db.data[user][title].status = 'claimed and redeemed';
} catch (error) {
console.error(' Got error', error);
redeem_action = 'redeemed?';
db.data[user][title].status = 'claimed and redeemed?';
console.log(' Redeemed successfully? Please report problems in https://github.com/vogler/free-games-claimer/issues/5');
}
} else {
console.error(` Redeem on ${store} not yet implemented!`);
}
if (cfg.debug) await page2.pause();
await page2.close();
}
notify_game.status = `<a href="${redeem_url}">${redeem_action}</a> ${code} on ${store}`;
} else {
notify_game.status = `claimed on ${store}`;
db.data[user][title].status = 'claimed';
}
db.data.claimed.push({ title, time: datetime(), store, code, url: page.url() });
// save screenshot of potential code just in case
const p = path.resolve(dirs.screenshots, 'prime-gaming', 'external', `${filenamify(title)}.png`);
await page.screenshot({ path: p, fullPage: true });
console.info('Saved a screenshot of page to', p);
run.c_external++;
await page.screenshot({ path: screenshot('external', `${filenamify(title)}.png`), fullPage: true });
}
// await page.pause();
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
}
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
try {
await page.click('button[data-type="Game"]');
} while (n);
const p = path.resolve(dirs.screenshots, 'prime-gaming', `${filenamify(datetime())}.png`);
// await page.screenshot({ path: p, fullPage: true });
await page.locator(games_sel).screenshot({ path: p });
} catch (error) {
console.error(error);
run.error = error.toString();
} finally {
// write out json db
run.endTime = datetime();
db.data.runs.push(run);
await db.write();
} catch {
// ignore if filter already selected
}
await context.close();
if (notify_games.length && games) { // make screenshot of all games if something was claimed and list exists
const p = screenshot(`${filenamify(datetime())}.png`);
await scrollUntilStable(() => games.locator('.item-card__action').count());
const viewportSize = page.viewportSize(); // current viewport size
await page.setViewportSize({ ...viewportSize, height: 3000 }); // increase height, otherwise element screenshot is cut off at the top and bottom
await games.screenshot({ path: p }); // screenshot of all claimed games
}
// https://github.com/vogler/free-games-claimer/issues/55
if (cfg.pg_claimdlc) {
console.log('Trying to claim in-game content...');
await page.click('button[data-type="InGameLoot"]');
const loot = page.locator('div[data-a-target="offer-list-IN_GAME_LOOT"]');
await loot.waitFor();
process.stdout.write('Loading all DLCs on page...');
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());
const cards = await loot.locator('[data-a-target="item-card"]:has(p:text-is("Claim"))').all();
console.log('Number of unclaimed DLC:', cards.length);
const dlcs = await Promise.all(cards.map(async card => ({
game: await card.locator('.item-card-details__body p').innerText(),
title: await card.locator('.item-card-details__body__primary').innerText(),
url: 'https://gaming.amazon.com' + await card.locator('a').first().getAttribute('href'),
})));
// console.log(dlcs);
const dlc_unlinked = {};
for (const dlc of dlcs) {
const title = `${dlc.game} - ${dlc.title}`;
const url = dlc.url;
console.log('Current DLC:', title);
if (cfg.debug) await page.pause();
if (cfg.dryrun) continue;
if (cfg.interactive && !await confirm()) continue;
db.data[user][title] ||= { title, time: datetime(), store: 'DLC', status: 'failed: need account linking' };
const notify_game = { title, url };
notify_games.push(notify_game); // status is updated below
try {
await page.goto(url, { waitUntil: 'domcontentloaded' });
// 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?
const claimOptions = [
page.click('.tw-button:has-text("Get in-game content")'),
page.click('.tw-button:has-text("Claim your gift")'),
(async () => {
await page.click('.tw-button:has-text("Claim")');
await page.click('button:has-text("Continue")').catch(() => {});
})(),
];
await Promise.any(claimOptions);
try {
await page.click('button:has-text("Continue")');
} catch {
// continue button not always present
}
const linkAccountButton = page.locator('[data-a-target="LinkAccountButton"]');
let unlinked_store;
if (await linkAccountButton.count()) {
unlinked_store = await linkAccountButton.first().getAttribute('aria-label');
console.debug(' LinkAccountButton label:', unlinked_store);
const match = unlinked_store?.match(/Link (.*) account/);
const extracted = match?.[1];
if (extracted) unlinked_store = extracted;
} else if (await page.locator('text=Link game account').count()) { // epic-games only?
console.error(' Missing account linking (epic-games specific button?):', await page.locator('button[data-a-target="gms-cta"]').innerText()); // track account-linking UI drift
unlinked_store = 'epic-games';
}
if (unlinked_store) {
console.error(' Missing account linking:', unlinked_store, url);
dlc_unlinked[unlinked_store] ??= [];
dlc_unlinked[unlinked_store].push(title);
} else {
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';
}
} catch (error) {
console.error(error);
} finally {
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
await page.click('button[data-type="InGameLoot"]');
}
}
console.log('DLC: Unlinked accounts:', dlc_unlinked);
}
} catch (error) {
process.exitCode ||= 1;
console.error('--- Exception:');
console.error(error); // .toString()?
if (error.message && process.exitCode != 130) notify(`prime-gaming failed: ${error.message.split('\n')[0]}`);
} finally {
await db.write(); // write out json db
if (notify_games.length) { // list should only include claimed games
notify(`prime-gaming (${user}):<br>${html_game_list(notify_games)}`);
}
}
if (page.video()) console.log('Recorded video:', await page.video().path());
await context.close();

9
sonar-project.properties Normal file
View file

@ -0,0 +1,9 @@
sonar.organization=vogler
sonar.projectKey=vogler_free-games-claimer
# relative paths to source directories. More details and properties are described
# in https://sonarcloud.io/documentation/project-administration/narrowing-the-focus/
sonar.sources=.
#Eslint issues
sonar.eslint.reportPaths = eslint_report.json

53
src/config.js Normal file
View file

@ -0,0 +1,53 @@
import * as dotenv from 'dotenv';
import { dataDir } from './util.js';
dotenv.config({ path: 'data/config.env' }); // loads env vars from file - will not set vars that are already set, i.e., can overwrite values from file by prefixing, e.g., VAR=VAL node ...
// Options - also see table in README.md
export const cfg = {
debug: process.env.DEBUG == '1' || process.env.PWDEBUG == '1', // runs non-headless and opens https://playwright.dev/docs/inspector
debug_network: process.env.DEBUG_NETWORK == '1', // log network requests and responses
record: process.env.RECORD == '1', // `recordHar` (network) + `recordVideo`
time: process.env.TIME == '1', // log duration of each step
dryrun: process.env.DRYRUN == '1', // don't claim anything
interactive: process.env.INTERACTIVE == '1', // confirm to claim, default skip
show: process.env.SHOW == '1', // run non-headless
get headless() {
return !this.debug && !this.show;
},
width: Number(process.env.WIDTH) || 1920, // width of the opened browser
height: Number(process.env.HEIGHT) || 1080, // height of the opened browser
timeout: (Number(process.env.TIMEOUT) || 60) * 1000, // default timeout for playwright is 30s
login_timeout: (Number(process.env.LOGIN_TIMEOUT) || 180) * 1000, // higher timeout for login, will wait twice: prompt + wait for manual login
novnc_port: process.env.NOVNC_PORT, // running in docker if set
notify: process.env.NOTIFY, // apprise notification services
notify_title: process.env.NOTIFY_TITLE, // apprise notification title
get dir() { // avoids ReferenceError: Cannot access 'dataDir' before initialization
return {
browser: process.env.BROWSER_DIR || dataDir('browser'), // for multiple accounts or testing
screenshots: process.env.SCREENSHOTS_DIR || dataDir('screenshots'), // set to 0 to disable screenshots
};
},
// auth epic-games
eg_email: process.env.EG_EMAIL || process.env.EMAIL,
eg_password: process.env.EG_PASSWORD || process.env.PASSWORD,
eg_otpkey: process.env.EG_OTPKEY,
eg_parentalpin: process.env.EG_PARENTALPIN,
// auth prime-gaming
pg_email: process.env.PG_EMAIL || process.env.EMAIL,
pg_password: process.env.PG_PASSWORD || process.env.PASSWORD,
pg_otpkey: process.env.PG_OTPKEY,
// auth gog
gog_email: process.env.GOG_EMAIL || process.env.EMAIL,
gog_password: process.env.GOG_PASSWORD || process.env.PASSWORD,
gog_newsletter: process.env.GOG_NEWSLETTER == '1', // do not unsubscribe from newsletter after claiming a game
// auth AliExpress
ae_email: process.env.AE_EMAIL || process.env.EMAIL,
ae_password: process.env.AE_PASSWORD || process.env.PASSWORD,
// OTP only via GOG_EMAIL, can't add app...
// experimmental
pg_redeem: process.env.PG_REDEEM == '1', // prime-gaming: redeem keys on external stores
lg_email: process.env.LG_EMAIL || process.env.PG_EMAIL || process.env.EMAIL, // prime-gaming: external: legacy-games: email to use for redeeming
pg_claimdlc: process.env.PG_CLAIMDLC == '1', // prime-gaming: claim in-game content
pg_timeLeft: Number(process.env.PG_TIMELEFT), // prime-gaming: check time left to claim and skip game if there are more than PG_TIMELEFT days left to claim it
};

32
src/migrate.js Normal file
View file

@ -0,0 +1,32 @@
import { existsSync } from 'node:fs';
import { Low } from 'lowdb';
import { JSONFile } from 'lowdb/node';
import { datetime } from './util.js';
const datetime_UTCtoLocalTimezone = async file => {
if (!existsSync(file)) return console.error('File does not exist:', file);
const db = new Low(new JSONFile(file));
await db.read();
db.data ||= {};
console.log('Migrating', file);
for (const user in db.data) {
for (const game in db.data[user]) {
const time1 = db.data[user][game].time;
const time1s = time1.endsWith('Z') ? time1 : time1 + ' UTC';
const time2 = datetime(new Date(time1s));
console.log([game, time1, time2]);
db.data[user][game].time = time2;
}
}
await db.write(); // write out json db
};
const args = process.argv.slice(2);
if (args[0] == 'localtime') {
const files = args.slice(1);
console.log('Will convert UTC datetime to local timezone for', files);
files.forEach(datetime_UTCtoLocalTimezone);
} else {
console.log('Usage: node migrate.js <cmd> <args>');
console.log(' node migrate.js localtime data/*.json');
}

148
src/util.js Normal file
View file

@ -0,0 +1,148 @@
// https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-js-when-using-es6-modules
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// not the same since these will give the absolute paths for this file instead of for the file using them
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// explicit object instead of Object.fromEntries since the built-in type would loose the keys, better type: https://dev.to/svehla/typescript-object-fromentries-389c
export const dataDir = s => path.resolve(__dirname, '..', 'data', s);
// modified path.resolve to return null if first argument is '0', used to disable screenshots
export const resolve = (...a) => a.length && a[0] == '0' ? null : path.resolve(...a);
// json database
import { JSONFilePreset } from 'lowdb/node';
export const jsonDb = (file, defaultData) => JSONFilePreset(dataDir(file), defaultData);
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
// date and time as UTC (no timezone offset) in nicely readable and sortable format, e.g., 2022-10-06 12:05:27.313
export const datetimeUTC = (d = new Date()) => d.toISOString().replace('T', ' ').replace('Z', '');
// same as datetimeUTC() but for local timezone, e.g., UTC + 2h for the above in DE
export const datetime = (d = new Date()) => datetimeUTC(new Date(d.getTime() - d.getTimezoneOffset() * 60000));
export const filenamify = s => s
.replaceAll(':', '.')
.replaceAll(/[^a-z0-9 _\-.]/gi, '_'); // alternative: https://www.npmjs.com/package/filenamify - On Unix-like systems, / is reserved. On Windows, <>:"/\|?* along with trailing periods are reserved.
export const handleSIGINT = (context = null) => process.on('SIGINT', async () => { // e.g. when killed by Ctrl-C
console.error('\nInterrupted by SIGINT. Exit!'); // Exception shows where the script was:\n'); // killed before catch in docker...
process.exitCode = 130; // 128+SIGINT to indicate to parent that process was killed
if (context) await context.close(); // in order to save recordings also on SIGINT, we need to disable Playwright's handleSIGINT and close the context ourselves
});
export const launchChromium = async options => {
const { chromium } = await import('playwright-chromium'); // stealth plugin needs no outdated playwright-extra
// https://www.nopecha.com extension source from https://github.com/NopeCHA/NopeCHA/releases/tag/0.1.16
// const ext = path.resolve('nopecha'); // used in Chromium, currently not needed in Firefox
const context = chromium.launchPersistentContext(cfg.dir.browser, {
// chrome will not work in linux arm64, only chromium
// channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge
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.'.
...options,
});
return context;
};
export const stealth = async context => {
// stealth with playwright: https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212
// https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions
const enabledEvasions = [
'chrome.app',
'chrome.csi',
'chrome.loadTimes',
'chrome.runtime',
// 'defaultArgs',
'iframe.contentWindow',
'media.codecs',
'navigator.hardwareConcurrency',
'navigator.languages',
'navigator.permissions',
'navigator.plugins',
// 'navigator.vendor',
'navigator.webdriver',
'sourceurl',
// 'user-agent-override', // doesn't work since playwright has no page.browser()
'webgl.vendor',
'window.outerdimensions',
];
const stealth = {
callbacks: [],
async evaluateOnNewDocument(...args) {
this.callbacks.push({ cb: args[0], a: args[1] });
},
};
for (const e of enabledEvasions) {
const evasion = await import(`puppeteer-extra-plugin-stealth/evasions/${e}/index.js`);
evasion.default().onPageCreated(stealth);
}
for (const evasion of stealth.callbacks) {
await context.addInitScript(evasion.cb, evasion.a);
}
};
// used prompts before, but couldn't cancel prompt
// alternative inquirer is big (node_modules 29MB, enquirer 9.7MB, prompts 9.8MB, none 9.4MB) and slower
// open issue: prevents handleSIGINT() to work if prompt is cancelled with Ctrl-C instead of Escape: https://github.com/enquirer/enquirer/issues/372
import Enquirer from 'enquirer'; const enquirer = new Enquirer();
const timeoutHint = () => 'timeout';
const cancelPromptWithHint = prompt => {
prompt.hint = timeoutHint;
prompt.cancel();
};
const applyPromptTimeout = (prompt, timeout) => {
if (!timeout) return;
const timer = setTimeout(cancelPromptWithHint, timeout, prompt);
const clearTimer = () => clearTimeout(timer);
prompt.on('submit', clearTimer);
prompt.on('cancel', clearTimer);
};
// cancel prompt after timeout ms; can be disabled per prompt via options.timeout = 0
const timeoutPlugin = defaultTimeout => enquirerInstance => {
const onPrompt = prompt => applyPromptTimeout(prompt, prompt.options?.timeout ?? defaultTimeout);
enquirerInstance.on('prompt', onPrompt);
};
enquirer.use(timeoutPlugin(cfg.login_timeout));
// 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 confirm = o => prompt({ type: 'confirm', message: 'Continue?', ...o });
// notifications via apprise CLI
import { execFile } from 'node:child_process';
import { cfg } from './config.js';
export const notify = html => new Promise(resolve => {
if (!cfg.notify) {
if (cfg.debug) console.debug('notify: NOTIFY is not set!');
return resolve();
}
const appriseBin = process.env.APPRISE_BIN || '/usr/local/bin/apprise';
const args = [cfg.notify, '-i', 'html', '-b', `'${html}'`];
if (cfg.notify_title) args.push('-t', cfg.notify_title);
if (cfg.debug) console.debug(`${appriseBin} ${args.join(' ')}`); // this also doesn't escape, but it's just for info
execFile(appriseBin, args, (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
if (error.message.includes('command not found')) {
console.info('Run `pip install apprise`. See https://github.com/vogler/free-games-claimer#notifications');
}
// don't fail the whole run on notification errors
return resolve();
}
if (stderr) console.error(`stderr: ${stderr}`);
if (stdout) console.log(`stdout: ${stdout}`);
resolve();
});
});
export const escapeHtml = unsafe => unsafe.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll('\'', '&#039;');
export const html_game_list = games => games.map(g => `- <a href="${g.url}">${escapeHtml(g.title)}</a> (${g.status})`).join('<br>');

41
src/version.js Normal file
View file

@ -0,0 +1,41 @@
import { log } from 'node:console';
import { execFile } from 'node:child_process';
const gitBin = process.env.GIT_BIN || '/usr/bin/git';
const runGit = (...args) => new Promise((resolve, reject) => {
execFile(gitBin, args, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (stderr) console.error(`stderr: ${stderr}`);
if (error) {
console.log(`error: ${error.message}`);
if (error.code === 'ENOENT' || error.message.includes('command not found')) {
console.info('Install git to check for updates!');
}
return reject(error);
}
resolve(stdout.trim());
});
});
let sha, date;
if (process.env.NOVNC_PORT) {
log('Running inside Docker.');
['COMMIT', 'BRANCH', 'NOW'].forEach(v => log(` ${v}:`, process.env[v]));
sha = process.env.COMMIT;
date = process.env.NOW;
} else {
log('Not running inside Docker.');
sha = await runGit('rev-parse', 'HEAD');
date = await runGit('show', '-s', '--format=%cD'); // same as format as `date -R` (RFC2822)
}
const gh = await (await fetch('https://api.github.com/repos/vogler/free-games-claimer/commits/main')).json();
log('Local commit:', sha, new Date(date));
log('Online commit:', gh.sha, new Date(gh.commit.committer.date));
if (sha == gh.sha) {
log('Running the latest version!');
} else {
log('Not running the latest version!');
}

70
steam-games.js Normal file
View file

@ -0,0 +1,70 @@
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
import { jsonDb, prompt } from './src/util.js';
import { cfg } from './src/config.js';
const db = await jsonDb('steam-games.json', {});
const user = cfg.steam_id || await prompt({ message: 'Enter Steam community id ("View my profile", then copy from URL)' });
// using https://github.com/apify/fingerprint-suite worked, but has no launchPersistentContext...
// from https://github.com/apify/fingerprint-suite/issues/162
import { FingerprintInjector } from 'fingerprint-injector';
import { FingerprintGenerator } from 'fingerprint-generator';
const { fingerprint, headers } = new FingerprintGenerator().getFingerprint({
devices: ['desktop'],
operatingSystems: ['windows'],
});
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
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,
},
extraHTTPHeaders: {
'accept-language': headers['accept-language'],
},
});
await new FingerprintInjector().attachFingerprintToPlaywright(context, { fingerprint, headers });
context.setDefaultTimeout(cfg.debug ? 0 : cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
try {
await page.goto(`https://steamcommunity.com/id/${user}/games?tab=all`);
const games = page.locator('div[data-featuretarget="gameslist-root"] > div.Panel > div.Panel > div');
await games.last().waitFor();
await page.keyboard.press('End');
await page.waitForLoadState('networkidle');
console.log('All Games:', await games.count());
for (const game of await games.all()) {
const title = await game.locator('span a').innerText();
let time, last, achievements, size;
const ltime = game.locator('span:has-text("total played")');
if (await ltime.count()) time = (await ltime.first().innerText()).split('\n')[1];
const llast = game.locator('span:has-text("last played")');
if (await llast.count()) last = (await llast.first().innerText()).split('\n')[1];
const lachievements = game.locator('a:has-text("achievements") + span');
if (await lachievements.count()) achievements = (await lachievements.first().innerText()).split('\n');
const lsize = game.locator('span:has(+ button)');
if (await lsize.count()) size = await lsize.first().innerText();
const url = await game.locator('a').first().getAttribute('href');
const img = await game.locator('img').first().getAttribute('src');
const stat = { title, time, last, achievements, size, url, img };
console.log(stat);
db.data[title] = stat;
}
} catch (error) {
process.exitCode ||= 1;
console.error('--- Exception:');
console.error(error); // .toString()?
} finally {
await db.write(); // write out json db
}
if (page.video()) console.log('Recorded video:', await page.video().path());
await context.close();

42
test/notify.js Normal file
View file

@ -0,0 +1,42 @@
import { delay, html_game_list, notify } from '../src/util.js';
import { cfg } from '../src/config.js';
const URL_CLAIM = 'https://gaming.amazon.com/home'; // dummy URL
console.debug('NOTIFY:', cfg.notify);
const scenarios = [
{
enabled: process.env.TEST_NOTIFY_EPIC === '1',
title: 'epic-games',
games: [
{ title: 'Epistory - Typing Chronicles', status: 'claimed', url: URL_CLAIM },
],
},
{
enabled: process.env.TEST_NOTIFY_PG === '1',
delayMs: 1000,
title: 'prime-gaming',
games: [
{ title: 'Faraway 2: Jungle Escape', status: 'claimed', url: URL_CLAIM },
{ title: 'Chicken Police - Paint it RED!', status: 'claimed', url: URL_CLAIM },
{ title: 'Lawn Mowing Simulator', status: 'claimed', url: URL_CLAIM },
{ title: 'Breathedge', status: 'claimed', url: URL_CLAIM },
{ title: 'The Evil Within 2', status: `<a href="${URL_CLAIM}">redeem</a> H97S6FB38FA6D09DEA on gog.com`, url: URL_CLAIM },
{ title: 'Beat Cop', status: `<a href="${URL_CLAIM}">redeem</a> BMKM8558EC55F7B38F on gog.com`, url: URL_CLAIM },
{ title: 'Dishonored 2', status: `<a href="${URL_CLAIM}">redeem</a> NNEK0987AB20DFBF8F on gog.com`, url: URL_CLAIM },
],
},
{
enabled: process.env.TEST_NOTIFY_GOG === '1',
delayMs: 1000,
title: 'gog',
games: [{ title: 'Haven Park', status: 'claimed', url: URL_CLAIM }],
},
];
for (const scenario of scenarios) {
if (!scenario.enabled) continue;
if (scenario.delayMs) await delay(scenario.delayMs);
await notify(`${scenario.title}:<br>${html_game_list(scenario.games)}`);
}

View file

@ -0,0 +1,21 @@
// open issue: prevents handleSIGINT() to work if prompt is cancelled with Ctrl-C instead of Escape: https://github.com/enquirer/enquirer/issues/372
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);
}
});
}
console.log(1);
onRawSIGINT(() => {
console.log('raw'); process.exit(1);
});
console.log(2);
// onRawSIGINT workaround for enquirer keeps the process from exiting here...

View file

@ -0,0 +1,18 @@
// https://github.com/enquirer/enquirer/issues/372
import { prompt, handleSIGINT } from '../src/util.js';
handleSIGINT();
console.log('hello');
console.error('hello error');
try {
const first = await prompt(); // SIGINT no longer handled if this is executed
const second = await prompt(); // SIGINT no longer handled if this is executed
console.log('values:', first, second);
setTimeout(() => console.log('timeout 3s'), 3000);
} catch (e) {
process.exitCode ||= 1;
console.log('catch. exitCode:', process.exitCode);
console.error(e);
}
console.log('end. exitCode:', process.exitCode);

View file

@ -0,0 +1,20 @@
// https://github.com/enquirer/enquirer/issues/372
import Enquirer from 'enquirer';
const enquirer = new Enquirer();
let interrupted = false;
process.on('SIGINT', () => {
if (interrupted) process.exit();
interrupted = true;
console.log('SIGINT');
});
await enquirer.prompt({
type: 'input',
name: 'username',
message: 'What is your username?',
});
await enquirer.prompt({
type: 'input',
name: 'username',
message: 'What is your username 2?',
});

223
unrealengine.js Normal file
View file

@ -0,0 +1,223 @@
import { firefox } from 'playwright-firefox'; // stealth plugin needs no outdated playwright-extra
import { authenticator } from 'otplib';
import path from 'node:path';
import { writeFileSync } from 'node:fs';
import { resolve, jsonDb, datetime, stealth, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js';
import { cfg } from './src/config.js';
const screenshot = (...a) => resolve(cfg.dir.screenshots, 'unrealengine', ...a);
const URL_CLAIM = 'https://www.unrealengine.com/marketplace/en-US/assets?count=20&sortBy=effectiveDate&sortDir=DESC&start=0&tag=4910';
const URL_LOGIN = 'https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM;
console.log(datetime(), 'started checking unrealengine');
const db = await jsonDb('unrealengine.json', {});
// https://playwright.dev/docs/auth#multi-factor-authentication
const context = await firefox.launchPersistentContext(cfg.dir.browser, {
headless: cfg.headless,
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', // Windows UA avoids "device not supported"; update when browser version changes
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/ue-${filenamify(datetime())}.har` } : undefined, // will record a HAR file with network requests and responses; can be imported in Chrome devtools
handleSIGINT: false, // have to handle ourselves and call context.close(), otherwise recordings from above won't be saved
});
handleSIGINT(context);
await stealth(context);
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
const page = context.pages().length ? context.pages()[0] : await context.newPage(); // should always exist
await page.setViewportSize({ width: cfg.width, height: cfg.height }); // workaround for https://github.com/vogler/free-games-claimer/issues/277 until Playwright fixes it
const notify_games = [];
let user;
try {
await context.addCookies([{ name: 'OptanonAlertBoxClosed', value: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), domain: '.epicgames.com', path: '/' }]); // Accept cookies to get rid of banner to save space on screen. Set accept time to 5 days ago.
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // 'domcontentloaded' faster than default 'load' https://playwright.dev/docs/api/class-page#page-goto
await page.waitForResponse(r => r.request().method() == 'POST' && r.url().startsWith('https://graphql.unrealengine.com/ue/graphql'));
while (await page.locator('unrealengine-navigation').getAttribute('isloggedin') != 'true') {
console.error('Not signed in anymore. Please login in the browser or here in the terminal.');
if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`);
if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); // give user some extra time to log in
console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`);
await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' });
if (cfg.eg_email && cfg.eg_password) console.info('Using email 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 email = cfg.eg_email || await prompt({ message: 'Enter email' });
const password = email && (cfg.eg_password || await prompt({ type: 'password', message: 'Enter password' }));
if (email && password) {
await page.fill('#email', email);
await page.click('button[type="submit"]');
await page.fill('#password', password);
await page.click('button[type="submit"]');
const watchCaptchaDuringLogin = async () => {
try {
await page.waitForSelector('#h_captcha_challenge_login_prod iframe', { timeout: 15000 });
console.error('Got a captcha during login (likely due to too many attempts)! You may solve it in the browser, get a new IP or try again in a few hours.');
notify('unrealengine: got captcha during login. Please check.');
} catch {
return;
}
};
const watchMfa = async () => {
try {
await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout });
console.log('Enter the security code to continue - This appears to be a new device, browser or location. A security code has been sent to your email address at ...');
const otp = cfg.eg_otpkey && authenticator.generate(cfg.eg_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 page.locator('input[name="code-input-0"]').pressSequentially(otp.toString());
await page.click('button[type="submit"]');
} catch {
return;
}
};
watchCaptchaDuringLogin();
watchMfa();
} else {
console.log('Waiting for you to login in the browser.');
await notify('unrealengine: no longer signed in and not enough options set for automatic login.');
if (cfg.headless) {
console.log('Run `SHOW=1 node unrealengine` to login in the opened browser.');
await context.close(); // finishes potential recording
process.exit(1);
}
}
await page.waitForURL('**unrealengine.com/marketplace/**');
if (!cfg.debug) context.setDefaultTimeout(cfg.timeout);
}
await page.waitForTimeout(1000);
user = await page.locator('unrealengine-navigation').getAttribute('displayname'); // 'null' if !isloggedin
console.log(`Signed in as ${user}`);
db.data[user] ||= {};
page.locator('button:has-text("Accept All Cookies")').click().catch(_ => { });
const ids = [];
for (const p of await page.locator('article.asset').all()) {
const link = p.locator('h3 a');
const title = await link.innerText();
const url = 'https://www.unrealengine.com' + await link.getAttribute('href');
console.log([title, url]);
const id = url.split('/').pop();
db.data[user][id] ||= { title, time: datetime(), url, status: 'failed' }; // this will be set on the initial run only!
const notify_game = { title, url, status: 'failed' };
notify_games.push(notify_game); // status is updated below
// if (await p.locator('.btn .add-review-btn').count()) { // did not work
if ((await p.getAttribute('class')).includes('asset--owned')) {
console.log(' ↳ Already claimed');
if (db.data[user][id].status != 'claimed') {
db.data[user][id].status = 'existed';
notify_game.status = 'existed';
}
continue;
}
if (await p.locator('.btn .in-cart').count()) {
console.log(' ↳ Already in cart');
} else {
await p.locator('.btn .add').click();
console.log(' ↳ Added to cart');
}
ids.push(id);
}
if (ids.length === 0) {
console.log('Nothing to claim');
} else {
await page.waitForTimeout(2000);
const price = (await page.locator('.shopping-cart .total .price').innerText()).split(' ');
console.log('Price: ', price[1], 'instead of', price[0]);
if (price[1] != '0') {
const err = 'Price is not 0! Exit! Please <a href="https://github.com/vogler/free-games-claimer/issues/44">report</a>.';
console.error(err);
notify('unrealengine: ' + err);
process.exit(1);
}
console.log('Click shopping cart');
await page.locator('.shopping-cart').click();
await page.locator('button.checkout').click();
console.log('Click checkout');
// maybe: Accept End User License Agreement
const acceptEulaIfPresent = async () => {
try {
await page.locator('[name=accept-label]').check({ timeout: 10000 });
console.log('Accept End User License Agreement');
await page.locator('span:text-is("Accept")').click(); // otherwise matches 'Accept All Cookies'
} catch {
return;
}
};
acceptEulaIfPresent();
await page.waitForSelector('#webPurchaseContainer iframe');
const iframe = page.frameLocator('#webPurchaseContainer iframe');
if (cfg.debug) await page.pause();
if (cfg.dryrun) {
console.log('DRYRUN=1 -> Skip order!');
throw new Error('DRYRUN=1');
}
console.log('Click Place Order');
// Playwright clicked before button was ready to handle event, https://github.com/vogler/free-games-claimer/issues/84#issuecomment-1474346591
await iframe.locator('button:has-text("Place Order"):not(:has(.payment-loading--loading))').click({ delay: 11 });
// I Agree button is only shown for EU accounts! https://github.com/vogler/free-games-claimer/pull/7#issuecomment-1038964872
const btnAgree = iframe.locator('button:has-text("I Agree")');
const acceptIfRequired = async () => {
try {
await btnAgree.waitFor({ timeout: 10000 });
await btnAgree.click();
} catch {
return;
}
}; // EU: wait for and click 'I Agree'
acceptIfRequired();
try {
// context.setDefaultTimeout(100 * 1000); // give time to solve captcha, iframe goes blank after 60s?
const captcha = iframe.locator('#h_captcha_challenge_checkout_free_prod iframe');
const watchCaptchaChallenge = async () => {
try {
await captcha.waitFor({ timeout: 10000 });
console.error(' Got hcaptcha challenge! Lost trust due to too many login attempts? You can solve the captcha in the browser or get a new IP address.');
} catch {
return;
}
}; // may time out if not shown
watchCaptchaChallenge();
await page.waitForSelector('text=Thank you');
for (const id of ids) {
db.data[user][id].status = 'claimed';
db.data[user][id].time = datetime(); // claimed time overwrites failed/dryrun time
}
notify_games.forEach(g => g.status == 'failed' && (g.status = 'claimed'));
console.log('Claimed successfully!');
} catch (e) {
console.log(e);
console.error(' Failed to claim! To avoid captchas try to get a new IP address.');
await page.screenshot({ path: screenshot('failed', `${filenamify(datetime())}.png`), fullPage: true });
notify_games.forEach(g => g.status = 'failed');
}
if (notify_games.length) await page.screenshot({ path: screenshot(`${filenamify(datetime())}.png`), fullPage: false }); // fullPage is quite long...
console.log('Done');
}
} catch (error) {
process.exitCode ||= 1;
console.error('--- Exception:');
console.error(error); // .toString()?
if (error.message && process.exitCode != 130) notify(`unrealengine 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(`unrealengine (${user}):<br>${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();

74
util.js
View file

@ -1,74 +0,0 @@
// https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-js-when-using-es6-modules
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// not the same since these will give the absolute paths for this file instead of for the file using them
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// explicit object instead of Object.fromEntries since the built-in type would loose the keys, better type: https://dev.to/svehla/typescript-object-fromentries-389c
const dataDir = s => path.resolve(__dirname, 'data', s);
export const dirs = {
data: dataDir('.'),
browser: dataDir('browser'),
screenshots: dataDir('screenshots'),
};
import { Low, JSONFile } from 'lowdb';
export const jsonDb = async file => {
const db = new Low(new JSONFile(dataDir(file)));
await db.read();
return db;
};
export const datetime = (d = new Date()) => d.toISOString().replace('T', ' ').replace('Z', '');
export const filenamify = s => s.replaceAll(':', '.').replace(/[^a-z0-9 _\-.]/gi, '_'); // alternative: https://www.npmjs.com/package/filenamify - On Unix-like systems, / is reserved. On Windows, <>:"/\|?* along with trailing periods are reserved.
// stealth with playwright: https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212
const newStealthContext = async (browser, contextOptions = {}, debug = false) => {
if (!debug) { // only need to fix userAgent in headless mode
const dummyContext = await browser.newContext();
const originalUserAgent = await (await dummyContext.newPage()).evaluate(() => navigator.userAgent);
await dummyContext.close();
// console.log('originalUserAgent:', originalUserAgent); // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/96.0.4664.110 Safari/537.36
contextOptions = {
...contextOptions,
userAgent: originalUserAgent.replace("Headless", ""), // HeadlessChrome -> Chrome, TODO needed?
};
}
};
export const stealth = async (context) => {
// stealth with playwright: https://github.com/berstend/puppeteer-extra/issues/454#issuecomment-917437212
// https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions
const enabledEvasions = [
'chrome.app',
'chrome.csi',
'chrome.loadTimes',
'chrome.runtime',
// 'defaultArgs',
'iframe.contentWindow',
'media.codecs',
'navigator.hardwareConcurrency',
'navigator.languages',
'navigator.permissions',
'navigator.plugins',
// 'navigator.vendor',
'navigator.webdriver',
'sourceurl',
// 'user-agent-override', // doesn't work since playwright has no page.browser()
'webgl.vendor',
'window.outerdimensions'
];
const stealth = {
callbacks: [],
async evaluateOnNewDocument(...args) {
this.callbacks.push({ cb: args[0], a: args[1] });
}
};
for (const e of enabledEvasions) {
const evasion = await import(`puppeteer-extra-plugin-stealth/evasions/${e}/index.js`);
evasion.default().onPageCreated(stealth);
}
for (let evasion of stealth.callbacks) {
await context.addInitScript(evasion.cb, evasion.a);
}
};