Last active
December 15, 2025 10:14
-
-
Save corentinbettiol/4434860f4715f5d6e9e240ed3e3198d3 to your computer and use it in GitHub Desktop.
Greysemonkey script that displays karma of user next to it's username, using hn firebase api & caching (24h).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Hacker News – Display user karma next to usernames (WCAG-friendly) | |
| // @namespace https://l3m.in | |
| // @version 1.2 | |
| // @description Display Hacker News user karma next to usernames using the HN Firebase API, with caching, rate limiting and WCAG-friendly colors. | |
| // @match https://news.ycombinator.com/* | |
| // @grant GM.getValue | |
| // @grant GM.setValue | |
| // @grant GM.xmlHttpRequest | |
| // @connect hacker-news.firebaseio.com | |
| // ==/UserScript== | |
| (async () => { | |
| 'use strict'; | |
| /* ========================= | |
| Configuration | |
| ========================== */ | |
| const API_URL = 'https://hacker-news.firebaseio.com/v0/user/'; | |
| const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours | |
| const REQUEST_DELAY = 1000; // 1 request per second | |
| /* ========================= | |
| Internal state | |
| ========================== */ | |
| let queue = []; | |
| let processing = false; | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| /* ========================= | |
| Cache helpers | |
| ========================== */ | |
| async function getCachedUser(username) { | |
| const entry = await GM.getValue(`user_${username}`, null); | |
| if (!entry) return null; | |
| if (Date.now() - entry.timestamp > CACHE_TTL) return null; | |
| return entry.karma; | |
| } | |
| async function setCachedUser(username, karma) { | |
| await GM.setValue(`user_${username}`, { | |
| karma, | |
| timestamp: Date.now() | |
| }); | |
| } | |
| /* ========================= | |
| API call | |
| ========================== */ | |
| function fetchUser(username) { | |
| return new Promise((resolve, reject) => { | |
| GM.xmlHttpRequest({ | |
| method: 'GET', | |
| url: `${API_URL}${username}.json`, | |
| onload: async res => { | |
| try { | |
| const data = JSON.parse(res.responseText); | |
| if (data && typeof data.karma === 'number') { | |
| await setCachedUser(username, data.karma); | |
| resolve(data.karma); | |
| } else { | |
| reject('Invalid user data'); | |
| } | |
| } catch (e) { | |
| reject(e); | |
| } | |
| }, | |
| onerror: reject | |
| }); | |
| }); | |
| } | |
| /* ========================= | |
| Color & visual helpers | |
| ========================== */ | |
| // Base color interpolation (dark gray -> dark amber) | |
| // Chosen to keep sufficient contrast on a light background | |
| function karmaBaseColor(karma) { | |
| const max = 1500; | |
| const t = Math.min(Math.max(karma, 0), max) / max; | |
| // #555555 -> #8a5a00 (WCAG-friendly on white) | |
| const r = Math.round(85 + (138 - 85) * t); | |
| const g = Math.round(85 + (90 - 85) * t); | |
| const b = Math.round(85 + (0 - 85) * t); | |
| return `rgb(${r}, ${g}, ${b})`; | |
| } | |
| function applyTierEffects(span, karma) { | |
| if (karma >= 1500 && karma <= 5000) { | |
| // Bronze (darkened for contrast) | |
| span.style.textShadow = '0 0 2px #7a4a12'; | |
| } else if (karma >= 5001 && karma <= 15000) { | |
| // Silver (neutral gray) | |
| span.style.textShadow = '0 0 2px #6b6b6b'; | |
| } else if (karma >= 15001 && karma <= 30000) { | |
| // Gold (dark gold, readable) | |
| span.style.color = '#c99a00'; | |
| span.style.textShadow = '0 1px solid #666, 0 0 3px #af8600'; | |
| } else if (karma >= 30001) { | |
| // High karma: readable gold + subtle animated glow | |
| span.style.color = '#c29500'; | |
| span.style.animation = 'hnGlowWCAG 2.5s ease-in-out infinite alternate'; | |
| } | |
| } | |
| /* ========================= | |
| DOM manipulation | |
| ========================== */ | |
| function appendKarma(el, karma) { | |
| if (el.dataset.karmaAdded) return; | |
| el.dataset.karmaAdded = '1'; | |
| const span = document.createElement('span'); | |
| span.textContent = ` (${karma})`; | |
| span.style.fontSize = '0.85em'; | |
| span.style.marginLeft = '2px'; | |
| span.style.color = karmaBaseColor(karma); | |
| applyTierEffects(span, karma); | |
| el.after(span); | |
| } | |
| /* ========================= | |
| Queue processing | |
| ========================== */ | |
| async function processQueue() { | |
| if (processing) return; | |
| processing = true; | |
| while (queue.length) { | |
| const { username, elements } = queue.shift(); | |
| try { | |
| const karma = await fetchUser(username); | |
| elements.forEach(el => appendKarma(el, karma)); | |
| } catch (e) { | |
| console.error('Karma fetch error:', username, e); | |
| } | |
| await sleep(REQUEST_DELAY); | |
| } | |
| processing = false; | |
| } | |
| /* ========================= | |
| Scan page for usernames | |
| ========================== */ | |
| async function scanUsers() { | |
| const links = Array.from(document.querySelectorAll('a[href^="user?id="]')); | |
| const map = new Map(); | |
| for (const link of links) { | |
| const username = link.textContent.trim(); | |
| if (!username) continue; | |
| const cached = await getCachedUser(username); | |
| if (cached !== null) { | |
| appendKarma(link, cached); | |
| } else { | |
| if (!map.has(username)) map.set(username, []); | |
| map.get(username).push(link); | |
| } | |
| } | |
| map.forEach((elements, username) => { | |
| queue.push({ username, elements }); | |
| }); | |
| processQueue(); | |
| } | |
| /* ========================= | |
| Global styles | |
| ========================== */ | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| @keyframes hnGlowWCAG { | |
| from { | |
| text-shadow: | |
| 0 1px #3d2f00, | |
| 0 0 4px #ac8400; | |
| } | |
| to { | |
| text-shadow: | |
| 0 1px #3d2f00, | |
| 0 0 10px #e6c24c; | |
| } | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| /* ========================= | |
| Init | |
| ========================== */ | |
| await scanUsers(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment