Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save corentinbettiol/4434860f4715f5d6e9e240ed3e3198d3 to your computer and use it in GitHub Desktop.

Select an option

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).
// ==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