🏠 Home 

Old Reddit with New Reddit Profile Pictures - API Key Version

Injects new Reddit profile pictures into Old Reddit and Reddit-Stream.com next to the username. Caches in localstorage. This version requires an API key. Enter your API Key under CLIENT_ID and CLIENT_SECRET or it will not work.

// ==UserScript==
// @name         Old Reddit with New Reddit Profile Pictures - API Key Version
// @namespace    typpi.online
// @version      7.0.7
// @description  Injects new Reddit profile pictures into Old Reddit and Reddit-Stream.com next to the username. Caches in localstorage. This version requires an API key. Enter your API Key under CLIENT_ID and CLIENT_SECRET or it will not work.
// @author       Nick2bad4u
// @match        *://*.reddit.com/*
// @match        *://reddit-stream.com/*
// @connect      reddit.com
// @connect      reddit-stream.com
// @grant        GM_xmlhttpRequest
// @homepageURL  https://github.com/Nick2bad4u/UserStyles
// @license      Unlicense
// @resource     https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @icon         https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @icon64       https://www.google.com/s2/favicons?sz=64&domain=reddit.com
// @run-at       document-start
// @tag          reddit
// ==/UserScript==
(function () {
'use strict';
console.log('Reddit Profile Picture Injector Script loaded');
// Reddit API credentials
/**
* @constant {string} CLIENT_ID
* The client ID used for authentication with Reddit's API.
* This ID is required to make authenticated requests to Reddit's API endpoints.
* Obtain this value by registering your application at https://www.reddit.com/prefs/apps
*/
const CLIENT_ID = 'EnterClientIDHere';
/**
* @constant {string} CLIENT_SECRET
* The client secret key required for Reddit API authentication.
* This key should be kept private and not shared publicly.
* Obtain this value from your Reddit API application settings.
*/
const CLIENT_SECRET = 'EnterClientSecretHere';
/**
* User agent string used for making API requests.
* Format: {ApplicationName}/{Version} by {Author}
* @constant {string}
*/
const USER_AGENT = 'ProfilePictureInjector/7.0.6 by Nick2bad4u';
/**
* Access token retrieved from localStorage for authentication purposes.
* @type {string|null}
*/
let accessToken = localStorage.getItem('accessToken');
// Retrieve cached profile pictures and timestamps from localStorage
/**
* Object containing cached profile picture URLs.
* Data is persisted in localStorage and parsed from JSON.
* @type {Object.<string, string>} Key-value pairs of username to profile picture URL
*/
let profilePictureCache = JSON.parse(
localStorage.getItem('profilePictureCache') || '{}',
);
/**
* Object storing timestamps for cached items.
* Retrieved from localStorage, defaults to empty object if not found.
* @type {Object.<string, number>}
*/
let cacheTimestamps = JSON.parse(
localStorage.getItem('cacheTimestamps') || '{}',
);
/**
* Duration in milliseconds for which profile picture data will be cached.
* Set to 7 days to balance between API rate limits and data freshness.
* @constant
* @type {number}
*/
const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
/**
* Maximum number of entries that can be stored in the cache.
* Prevents memory overflow by limiting cache size.
* @constant {number}
*/
const MAX_CACHE_SIZE = 100000; // Maximum number of cache entries
/**
* Array of keys from the profilePictureCache object representing cached profile picture entries
* @type {string[]}
* @const
*/
const cacheEntries = Object.keys(profilePictureCache);
// Rate limit variables
/**
* Remaining number of API requests allowed before hitting rate limit
* @type {number}
* @default 1000
*/
let rateLimitRemaining = 1000;
/**
* Unix timestamp indicating when the Reddit API rate limit will reset
* @type {number}
*/
let rateLimitResetTime = 0;
/**
* Date object representing when the rate limit will reset
* @type {Date}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const resetDate = new Date(rateLimitResetTime);
/**
* Current timestamp in milliseconds since January 1, 1970 00:00:00 UTC.
* @type {number}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const now = Date.now();
// Save the cache to localStorage
/**
* Saves the profile picture cache and cache timestamps to localStorage.
* The cache is stored as stringified JSON under 'profilePictureCache' key,
* and timestamps are stored under 'cacheTimestamps' key.
*/
function saveCache() {
localStorage.setItem(
'profilePictureCache',
JSON.stringify(profilePictureCache),
);
localStorage.setItem('cacheTimestamps', JSON.stringify(cacheTimestamps));
}
// Remove old cache entries
/**
* Removes expired entries from the Reddit profile picture URL cache.
* Iterates through cached usernames and removes entries older than CACHE_DURATION.
* After cleaning expired entries, saves the updated cache to storage.
*
* @function flushOldCache
* @returns {void}
*
* @requires CACHE_DURATION - Maximum age of cache entries in milliseconds
* @requires cacheTimestamps - Object storing timestamps for each cached username
* @requires profilePictureCache - Object storing profile picture URLs by username
* @requires saveCache - Function to persist the cache to storage
*/
function flushOldCache() {
console.log('Flushing old Reddit profile picture URL cache');
const now = Date.now();
for (const username in cacheTimestamps) {
if (now - cacheTimestamps[username] > CACHE_DURATION) {
console.log(`Deleting cache for Reddit user - ${username}`);
delete profilePictureCache[username];
delete cacheTimestamps[username];
}
}
saveCache();
console.log('Old cache entries flushed');
}
// Limit the size of the cache to the maximum allowed entries
/**
* Manages the size of the profile picture cache by removing oldest entries when the maximum size is exceeded.
* Sorts entries by timestamp and removes the oldest ones until the cache size is within the specified limit.
* After removal, saves the updated cache to persistent storage.
*
* @function limitCacheSize
* @returns {void}
*
* @uses profilePictureCache - Global object storing profile picture URLs
* @uses cacheTimestamps - Global object storing timestamps for each cache entry
* @uses MAX_CACHE_SIZE - Global constant defining maximum number of entries allowed in cache
* @uses saveCache - Function to persist the cache to storage
*/
function limitCacheSize() {
const cacheEntries = Object.keys(profilePictureCache);
if (cacheEntries.length > MAX_CACHE_SIZE) {
console.log(`Current cache size: ${cacheEntries.length} URLs`);
console.log('Cache size exceeded, removing oldest entries');
const sortedEntries = cacheEntries.sort(
(a, b) => cacheTimestamps[a] - cacheTimestamps[b],
);
const entriesToRemove = sortedEntries.slice(
0,
cacheEntries.length - MAX_CACHE_SIZE,
);
entriesToRemove.forEach((username) => {
delete profilePictureCache[username];
delete cacheTimestamps[username];
});
saveCache();
console.log(
`Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`,
);
}
}
/**
* Calculates the total size in bytes of the profile picture cache and its timestamps.
* The size is estimated by serializing cache entries to JSON and measuring their byte length.
* Each cache entry consists of picture data and timestamp data for a username.
* @returns {number} The total size of the cache in bytes
*/
function getCacheSizeInBytes() {
const cacheEntries = Object.keys(profilePictureCache);
let totalSize = 0;
// Calculate size of profilePictureCache
cacheEntries.forEach((username) => {
const pictureData = profilePictureCache[username];
const timestampData = cacheTimestamps[username];
// Estimate size of data by serializing to JSON and getting the length
totalSize += new TextEncoder().encode(JSON.stringify(pictureData)).length;
totalSize += new TextEncoder().encode(
JSON.stringify(timestampData),
).length;
});
return totalSize; // in bytes
}
/**
* Calculates the current cache size in megabytes.
* @returns {number} The size of the cache in megabytes (MB)
*/
function getCacheSizeInMB() {
return getCacheSizeInBytes() / (#### * ####); // Convert bytes to MB
}
/**
* Calculates the cache size in kilobytes (KB).
* @returns {number} The size of the cache in KB, calculated by dividing the size in bytes by ####.
*/
function getCacheSizeInKB() {
return getCacheSizeInBytes() / ####; // Convert bytes to KB
}
// Obtain an access token from Reddit API
/**
* Obtains an access token from Reddit's API using client credentials.
* The token is stored in localStorage along with its expiration time.
*
* @async
* @function getAccessToken
* @returns {Promise<string|null>} The access token if successful, null if the request fails
* @throws {Error} When the network request fails
*
* @example
* const token = await getAccessToken();
* if (token) {
*   // Use token for authenticated requests
* }
*/
async function getAccessToken() {
console.log('Obtaining access token');
const credentials = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
try {
const response = await fetch(
'https://www.reddit.com/api/v1/access_token',
{
method: 'POST',
headers: {
Authorization: `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials',
},
);
if (!response.ok) {
console.error('Failed to obtain access token:', response.statusText);
return null;
}
const data = await response.json();
accessToken = data.access_token;
const expiration = Date.now() + data.expires_in * 1000;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('tokenExpiration', expiration.toString());
console.log('Access token obtained and saved');
return accessToken;
} catch (error) {
console.error('Error obtaining access token:', error);
return null;
}
}
// Fetch profile pictures for a list of usernames
/**
* Fetches profile pictures for a list of Reddit usernames using Reddit's OAuth API
* @async
* @param {string[]} usernames - Array of Reddit usernames to fetch profile pictures for
* @returns {Promise<(string|null)[]>} Array of profile picture URLs corresponding to the input usernames. Returns null for usernames where fetching failed
* @description
* - Handles rate limiting by waiting when limit is reached
* - Manages OAuth token refresh when expired
* - Caches profile pictures to avoid redundant API calls
* - Filters out [deleted] and [removed] usernames
* - Updates rate limit tracking based on API response headers
* - Saves fetched profile pictures to cache
* @throws {Error} Possible network or API errors during fetch operations
*/
async function fetchProfilePictures(usernames) {
console.log('Fetching profile pictures');
const now = Date.now();
const tokenExpiration = parseInt(
localStorage.getItem('tokenExpiration'),
10,
);
// Check rate limit
if (rateLimitRemaining <= 0 && now < rateLimitResetTime) {
console.warn('Rate limit reached. Waiting until reset...');
const timeRemaining = rateLimitResetTime - now;
const minutesRemaining = Math.floor(timeRemaining / 60000);
const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
console.log(
`Rate limit will reset in ${minutesRemaining} minutes and ${secondsRemaining} seconds.`,
);
await new Promise((resolve) =>
setTimeout(resolve, rateLimitResetTime - now),
);
}
// Refresh access token if expired
if (!accessToken || now > tokenExpiration) {
accessToken = await getAccessToken();
if (!accessToken) return null;
}
// Filter out cached usernames
const uncachedUsernames = usernames.filter(
(username) =>
!profilePictureCache[username] &&
username !== '[deleted]' &&
username !== '[removed]',
);
if (uncachedUsernames.length === 0) {
console.log('All usernames are cached');
return usernames.map((username) => profilePictureCache[username]);
}
// Fetch profile pictures for uncached usernames
/**
* Array of promises that fetch profile pictures for uncached Reddit usernames using Reddit's OAuth API
* @type {Promise<(string|null)>[]}
* @description Each promise attempts to:
* 1. Fetch user data from Reddit's OAuth API
* 2. Extract and cache the profile picture URL
* 3. Update rate limit tracking
* 4. Handle errors gracefully
* @returns {Promise<(string|null)>[]} Array of promises that resolve to either:
* - Profile picture URLs (string) for successful fetches
* - null for failed fetches or users without profile pictures
* @throws {Error} Individual promises may throw network or API errors, but these are caught and handled
*/
const fetchPromises = uncachedUsernames.map(async (username) => {
try {
const response = await fetch(
`https://oauth.reddit.com/user/${username}/about`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'User-Agent': USER_AGENT,
},
},
);
// Update rate limit
rateLimitRemaining =
parseInt(response.headers.get('x-ratelimit-remaining')) ||
rateLimitRemaining;
rateLimitResetTime =
now + parseInt(response.headers.get('x-ratelimit-reset')) * 1000 ||
rateLimitResetTime;
// Log rate limit information
const timeRemaining = rateLimitResetTime - now;
const minutesRemaining = Math.floor(timeRemaining / 60000);
const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
console.log(
`Rate Limit Requests Remaining: ${rateLimitRemaining}, 1000 more requests will be added in ${minutesRemaining} minutes and ${secondsRemaining} seconds`,
);
if (!response.ok) {
console.error(
`Error fetching profile picture for ${username}: ${response.statusText}`,
);
return null;
}
const data = await response.json();
if (data.data && data.data.icon_img) {
const profilePictureUrl = data.data.icon_img.split('?')[0];
profilePictureCache[username] = profilePictureUrl;
cacheTimestamps[username] = Date.now();
saveCache();
console.log(`Fetched profile picture: ${username}`);
return profilePictureUrl;
} else {
console.warn(`No profile picture found for: ${username}`);
return null;
}
} catch (error) {
console.error(`Error fetching profile picture for ${username}:`, error);
return null;
}
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const r###lts = await Promise.all(fetchPromises);
limitCacheSize();
return usernames.map((username) => profilePictureCache[username]);
}
/**
* Injects profile pictures next to user comments and adds hover functionality for enlarged views
* @async
* @param {NodeList} comments - NodeList of comment elements to process
* @returns {Promise<void>}
*
* @description
* This function:
* 1. Extracts usernames from comments, filtering out deleted/removed users
* 2. Fetches profile picture URLs for valid usernames
* 3. Creates and injects profile picture elements before each comment
* 4. Adds click handlers to open full-size images in new tabs
* 5. Implements hover functionality to show enlarged previews
* 6. Tracks injection count and logs cache statistics
* 7. Reports rate limit status for API calls
*
* @requires fetchProfilePictures - External function to retrieve profile picture URLs
* @requires cacheEntries - Global array tracking cached URLs
* @requires MAX_CACHE_SIZE - Global constant for maximum cache size
* @requires rateLimitResetTime - Global variable tracking API rate limit reset time
* @requires rateLimitRemaining - Global variable tracking remaining API calls
* @requires getCacheSizeInMB - Function to calculate cache size in megabytes
* @requires getCacheSizeInKB - Function to calculate cache size in kilobytes
*/
async function injectProfilePictures(comments) {
console.log(`Comments found: ${comments.length}`);
const usernames = Array.from(comments)
.map((comment) => comment.textContent.trim())
.filter(
(username) => username !== '[deleted]' && username !== '[removed]',
);
const profilePictureUrls = await fetchProfilePictures(usernames);
let injectedCount = 0; // Counter for injected profile pictures
comments.forEach((comment, index) => {
const username = usernames[index];
const profilePictureUrl = profilePictureUrls[index];
if (
profilePictureUrl &&
!comment.previousElementSibling?.classList.contains('profile-picture')
) {
console.log(`Injecting profile picture: ${username}`);
const img = document.createElement('img');
img.src = profilePictureUrl;
img.classList.add('profile-picture');
img.onerror = () => {
img.style.display = 'none';
};
img.addEventListener('click', () => {
window.open(profilePictureUrl, '_blank');
});
comment.insertAdjacentElement('beforebegin', img);
const enlargedImg = document.createElement('img');
enlargedImg.src = profilePictureUrl;
enlargedImg.classList.add('enlarged-profile-picture');
document.body.appendChild(enlargedImg);
img.addEventListener('mouseover', () => {
enlargedImg.style.display = 'block';
const rect = img.getBoundingClientRect();
enlargedImg.style.top = `${rect.top + window.scrollY + 20}px`;
enlargedImg.style.left = `${rect.left + window.scrollX + 20}px`;
});
img.addEventListener('mouseout', () => {
enlargedImg.style.display = 'none';
});
injectedCount++; // Increment count after successful injection
}
});
console.log(`Profile pictures injected this run: ${injectedCount}`);
console.log(`Current cache size: ${cacheEntries.length}`);
console.log(
`Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`,
);
const currentCacheSizeMB = getCacheSizeInMB();
const currentCacheSizeKB = getCacheSizeInKB();
console.log(
`Current cache size: ${currentCacheSizeMB.toFixed(2)} MB or ${currentCacheSizeKB.toFixed(2)} KB`,
);
const timeRemaining = rateLimitResetTime - Date.now();
const minutesRemaining = Math.floor(timeRemaining / 60000);
const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
console.log(
`Rate Limit Requests Remaining: ${rateLimitRemaining} requests, refresh in ${minutesRemaining} minutes and ${secondsRemaining} seconds`,
);
}
/**
* Sets up a MutationObserver to watch for new comments on Reddit.
* The observer looks for elements with class 'author' or 'c-username'.
* When new comments are detected, it disconnects the observer and
* injects profile pictures into the found elements.
*
* The observer monitors the entire document body for DOM changes,
* including nested elements.
*
* @function setupObserver
*/
function setupObserver() {
console.log('Setting up observer');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const observer = new MutationObserver((mutations) => {
const comments = document.querySelectorAll('.author, .c-username');
if (comments.length > 0) {
console.log('New comments detected');
observer.disconnect();
injectProfilePictures(comments);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
console.log('Observer initialized');
}
// Run the script
/**
* Initializes and runs the main script functionality.
* This function performs the following operations:
* 1. Clears outdated cache entries
* 2. Logs the current state of the profile picture cache
* 3. Initializes the DOM observer
* @function runScript
*/
function runScript() {
flushOldCache();
console.log('Cache loaded:', profilePictureCache);
setupObserver();
}
window.addEventListener('load', () => {
console.log('Page loaded');
runScript();
});
// Add CSS styles for profile pictures
/**
* Creates a new style element to be injected into the document
* @type {HTMLStyleElement}
*/
const style = document.createElement('style');
style.textContent = `
.profile-picture {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 5px;
transition: transform 0.2s ease-in-out;
position: relative;
z-index: 1;
cursor: pointer;
}
.enlarged-profile-picture {
width: 250px;
height: 250px;
border-radius: 50%;
position: absolute;
display: none;
z-index: 1000;
pointer-events: none;
outline: 3px solid #000;
box-shadow: 0 4px 8px rgba(0, 0, 0, 1);
background-color: rgba(0, 0, 0, 1);
}
`;
document.head.appendChild(style);
})();