Skip to content

Instantly share code, notes, and snippets.

@robertsez
Last active September 3, 2024 09:30
Show Gist options
  • Save robertsez/f91a6f02b032a9146135ecce65a118d4 to your computer and use it in GitHub Desktop.
Save robertsez/f91a6f02b032a9146135ecce65a118d4 to your computer and use it in GitHub Desktop.
A simple frontend for Hamster Kombat promo key generator

Hamster Kombat key generator frontend 🐹

This HTML file serves as a simple, silly frontend for delasy's key generator gist.
Originally, the previously mentioned gist was a direct dependency on this gist, but after the owner made some breaking changes that made my monkey patching very difficult, I decided that it was easier to just maintain a dedicated fork (I've copied his code inside this gist directly).

Why did I create this?

  • So that I could quickly run this via browser without pasting the code into the console every time.
  • I don't want to setup Node.js on my machine.
  • I slightly edited the implementation to my liking:
    1. I think that generating keys once at the time is better for avoiding detection.
    2. To better simulate real game playtime I also added "game event registration" delay control settings and sound effects for leaving this working at the background.
    3. I don't think that running this with random clientIds for every key generation is good for avoiding detection, so I injected some logic for clientId caching at local storage.
  • Added QR code generation so that I could easily get keys to my mobile device.

Caution

If you choose to run this, you agree that you are doing it at your own risk! Not just this code is just terribly bad (monkey patching, use of eval(), etc.), but you also risk losing a part of (or whole) airdrop in Hamster Kombat.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hampster kombaint cheats</title>
<link rel="icon" href="https://hamsterkombatgame.io/favicon.ico" type="image/x-icon">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<style>
.container {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
text-align: center;
}
.qrcode-container {
text-align: center;
width: 256px;
height: 256px;
margin: 0 auto;
}
.generate-btn {
margin: 5px;
}
@keyframes blink {
50% {
opacity: 0.6;
background-color: rgb(21, 92, 0);
}
}
.blink {
animation: blink 2.75s infinite;
}
#result-tube {
background-color: #cce1f7;
border-radius: 10px;
padding: 10px;
margin: 20px;
display: flex;
align-items: center;
justify-content: center;
}
#hampster,
#forsen-cd {
border-radius: 10px;
margin-bottom: 20px;
}
#forsen-cd {
width: 50px;
height: 50px;
margin-left: 10px;
}
#log {
font-family: monospace;
}
</style>
</head>
<body>
<main class="container">
<h1>Hampster kombaint cheats
<img id="forsen-cd"
src="https://static-cdn.jtvnw.net/jtv_user_pictures/519ee1f3-6723-4237-9fa3-bf33aae5c8f2-profile_image-300x300.png"
alt="forsenCD">
✌️
</h1>
<img id="hampster" src="https://qph.cf2.quoracdn.net/main-qimg-f9f787c5820fbc13c83127345a419785.webp"
alt="Hampster" class="img-fluid">
<div id="result-tube" class="d-none">
<h3 id="result-holder">...</h3>
<button id="result-copy" class="btn btn-secondary m-2" onclick="copy()">📋</button>
</div>
<div id="qrcode-container" class="qrcode-container mt-2 mb-2 d-none">
<div id="qrcode"></div>
</div>
<div class="generator row">
<div class="generator-buttons col border">
<h4>Games</h4>
<p>Select a game below to start the key generation</p>
</div>
<div class="generator-settings col border">
<h4>Delay settings</h4>
<p>Extend key generation delays to lower the risk for detection. Longer delays better simulate real playtime. A
sound will play once the next key is ready. Hover over any game button to see estimated generation time</p>
<div class="input-group mb-3">
<label class="input-group-text" for="delay">Multiplier</label>
<select class="form-select" id="delay">
<option value="0.5">0.5x - Fast delay (very risky, might crash)</option>
<option value="0.85">0.85x - Faster delay (risky)</option>
<option value="1" selected>1x - Realistic delay</option>
<option value="1.5">1.5x - Long delay</option>
<option value="2">2x - Very long delay</option>
<option value="custom">Custom delay multiplier</option>
</select>
</div>
<div class="input-group mb-3 d-none" id="custom-delay">
<label class="input-group-text" for="custom-delay-input">Custom delay multiplier</label>
<input type="number" class="form-control" id="custom-delay-input" min="0" step="0.1" value="1">
</div>
<div class="input-group mb-3" id="eta">
<label class="input-group-text" for="min-max-eta">Estimated time range</label>
<input id="min-max-eta" class="form-control" value="ETA" disabled></input>
</div>
</div>
</div>
<div class="log-area p-5">
<h3>🏆 Champions Log 🏆</h3>
<textarea id="log" class="form-control" rows="12" readonly></textarea>
</div>
</main>
<script>
class Logger {
static formatMessage() {
return Array.from(arguments).map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg
).join(' ');
}
static debug() {
if (!DEBUG) {
return;
}
const msg = Logger.formatMessage.apply(null, arguments);
console.log(msg);
logToTextArea(msg);
}
static info() {
const msg = Logger.formatMessage.apply(null, arguments);
console.info(msg);
logToTextArea(msg);
}
static panic() {
const msg = Logger.formatMessage.apply(null, arguments);
console.error(msg);
logToTextArea(msg);
}
}
class GamePromo {
constructor() {
this.authToken = null;
this.config = {};
this.hasCode = false;
this.key = null;
this.origin = 'android';
this.eventCounter = 0;
this.authTokenCache = {};
}
async fetchApi(path, body = null, retry = 0) {
const headers = {
accept: '*/*',
'accept-encoding': 'deflate, gzip',
'content-type': 'application/json',
};
if (this.authToken !== null) {
headers.authorization = `Bearer ${this.authToken}`;
}
if (this.config['user-agent'] !== undefined) {
headers['user-agent'] = this.config['user-agent'];
}
if (this.config['unity-version'] !== undefined) {
headers['x-unity-version'] = this.config['unity-version'];
}
const url = `https://api.gamepromo.io${path}`;
const options = {
method: 'POST',
cache: 'no-store',
headers,
body: JSON.stringify(body),
};
Logger.debug('Request:', url, options);
let res;
try {
res = await fetch(url, options);
} catch (err) {
if (retry < SERVER_ERROR_RETRIES) {
Logger.info('Network error, will retry after cooldown period.');
Logger.debug(err);
await globalDelay(SERVER_ERROR_COOLDOWN);
return this.fetchApi(path, body, retry + 1);
}
throw err;
}
if (!res.ok) {
if (DEBUG) {
const text = await res.text();
Logger.debug('Response:', text);
}
if (retry < SERVER_ERROR_RETRIES) {
Logger.info('Received internal server error, will retry after cooldown period.');
await globalDelay(SERVER_ERROR_COOLDOWN);
return this.fetchApi(path, body, retry + 1);
}
throw new Error(`${res.status} ${res.statusText}`);
}
const data = await res.json();
Logger.debug('Response:', data);
return data;
}
async configSet(key, value) {
this.config[key] = value;
}
async loginFetch(data) {
const gameId = this.config['game-id'];
if (this.authTokenCache[gameId] !== undefined) {
this.authToken = this.authTokenCache[gameId];
Logger.debug(`Skipping login for ${gameId}, using cached auth token: ${this.authToken}`);
return;
}
const res = await this.fetchApi('/promo/login-client', {
appToken: this.config['app-token'],
...data,
});
if (typeof res.clientToken === 'string' && res.clientToken !== '') {
this.authToken = res.clientToken;
this.authTokenCache[gameId] = res.clientToken;
}
}
async eventFetch(data) {
Logger.debug(`Registering event #${++this.eventCounter}`);
const promoId = this.config['promo-id'];
// on ios promoId is sent as first property, on android it's sent last
const payload = this.origin === 'ios' ? { promoId, ...data } : { ...data, promoId };
const res = await this.fetchApi('/promo/register-event', payload);
if (res.hasCode === true) {
this.hasCode = true;
}
}
async collectFetch() {
const res = await this.fetchApi('/promo/create-code', {
promoId: this.config['promo-id'],
});
if (typeof res.promoCode === 'string' && res.promoCode !== '') {
this.key = res.promoCode;
}
}
async getCode(gameKey, delayMultiplier) {
this.authToken = null;
this.config = {};
this.hasCode = false;
this.key = null;
this.origin = 'android';
this.eventCounter = 0;
Logger.debug('origin:', this.origin);
await GAMES[gameKey]['payload']({
collect: this.collectFetch.bind(this),
delay: async (ms) => {
const totalMs = Math.floor(ms * (Math.random() / 4 + 1));
await globalDelay(totalMs * delayMultiplier);
},
id: globalId,
instance: this,
login: this.loginFetch.bind(this),
event: this.eventFetch.bind(this),
origin: this.origin,
setup: this.configSet.bind(this),
});
if (this.key === null) {
throw new Error(`Unable to get ${gameKey} promo.`);
}
return this.key;
}
}
async function globalDelay(ms) {
const minutes = Math.floor(ms / 60000);
if (minutes > 0) {
Logger.debug(`Waiting ~${minutes} ${minutesFmt(minutes)}`);
}
else {
Logger.debug(`Waiting ${Math.floor(ms / 1000)} seconds`);
}
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function maybeCachedClientId(gameKey) {
return localStorage.getItem(`clientId-${gameKey}`);
}
function cacheClientId(gameKey, clientId) {
if (clientId === null) {
return;
}
Logger.debug(`Storing new client ID for ${gameKey}: ${clientId}`);
localStorage.setItem(`clientId-${gameKey}`, clientId);
}
function globalId(type, game = null) {
if (game !== null) {
var cachedClientId = maybeCachedClientId(game);
if (cachedClientId !== null) {
if (type === 'ts7d' || type === 'ts19d') {
const timestamp = Date.now();
cachedClientId = `${timestamp}-${cachedClientId}`;
}
Logger.debug(`Using cached client ID for ${game}: ${cachedClientId}`);
return cachedClientId;
}
}
var newId, idToCache;
switch (type) {
case 'rand16': {
newId = Array.from(
crypto.getRandomValues(new Uint8Array(8)),
(it) => it.toString(16).padStart(2, '0'),
).join('');
idToCache = newId;
break;
}
case 'rand32': {
newId = Array.from(
crypto.getRandomValues(new Uint8Array(16)),
(it) => it.toString(16).padStart(2, '0'),
).join('');
idToCache = newId;
break;
}
case 'uuid':
case 'uuid-upper': {
const val = '10000000-1000-4000-8000-100000000000'.replace(
/[018]/g,
(c) => (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16),
);
newId = type === 'uuid-upper' ? val.toUpperCase() : val;
idToCache = newId;
break;
}
case 'ts': {
newId = Date.now().toString();
idToCache = null;
break;
}
case 'ts7d':
case 'ts19d': {
const timestamp = Date.now();
const buf = Array(type === 'ts7d' ? 7 : 19).fill();
const digits = buf.map(() => Math.floor(Math.random() * 10)).join('');
newId = `${timestamp}-${digits}`;
idToCache = digits;
break;
}
default: {
throw new Error(`Tried generating unknown id '${type}'.`);
}
}
if (game !== null) {
cacheClientId(game, idToCache);
}
return newId;
}
function minutesFmt(minutes) {
return minutes == 1 ? 'minute' : 'minutes';
}
const GP = new GamePromo();
const DEBUG = true;
const SERVER_ERROR_COOLDOWN = 300_000;
const SERVER_ERROR_RETRIES = 3;
const WITH_REINSTALL_TIME = false;
const GAMES = {
CAFE: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'CAFE');
setup('app-token', 'bc0971b8-04df-4e72-8a3e-ec4dc663cd11');
setup('promo-id', 'bc0971b8-04df-4e72-8a3e-ec4dc663cd11');
await login({ clientId: id('rand16', 'CAFE'), clientOrigin: origin, clientVersion: '2.24.0' });
while (!instance.hasCode) {
await delay(90_000);
await event({ eventId: id('ts'), eventOrigin: 'undefined', eventType: '5visitorsChecks' });
}
await collect();
},
'meta': {
'id': 'CAFE',
'name': '☕ Cafe Dash',
'delay': 90_000,
'estimated-events': 10
}
},
TRIM: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'TRIM');
setup('app-token', 'ef319a80-949a-492e-8ee0-424fb5fc20a6');
setup('promo-id', 'ef319a80-949a-492e-8ee0-424fb5fc20a6');
setup('unity-version', '2021.3.17f1');
if (origin === 'ios') {
setup('user-agent', 'MowandTrim/170 CFNetwork/1498.700.2 Darwin/23.6.0');
} else {
setup('user-agent', 'UnityPlayer/2021.3.17f1 (UnityWebRequest/1.0, libcurl/7.84.0-DEV)');
}
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'ts7d' : 'ts19d', 'TRIM') });
while (!instance.hasCode) {
await delay(50_000);
await event({ eventId: 'StartLevel', eventOrigin: 'undefined' });
}
await collect();
},
'meta': {
'id': 'TRIM',
'name': '🌾 Mow and Trim',
'delay': 50_000,
'estimated-events': 8
}
},
RACE: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'RACE');
setup('app-token', '8814a785-97fb-4177-9193-ca4180ff9da8');
setup('promo-id', '8814a785-97fb-4177-9193-ca4180ff9da8');
setup('unity-version', '2020.3.18f1');
if (origin === 'ios') {
setup('user-agent', 'Truckbountyhole/12 CFNetwork/1498.700.2 Darwin/23.6.0');
} else {
setup('user-agent', 'UnityPlayer/2020.3.18f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)');
}
await login({ clientOrigin: origin, clientId: id('uuid', 'RACE') });
while (!instance.hasCode) {
await delay(60_000);
await event({ eventId: id('uuid'), eventOrigin: 'undefined', eventType: 'racing' });
}
await collect();
},
'meta': {
'id': 'RACE',
'name': '🚚 Mud Racing',
'delay': 60_000,
'estimated-events': 6
}
},
POLY: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'POLY');
setup('app-token', '2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71');
setup('promo-id', '2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71');
setup('unity-version', '2021.3.39f1');
if (origin === 'ios') {
setup('user-agent', 'Polysphere/147 CFNetwork/1498.700.2 Darwin/23.6.0');
} else {
setup('user-agent', 'UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)');
}
await login({ clientOrigin: origin, clientId: id('uuid', 'POLY'), clientVersion: '1.15.2' });
while (!instance.hasCode) {
await delay(20_000); // imo 10 sec is too low
await event({ eventId: id('uuid'), eventOrigin: 'undefined', eventType: 'test' });
}
await collect();
},
'meta': {
'id': 'POLY',
'name': '🔵 Polysphere',
'delay': 20_000,
'estimated-events': 16
}
},
TWERK: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'TWERK');
setup('app-token', '61308365-9d16-4040-8bb0-2f4a4c69074c');
setup('promo-id', '61308365-9d16-4040-8bb0-2f4a4c69074c');
setup('unity-version', '2021.3.15f1');
if (origin === 'ios') {
setup('user-agent', 'Twerk/485 CFNetwork/1498.700.2 Darwin/23.6.0');
} else {
setup('user-agent', 'UnityPlayer/2021.3.15f1 (UnityWebRequest/1.0, libcurl/7.84.0-DEV)');
}
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'ts7d' : 'ts19d', 'TWERK') });
while (!instance.hasCode) {
await delay(30_000);
await event({ eventId: 'StartLevel', eventOrigin: 'undefined' });
}
await collect();
},
'meta': {
'id': 'TWERK',
'name': '🎖️ Twerk Race',
'delay': 30_000,
'estimated-events': 10
}
},
MERGE: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'MERGE');
setup('app-token', '8d1cc2ad-e097-4b86-90ef-7a27e19fb833');
setup('promo-id', 'dc128d28-c45b-411c-98ff-ac7726fbaea4');
if (origin === 'ios') {
setup('user-agent', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148');
} else {
setup('user-agent', 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36');
}
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'ts7d' : 'ts19d', 'MERGE') });
while (!instance.hasCode) {
await delay(60_000);
await event({ eventOrigin: 'undefined', eventId: id('uuid'), eventType: 'spend-energy' });
}
await collect();
},
'meta': {
'id': 'MERGE',
'name': '🔗 Merge Away',
'delay': 60_000,
'estimated-events': 7
}
},
// Currently deactivated
//
// CLONE: {
// 'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
// setup('game-id', 'CLONE');
// setup('app-token', '74ee0b5b-775e-4bee-974f-63e7f4d5bacb');
// setup('promo-id', 'fe693b26-b342-4159-8808-15e3ff7f8767');
// setup('unity-version', '2022.3.25f1');
// if (origin === 'ios') {
// setup('user-agent', 'Myclonearmy/12 CFNetwork/1498.700.2 Darwin/23.6.0');
// } else {
// setup('user-agent', 'UnityPlayer/2022.3.25f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)');
// }
// await login({ clientId: id(origin === 'ios' ? 'uuid-upper' : 'rand32', 'CLONE'), clientOrigin: origin });
// while (!instance.hasCode) {
// await delay(150_000);
// await event({ eventId: id('uuid'), eventType: 'MiniQuest', eventOrigin: 'undefined' });
// }
// await collect();
// },
// 'meta': {
// 'id': 'CLONE',
// 'name': '👾 My Clone Army',
// 'delay': 150_000,
// 'estimated-events': 5
// }
// },
CUBE: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'CUBE');
setup('app-token', 'd1690a07-3780-4068-810f-9b5bbf2931b2');
setup('promo-id', 'b4170868-cef0-424f-8eb9-be0622e8e8e3');
setup('unity-version', '2022.3.20f1');
if (origin === 'ios') {
setup('user-agent', 'ChainCube/3 CFNetwork/1498.700.2 Darwin/23.6.0');
} else {
setup('user-agent', 'UnityPlayer/2022.3.20f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)');
}
await login({ clientOrigin: origin, clientId: id('uuid', 'CUBE'), clientVersion: '1.78.33' });
while (!instance.hasCode) {
await delay(150_000);
await event({ eventId: id('uuid'), eventOrigin: 'undefined', eventType: 'cube_sent' });
}
await collect();
},
'meta': {
'id': 'CUBE',
'name': '🔲 Chain Cube 2028',
'delay': 150_000,
'estimated-events': 3
}
},
TRAIN: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'TRAIN');
setup('app-token', '82647f43-3f87-402d-88dd-09a90025313f');
setup('promo-id', 'c4480ac7-e178-4973-8061-9ed5b2e17954');
setup('unity-version', '2022.3.20f1');
if (origin === 'ios') {
setup('user-agent', 'TrainMiner/20 CFNetwork/1498.700.2 Darwin/23.6.0');
} else {
setup('user-agent', 'UnityPlayer/2022.3.20f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)');
}
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'uuid-upper' : 'rand32', 'TRAIN'), clientVersion: '2.4.16' });
while (!instance.hasCode) {
await delay(600_000);
await event({ eventId: id('uuid'), eventOrigin: 'undefined', eventType: 'hitStatue' });
}
await collect();
},
'meta': {
'id': 'TRAIN',
'name': '🚂 Train Miner',
'delay': 600_000,
'estimated-events': 1
}
},
BIKE: {
'payload': async ({ collect, delay, event, id, instance, login, origin, setup }) => {
setup('game-id', 'BIKE');
setup('app-token', 'd28721be-fd2d-4b45-869e-9f253b554e50');
setup('promo-id', '43e35910-c168-4634-ad4f-52fd764a843f');
// todo(delasy): Actually scan BIKE game headers
await login({ clientOrigin: origin, clientId: id(origin === 'ios' ? 'ts7d' : 'ts19d', 'BIKE') });
while (!instance.hasCode) {
await delay(50_000);
await event({ eventId: id('uuid'), eventOrigin: 'undefined' });
}
await collect();
},
'meta': {
'id': 'BIKE',
'name': '🚲 Bike Ride 3D',
'delay': 50_000,
'estimated-events': 13
}
},
};
const LOG = document.getElementById('log');
const CUSTOM_DELAY = document.getElementById('custom-delay');
const CUSTOM_DELAY_INPUT = document.getElementById('custom-delay-input');
const DELAY_SELECT = document.getElementById('delay');
const MIN_MAX_ETA = document.getElementById('min-max-eta');
const RESULT_TUBE = document.getElementById('result-tube');
const RESULT_HOLDER = document.getElementById('result-holder');
const RESULT_COPY = document.getElementById('result-copy');
const QR_CODE_CONTAINER = document.getElementById('qrcode-container');
var qrCode;
function handleError(error) {
Logger.panic('Error: ', error);
logToTextArea(error);
}
function clearLog() {
LOG.value = '';
}
function calculateEtaInSeconds(gameKey) {
const meta = GAMES[gameKey]['meta'];
const totalMs = meta['estimated-events'] * meta['delay'] * getDelayMultiplier();
const withRandomness = totalMs * ((1 / 4) / 1.5 + 1); // 2/3 of: ms * (Math.random() / 4 + 1
return withRandomness / 1000;
}
function allGenerateButtons() {
return document.querySelectorAll('.generate-btn');
}
function createGenerateButtons() {
const games = Object.keys(GAMES);
for (const game of games) {
const button = document.createElement('button');
button.classList.add('btn', 'btn-primary', 'generate-btn');
button.setAttribute('game-id', game);
button.textContent = GAMES[game]['meta']['name'];
button.onclick = () => main(button).catch(handleError);
document.querySelector('.generator-buttons').appendChild(button);
}
}
function refreshGenerateButtonsTooltips() {
for (const button of allGenerateButtons()) {
const gameId = button.getAttribute('game-id');
const estimatedTime = calculateEtaInSeconds(gameId);
var tooltip;
if (estimatedTime > 0) {
const estimatedTimeMinutes = Math.floor(estimatedTime / 60);
tooltip = `ETA: ~${estimatedTimeMinutes} ${minutesFmt(estimatedTimeMinutes)}`;
}
else {
tooltip = 'ETA: N/A';
}
button.setAttribute('data-toggle', 'tooltip');
button.setAttribute('data-placement', 'top');
button.setAttribute('title', tooltip);
}
}
function refreshMinMaxEta() {
var minEta = null, maxEta = null;
for (const game of Object.keys(GAMES)) {
const eta = calculateEtaInSeconds(game);
if (eta <= 0) {
continue;
}
if (minEta === null || eta < minEta) {
minEta = eta;
}
if (maxEta === null || eta > maxEta) {
maxEta = eta;
}
}
const minMinutes = Math.floor(minEta / 60);
const maxMinutes = Math.floor(maxEta / 60);
MIN_MAX_ETA.value = `${minMinutes} - ${maxMinutes} ${minutesFmt(maxMinutes)}`;
}
function logToTextArea(message) {
LOG.value += message + '\n';
LOG.scrollTop = LOG.scrollHeight;
}
async function copy() {
await navigator.clipboard.writeText(RESULT_HOLDER.innerText);
}
function generatingEffect() {
const textArray = ['.', '..', '...'];
let index = 0;
return setInterval(() => {
RESULT_HOLDER.innerText = textArray[index];
index = (index + 1) % textArray.length;
}, 500);
}
function toggleCopyButton(visible) {
RESULT_COPY.classList.toggle('d-none', !visible);
}
function toggleQrCodeContainer(visible) {
QR_CODE_CONTAINER.classList.toggle('d-none', !visible);
}
function toggleGenerateButtonActive(button, active) {
button.classList.toggle('btn-success', active);
button.classList.toggle('blink', active);
}
function getDelayMultiplier() {
const delayMultiplier = DELAY_SELECT.options[DELAY_SELECT.selectedIndex].value;
if (delayMultiplier === 'custom') {
return CUSTOM_DELAY_INPUT.value;
}
return delayMultiplier;
}
async function getPromoCode(gameKey, delayMultiplier) {
return GP.getCode(gameKey, delayMultiplier);
}
async function main(pressedButton) {
const game = pressedButton.getAttribute('game-id');
const delayMultiplier = getDelayMultiplier();
const startDate = Date.now();
const generatingEffectInterval = generatingEffect();
RESULT_TUBE.classList.remove('d-none');
toggleGenerateButtonActive(pressedButton, true);
allGenerateButtons().forEach(b => b.disabled = true);
toggleCopyButton(false);
toggleQrCodeContainer(false);
try {
const code = await getPromoCode(game, delayMultiplier);
const endDate = new Date();
const minutes = Math.floor((endDate - startDate) / 60000);
Logger.info(`Generated at: ${endDate.toLocaleString()} (took ~${minutes} ${minutesFmt(minutes)} with ${GP.eventCounter} events)`);
Logger.info('*************************** PROMO CODE ***************************');
Logger.info(code);
Logger.info('******************************************************************');
clearInterval(generatingEffectInterval); // must stop this before showing result
RESULT_HOLDER.innerText = code;
toggleCopyButton(true);
qrCode.clear();
qrCode.makeCode(code);
toggleQrCodeContainer(true);
} catch (error) {
clearInterval(generatingEffectInterval);
handleError(error);
} finally {
allGenerateButtons().forEach(b => b.disabled = false);
toggleGenerateButtonActive(pressedButton, false);
new Audio('https://opengameart.org/sites/default/files/audio_preview/chirptone.mp3.ogg').play();
}
}
// The only reason I want (I actually don't) to do this, is to be able to
// run this HTML via https://html-preview.github.io
// I can't check for DOMContentLoaded to create the QRCode instance and
// the script is loaded too late
fetch('https://cdn.jsdelivr.net/gh/davidshimjs/qrcodejs/qrcode.min.js')
.then(response => response.text())
.then(jsCode => {
eval(jsCode);
qrCode = new QRCode("qrcode", {
text: "",
width: 256,
height: 256
});
})
.catch(error => {
handleError(error);
});
DELAY_SELECT.addEventListener('change', (event) => {
CUSTOM_DELAY.classList.toggle('d-none', event.target.value !== 'custom');
refreshGenerateButtonsTooltips();
refreshMinMaxEta();
});
CUSTOM_DELAY_INPUT.addEventListener('input', () => {
refreshGenerateButtonsTooltips();
refreshMinMaxEta();
});
createGenerateButtons();
refreshGenerateButtonsTooltips();
refreshMinMaxEta();
clearLog();
Logger.info('Welcome champion! forsenCD ✌️');
Logger.info('Choose a game and press the button to get the promo code.');
Logger.info('==================================================================');
</script>
</body>
</html>
@robertsez
Copy link
Author

Hello, good time. Thank you for your good program. It is a really good idea. Only one item in the program code is used from the iPhone system, how to change it to Android? Caching new client ID for TWERK: 7a5259b1-38d3-4666-b407-6a8192d165ab Injected cached client ID for TWERK: 7a5259b1-38d3-4666-b407-6a8192d165ab https://api.gamepromo.io/promo/login-client { "method": "POST", "cache": "no-store", "headers": { "content-type": "application/json" }, "body": "{\"appToken\":\"61308365-9d16-4040-8bb0-2f4a4c69074c\",\"clientId\":\"7a5259b1-38d3-4666-b407-6a8192d165ab\",\"clientOrigin\":\"ios\"}" } { "clientToken": "61308365-9d16-4040-8bb0-2f4a4c69074c:ios:7a5259b1-38d3-4666-b407-6a8192d165ab:8BCU2JEwRaG:1723999643066" } Waiting 20000ms

Hello. Thanks! I've set it to android only now (I assume that most of the users are using Android). It was set to ios in the original gist.

Also, I've updated the actual generator to match changes in the mentioned gist. From now on this gist is kind of a fork of delasy's gist, because I could not keep it as a dependency, since it became difficult to monkey patch his code.

@robertsez
Copy link
Author

It is much better now. Just one thing, put the User-Agent like this so that the mobile phone specifications are the phone.

///// Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36 /////

This is more real

Did you get this from your actual device? I assume it is for the MERGE game?

@robertsez
Copy link
Author

robertsez commented Aug 19, 2024

As I did not intercept the requests myself, I previously did not understand the meaning of /promo/register-event endpoint. Now I finally get what they are for and I realized that the current delay settings are useless. I will try to rework those to add more control over the delay between sending these kind of requests, instead of adding delay between the individual key generations.

Edit: I have now reworked the delay settings.

@robertsez
Copy link
Author

الان خیلی بهتره فقط یه چیز User-Agent رو اینجوری بذار تا مشخصات گوشی موبایل باشه.
///// Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML، مانند Gecko) Chrome/114.0.0.0 Mobile Safari/537.36 /////
این واقعی تر است

آیا این را از دستگاه واقعی خود دریافت کردید؟ فکر کنم برای بازی باشه MERGE؟

Hi, yes I saw this while intercepting the app. Of course, you have to increase that part of the Android model. Because it was registered based on my phone model.

The original publisher (delasy) because it was not done from the phone, this error occurred. He announced that he used third-party programs for extraction, I think that's why this discrepancy occurred.

Okay. I've swapped it with your provided User-Agent.

@8wx
Copy link

8wx commented Aug 27, 2024

This just doesn't seem to work. I've tried different game, I get no keys at all.

@8wx
Copy link

8wx commented Aug 27, 2024

Welcome champion! forsenCD ✌️
Choose a game and press the button to get the promo code.
==================================================================
origin: android
Storing new client ID for POLY: b3ab14a1-d91e-4563-ac1c-4ff383b28dd2
Request: https://api.gamepromo.io/promo/login-client {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"appToken\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\",\"clientOrigin\":\"android\",\"clientId\":\"b3ab14a1-d91e-4563-ac1c-4ff383b28dd2\",\"clientVersion\":\"1.15.2\"}"
}
Response: {
  "clientToken": "2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068"
}
Waiting 20 seconds
Registering event #1
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"0dd91150-6d86-4c02-8d2c-33ec372c507c\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 23 seconds
Registering event #2
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"b76ade17-2c36-4e39-a01b-6fcf94a3cfb3\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 24 seconds
Registering event #3
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"d8504f2d-da93-4436-b566-c04fd2014cd9\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 23 seconds
Registering event #4
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"3d75af3a-1d42-4776-b7a4-6a75ee70010b\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 22 seconds
Registering event #5
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"f597a57b-e8fc-4ea1-9f2f-1a05594107f3\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 22 seconds
Registering event #6
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"df844d47-b4f6-45f0-8e38-b92f7e5c1644\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 23 seconds
Registering event #7
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"e4359f45-5d81-4a15-849e-78f3454585ce\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 23 seconds
Registering event #8
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"ed35c135-4e25-47a3-8aab-40c402987714\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 24 seconds
Registering event #9
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"9b0a60b6-8e46-4d7b-be3c-3975fa1868f5\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 23 seconds
Registering event #10
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"93a82c49-498f-464e-b835-3e2b9ecf985c\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 22 seconds
Registering event #11
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"1b9e20d8-8930-4136-b5d6-945675579cb6\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 22 seconds
Registering event #12
Request: https://api.gamepromo.io/promo/register-event {
  "method": "POST",
  "cache": "no-store",
  "headers": {
    "accept": "*/*",
    "accept-encoding": "deflate, gzip",
    "content-type": "application/json",
    "authorization": "Bearer 2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71:android:b3ab14a1-d91e-4563-ac1c-4ff383b28dd2:8BRNOBzUcMG:1724775259068",
    "user-agent": "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)",
    "x-unity-version": "2021.3.39f1"
  },
  "body": "{\"eventId\":\"7d3f3596-73f5-4b59-9c33-b5d1cdd438d5\",\"eventOrigin\":\"undefined\",\"eventType\":\"test\",\"promoId\":\"2aaf5aee-2cbc-47ec-8a3f-0962cc14bc71\"}"
}
Response: {
  "hasCode": false
}
Waiting 22 seconds

@robertsez
Copy link
Author

This just doesn't seem to work. I've tried different game, I get no keys at all.

I've just checked and got POLY key in 6 minutes with 16 events. You have to either:

  1. Wait longer (as per your logs you only waited for 12 events)
  2. Lower the delay between events (I would not suggest that, but that will speed it up)
  3. If the previous two options do not fix it, you should clear your browser cache.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment