Reveals all game keys on Humble Bundle download pages with one click
Size
14.6 KB
Version
1.1.10
Created
Dec 8, 2025
Updated
about 1 month ago
1// ==UserScript==
2// @name Humble Bundle Key Revealer
3// @description Reveals all game keys on Humble Bundle download pages with one click
4// @version 1.1.10
5// @match https://*.humblebundle.com/*
6// @icon https://cdn.humblebundle.com/static/hashed/47e474eed38083df699b7dfd8d29d575e3398f1e.ico
7// @grant GM.setClipboard
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('Humble Bundle Key Revealer extension loaded');
13
14 function createRevealAllButton() {
15 // Check if we're on a downloads page with keys
16 const keyFields = document.querySelectorAll('.js-keyfield.keyfield.enabled');
17
18 if (keyFields.length === 0) {
19 console.log('No keys found on this page');
20 return;
21 }
22
23 console.log(`Found ${keyFields.length} keys to reveal`);
24
25 // Find the key container to add our button
26 const keyContainer = document.querySelector('.key-container.wrapper');
27
28 if (!keyContainer) {
29 console.log('Key container not found');
30 return;
31 }
32
33 // Check if button already exists
34 if (document.getElementById('reveal-all-keys-btn')) {
35 console.log('Button already exists');
36 return;
37 }
38
39 // Create the reveal all button
40 const revealButton = document.createElement('button');
41 revealButton.id = 'reveal-all-keys-btn';
42 revealButton.textContent = '🔑 Reveal All Keys';
43 revealButton.style.cssText = `
44 background: linear-gradient(135deg, #cc2929 0%, #a02020 100%);
45 color: white;
46 border: none;
47 padding: 12px 24px;
48 font-size: 16px;
49 font-weight: bold;
50 border-radius: 4px;
51 cursor: pointer;
52 margin-bottom: 20px;
53 box-shadow: 0 2px 4px rgba(0,0,0,0.2);
54 transition: all 0.3s ease;
55 display: inline-block;
56 `;
57
58 // Add hover effect
59 revealButton.addEventListener('mouseenter', () => {
60 revealButton.style.transform = 'translateY(-2px)';
61 revealButton.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
62 });
63
64 revealButton.addEventListener('mouseleave', () => {
65 revealButton.style.transform = 'translateY(0)';
66 revealButton.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
67 });
68
69 // Add click handler to reveal all keys
70 revealButton.addEventListener('click', () => {
71 console.log('Revealing all keys...');
72
73 const keyFieldsToReveal = document.querySelectorAll('.js-keyfield.keyfield.enabled');
74 let revealedCount = 0;
75 const gameKeysData = [];
76
77 keyFieldsToReveal.forEach((keyField, index) => {
78 // Check if key is already revealed (has class 'redeemed')
79 const isAlreadyRevealed = keyField.classList.contains('redeemed');
80
81 if (isAlreadyRevealed) {
82 // Key is already revealed, extract it immediately
83 const keyRedeemer = keyField.closest('.key-redeemer') || keyField.closest('.content-choice');
84 let gameName = 'Unknown Game';
85
86 if (keyRedeemer) {
87 // Try different selectors for game name
88 const nameElement = keyRedeemer.querySelector('.heading-text h4') ||
89 keyRedeemer.querySelector('.content-choice-title');
90 gameName = nameElement?.textContent.trim() || 'Unknown Game';
91 }
92
93 const keyValue = keyField.querySelector('.keyfield-value')?.textContent.trim() || 'Key not found';
94 const cleanGameName = gameName.replace(/Look up on Steam/g, '').trim();
95
96 gameKeysData.push({ name: cleanGameName, key: keyValue });
97 console.log(`Collected (already revealed): ${cleanGameName} - ${keyValue}`);
98 revealedCount++;
99
100 // Check if all keys are collected
101 if (revealedCount === keyFieldsToReveal.length) {
102 copyToClipboard(gameKeysData, revealButton);
103 }
104 } else {
105 // Key needs to be revealed first
106 setTimeout(() => {
107 keyField.click();
108 revealedCount++;
109 console.log(`Revealed key ${revealedCount}/${keyFieldsToReveal.length}`);
110
111 // Wait a bit for the key to be revealed, then extract it
112 setTimeout(() => {
113 // Find the game name from the heading
114 const keyRedeemer = keyField.closest('.key-redeemer');
115 const gameName = keyRedeemer ? keyRedeemer.querySelector('.heading-text h4')?.textContent.trim() : 'Unknown Game';
116
117 // Find the revealed key value - wait for it to change from "Reveal your Steam key"
118 const keyValueElement = keyField.querySelector('.keyfield-value');
119 let keyValue = keyValueElement?.textContent.trim() || 'Key not found';
120
121 // Clean up the game name (remove Steam icon text if present)
122 const cleanGameName = gameName.replace(/Look up on Steam/g, '').trim();
123
124 // Store the game name and key
125 gameKeysData.push({ name: cleanGameName, key: keyValue });
126 console.log(`Collected: ${cleanGameName} - ${keyValue}`);
127
128 // If all keys are collected, copy to clipboard
129 if (gameKeysData.length === keyFieldsToReveal.length) {
130 copyToClipboard(gameKeysData, revealButton);
131 }
132 }, 1000); // Wait 1000ms for key to be revealed
133
134 // Update button text with progress
135 if (revealedCount < keyFieldsToReveal.length) {
136 revealButton.textContent = `Revealing... (${revealedCount}/${keyFieldsToReveal.length})`;
137 }
138 }, index * 200); // 200ms delay between each click
139 }
140 });
141 });
142
143 // Insert the button at the top of the key container
144 keyContainer.insertBefore(revealButton, keyContainer.firstChild);
145 console.log('Reveal All Keys button added successfully');
146 }
147
148 function createRevealAllButtonForChoicePage() {
149 // Check if we're on the Humble Choice page with game tiles
150 const gameTiles = document.querySelectorAll('.content-choice.claimed');
151
152 if (gameTiles.length === 0) {
153 console.log('No game tiles found on this page');
154 return;
155 }
156
157 console.log(`Found ${gameTiles.length} game tiles on Choice page`);
158
159 // Find the content choices wrapper to add our button
160 const choicesWrapper = document.querySelector('.content-choices-wrapper');
161
162 if (!choicesWrapper) {
163 console.log('Content choices wrapper not found');
164 return;
165 }
166
167 // Check if button already exists
168 if (document.getElementById('reveal-all-keys-btn')) {
169 console.log('Button already exists');
170 return;
171 }
172
173 // Create the reveal all button
174 const revealButton = document.createElement('button');
175 revealButton.id = 'reveal-all-keys-btn';
176 revealButton.textContent = '🔑 Copy All Keys';
177 revealButton.style.cssText = `
178 background: linear-gradient(135deg, #cc2929 0%, #a02020 100%);
179 color: white;
180 border: none;
181 padding: 12px 24px;
182 font-size: 16px;
183 font-weight: bold;
184 border-radius: 4px;
185 cursor: pointer;
186 margin: 20px;
187 box-shadow: 0 2px 4px rgba(0,0,0,0.2);
188 transition: all 0.3s ease;
189 display: inline-block;
190 position: relative;
191 z-index: 1000;
192 `;
193
194 // Add hover effect
195 revealButton.addEventListener('mouseenter', () => {
196 revealButton.style.transform = 'translateY(-2px)';
197 revealButton.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
198 });
199
200 revealButton.addEventListener('mouseleave', () => {
201 revealButton.style.transform = 'translateY(0)';
202 revealButton.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
203 });
204
205 // Add click handler to collect all keys by clicking each game
206 revealButton.addEventListener('click', async () => {
207 console.log('Collecting all keys from Choice page by clicking each game...');
208
209 const gameTilesToClick = document.querySelectorAll('.content-choice.claimed .js-open-choice-modal');
210 const gameKeysData = [];
211
212 console.log(`Found ${gameTilesToClick.length} games to click`);
213
214 if (gameTilesToClick.length === 0) {
215 revealButton.textContent = '❌ No games found';
216 setTimeout(() => {
217 revealButton.textContent = '🔑 Copy All Keys';
218 }, 2000);
219 return;
220 }
221
222 // Click each game tile one by one
223 for (let i = 0; i < gameTilesToClick.length; i++) {
224 const tile = gameTilesToClick[i];
225 revealButton.textContent = `Processing... (${i + 1}/${gameTilesToClick.length})`;
226
227 // Click the game tile to open its modal
228 tile.click();
229 console.log(`Clicked game ${i + 1}`);
230
231 // Wait for modal to appear and key to load
232 await new Promise(resolve => setTimeout(resolve, 2500));
233
234 // Find the modal and extract the key
235 const modal = document.querySelector('#site-modal');
236 if (modal) {
237 const gameName = modal.querySelector('.game-name, h2, .modal-title')?.textContent.trim() ||
238 tile.querySelector('.content-choice-title')?.textContent.trim() ||
239 'Unknown Game';
240
241 const keyField = modal.querySelector('.js-keyfield.keyfield.redeemed .keyfield-value');
242 const keyValue = keyField?.textContent.trim() || 'Key not found';
243
244 const cleanGameName = gameName.replace(/Look up on Steam/g, '').trim();
245 gameKeysData.push({ name: cleanGameName, key: keyValue });
246 console.log(`Collected: ${cleanGameName} - ${keyValue}`);
247
248 // Close the modal by clicking outside or close button
249 const closeButton = modal.querySelector('.js-close-modal, .close-button, [aria-label="Close"]');
250 if (closeButton) {
251 closeButton.click();
252 } else {
253 // Click on the modal backdrop to close
254 const backdrop = modal.querySelector('.humblemodal-wrapper');
255 if (backdrop) {
256 backdrop.click();
257 }
258 }
259
260 // Wait for modal to close
261 await new Promise(resolve => setTimeout(resolve, 800));
262 }
263 }
264
265 // All games processed, now copy to clipboard
266 if (gameKeysData.length > 0) {
267 console.log(`Finished collecting ${gameKeysData.length} keys, calling copyToClipboard...`);
268 copyToClipboard(gameKeysData, revealButton);
269 } else {
270 console.log('No keys found');
271 revealButton.textContent = '❌ No keys found';
272 setTimeout(() => {
273 revealButton.textContent = '🔑 Copy All Keys';
274 }, 2000);
275 }
276 });
277
278 // Insert the button at the top of the choices wrapper
279 choicesWrapper.insertBefore(revealButton, choicesWrapper.firstChild);
280 console.log('Copy All Keys button added successfully to Choice page');
281 }
282
283 function copyToClipboard(gameKeysData, revealButton) {
284 const formattedText = gameKeysData.map(item => `${item.name} - ${item.key}`).join('\n');
285
286 // Copy to clipboard
287 GM.setClipboard(formattedText).then(() => {
288 console.log('All keys copied to clipboard!');
289 revealButton.textContent = '✅ Keys Copied to Clipboard!';
290 revealButton.style.background = 'linear-gradient(135deg, #28a745 0%, #1e7e34 100%)';
291
292 // Reset button after 3 seconds
293 setTimeout(() => {
294 revealButton.textContent = '🔑 Copy All Keys';
295 revealButton.style.background = 'linear-gradient(135deg, #cc2929 0%, #a02020 100%)';
296 }, 3000);
297 }).catch(err => {
298 console.error('Failed to copy to clipboard:', err);
299 revealButton.textContent = '✅ All Keys Revealed!';
300 revealButton.style.background = 'linear-gradient(135deg, #28a745 0%, #1e7e34 100%)';
301
302 // Reset button after 3 seconds
303 setTimeout(() => {
304 revealButton.textContent = '🔑 Copy All Keys';
305 revealButton.style.background = 'linear-gradient(135deg, #cc2929 0%, #a02020 100%)';
306 }, 3000);
307 });
308 }
309
310 // Wait for the page to load
311 function init() {
312 // Try to create the button immediately
313 createRevealAllButton();
314 createRevealAllButtonForChoicePage();
315
316 // Also observe for dynamic content loading
317 const observer = new MutationObserver(() => {
318 createRevealAllButton();
319 createRevealAllButtonForChoicePage();
320 });
321
322 // Observe the body for changes
323 if (document.body) {
324 observer.observe(document.body, {
325 childList: true,
326 subtree: true
327 });
328 }
329 }
330
331 // Start when DOM is ready
332 if (document.readyState === 'loading') {
333 document.addEventListener('DOMContentLoaded', init);
334 } else {
335 init();
336 }
337
338})();