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.

  1. // ==UserScript==
  2. // @name Old Reddit with New Reddit Profile Pictures - API Key Version
  3. // @namespace typpi.online
  4. // @version 7.0.7
  5. // @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.
  6. // @author Nick2bad4u
  7. // @match *://*.reddit.com/*
  8. // @match *://reddit-stream.com/*
  9. // @connect reddit.com
  10. // @connect reddit-stream.com
  11. // @grant GM_xmlhttpRequest
  12. // @homepageURL https://github.com/Nick2bad4u/UserStyles
  13. // @license Unlicense
  14. // @resource https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  15. // @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  16. // @icon64 https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  17. // @run-at document-start
  18. // @tag reddit
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23. console.log('Reddit Profile Picture Injector Script loaded');
  24.  
  25. // Reddit API credentials
  26. /**
  27. * @constant {string} CLIENT_ID
  28. * The client ID used for authentication with Reddit's API.
  29. * This ID is required to make authenticated requests to Reddit's API endpoints.
  30. * Obtain this value by registering your application at https://www.reddit.com/prefs/apps
  31. */
  32. const CLIENT_ID = 'EnterClientIDHere';
  33. /**
  34. * @constant {string} CLIENT_SECRET
  35. * The client secret key required for Reddit API authentication.
  36. * This key should be kept private and not shared publicly.
  37. * Obtain this value from your Reddit API application settings.
  38. */
  39. const CLIENT_SECRET = 'EnterClientSecretHere';
  40. /**
  41. * User agent string used for making API requests.
  42. * Format: {ApplicationName}/{Version} by {Author}
  43. * @constant {string}
  44. */
  45. const USER_AGENT = 'ProfilePictureInjector/7.0.6 by Nick2bad4u';
  46. /**
  47. * Access token retrieved from localStorage for authentication purposes.
  48. * @type {string|null}
  49. */
  50. let accessToken = localStorage.getItem('accessToken');
  51.  
  52. // Retrieve cached profile pictures and timestamps from localStorage
  53. /**
  54. * Object containing cached profile picture URLs.
  55. * Data is persisted in localStorage and parsed from JSON.
  56. * @type {Object.<string, string>} Key-value pairs of username to profile picture URL
  57. */
  58. let profilePictureCache = JSON.parse(
  59. localStorage.getItem('profilePictureCache') || '{}',
  60. );
  61. /**
  62. * Object storing timestamps for cached items.
  63. * Retrieved from localStorage, defaults to empty object if not found.
  64. * @type {Object.<string, number>}
  65. */
  66. let cacheTimestamps = JSON.parse(
  67. localStorage.getItem('cacheTimestamps') || '{}',
  68. );
  69. /**
  70. * Duration in milliseconds for which profile picture data will be cached.
  71. * Set to 7 days to balance between API rate limits and data freshness.
  72. * @constant
  73. * @type {number}
  74. */
  75. const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
  76. /**
  77. * Maximum number of entries that can be stored in the cache.
  78. * Prevents memory overflow by limiting cache size.
  79. * @constant {number}
  80. */
  81. const MAX_CACHE_SIZE = 100000; // Maximum number of cache entries
  82. /**
  83. * Array of keys from the profilePictureCache object representing cached profile picture entries
  84. * @type {string[]}
  85. * @const
  86. */
  87. const cacheEntries = Object.keys(profilePictureCache);
  88.  
  89. // Rate limit variables
  90. /**
  91. * Remaining number of API requests allowed before hitting rate limit
  92. * @type {number}
  93. * @default 1000
  94. */
  95. let rateLimitRemaining = 1000;
  96. /**
  97. * Unix timestamp indicating when the Reddit API rate limit will reset
  98. * @type {number}
  99. */
  100. let rateLimitResetTime = 0;
  101. /**
  102. * Date object representing when the rate limit will reset
  103. * @type {Date}
  104. */
  105. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  106. const resetDate = new Date(rateLimitResetTime);
  107. /**
  108. * Current timestamp in milliseconds since January 1, 1970 00:00:00 UTC.
  109. * @type {number}
  110. */
  111. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  112. const now = Date.now();
  113.  
  114. // Save the cache to localStorage
  115. /**
  116. * Saves the profile picture cache and cache timestamps to localStorage.
  117. * The cache is stored as stringified JSON under 'profilePictureCache' key,
  118. * and timestamps are stored under 'cacheTimestamps' key.
  119. */
  120. function saveCache() {
  121. localStorage.setItem(
  122. 'profilePictureCache',
  123. JSON.stringify(profilePictureCache),
  124. );
  125. localStorage.setItem('cacheTimestamps', JSON.stringify(cacheTimestamps));
  126. }
  127.  
  128. // Remove old cache entries
  129. /**
  130. * Removes expired entries from the Reddit profile picture URL cache.
  131. * Iterates through cached usernames and removes entries older than CACHE_DURATION.
  132. * After cleaning expired entries, saves the updated cache to storage.
  133. *
  134. * @function flushOldCache
  135. * @returns {void}
  136. *
  137. * @requires CACHE_DURATION - Maximum age of cache entries in milliseconds
  138. * @requires cacheTimestamps - Object storing timestamps for each cached username
  139. * @requires profilePictureCache - Object storing profile picture URLs by username
  140. * @requires saveCache - Function to persist the cache to storage
  141. */
  142. function flushOldCache() {
  143. console.log('Flushing old Reddit profile picture URL cache');
  144. const now = Date.now();
  145. for (const username in cacheTimestamps) {
  146. if (now - cacheTimestamps[username] > CACHE_DURATION) {
  147. console.log(`Deleting cache for Reddit user - ${username}`);
  148. delete profilePictureCache[username];
  149. delete cacheTimestamps[username];
  150. }
  151. }
  152. saveCache();
  153. console.log('Old cache entries flushed');
  154. }
  155.  
  156. // Limit the size of the cache to the maximum allowed entries
  157. /**
  158. * Manages the size of the profile picture cache by removing oldest entries when the maximum size is exceeded.
  159. * Sorts entries by timestamp and removes the oldest ones until the cache size is within the specified limit.
  160. * After removal, saves the updated cache to persistent storage.
  161. *
  162. * @function limitCacheSize
  163. * @returns {void}
  164. *
  165. * @uses profilePictureCache - Global object storing profile picture URLs
  166. * @uses cacheTimestamps - Global object storing timestamps for each cache entry
  167. * @uses MAX_CACHE_SIZE - Global constant defining maximum number of entries allowed in cache
  168. * @uses saveCache - Function to persist the cache to storage
  169. */
  170. function limitCacheSize() {
  171. const cacheEntries = Object.keys(profilePictureCache);
  172. if (cacheEntries.length > MAX_CACHE_SIZE) {
  173. console.log(`Current cache size: ${cacheEntries.length} URLs`);
  174. console.log('Cache size exceeded, removing oldest entries');
  175. const sortedEntries = cacheEntries.sort(
  176. (a, b) => cacheTimestamps[a] - cacheTimestamps[b],
  177. );
  178. const entriesToRemove = sortedEntries.slice(
  179. 0,
  180. cacheEntries.length - MAX_CACHE_SIZE,
  181. );
  182. entriesToRemove.forEach((username) => {
  183. delete profilePictureCache[username];
  184. delete cacheTimestamps[username];
  185. });
  186. saveCache();
  187. console.log(
  188. `Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`,
  189. );
  190. }
  191. }
  192.  
  193. /**
  194. * Calculates the total size in bytes of the profile picture cache and its timestamps.
  195. * The size is estimated by serializing cache entries to JSON and measuring their byte length.
  196. * Each cache entry consists of picture data and timestamp data for a username.
  197. * @returns {number} The total size of the cache in bytes
  198. */
  199. function getCacheSizeInBytes() {
  200. const cacheEntries = Object.keys(profilePictureCache);
  201. let totalSize = 0;
  202.  
  203. // Calculate size of profilePictureCache
  204. cacheEntries.forEach((username) => {
  205. const pictureData = profilePictureCache[username];
  206. const timestampData = cacheTimestamps[username];
  207.  
  208. // Estimate size of data by serializing to JSON and getting the length
  209. totalSize += new TextEncoder().encode(JSON.stringify(pictureData)).length;
  210. totalSize += new TextEncoder().encode(
  211. JSON.stringify(timestampData),
  212. ).length;
  213. });
  214.  
  215. return totalSize; // in bytes
  216. }
  217.  
  218. /**
  219. * Calculates the current cache size in megabytes.
  220. * @returns {number} The size of the cache in megabytes (MB)
  221. */
  222. function getCacheSizeInMB() {
  223. return getCacheSizeInBytes() / (1024 * 1024); // Convert bytes to MB
  224. }
  225.  
  226. /**
  227. * Calculates the cache size in kilobytes (KB).
  228. * @returns {number} The size of the cache in KB, calculated by dividing the size in bytes by 1024.
  229. */
  230. function getCacheSizeInKB() {
  231. return getCacheSizeInBytes() / 1024; // Convert bytes to KB
  232. }
  233.  
  234. // Obtain an access token from Reddit API
  235. /**
  236. * Obtains an access token from Reddit's API using client credentials.
  237. * The token is stored in localStorage along with its expiration time.
  238. *
  239. * @async
  240. * @function getAccessToken
  241. * @returns {Promise<string|null>} The access token if successful, null if the request fails
  242. * @throws {Error} When the network request fails
  243. *
  244. * @example
  245. * const token = await getAccessToken();
  246. * if (token) {
  247. * // Use token for authenticated requests
  248. * }
  249. */
  250. async function getAccessToken() {
  251. console.log('Obtaining access token');
  252. const credentials = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
  253. try {
  254. const response = await fetch(
  255. 'https://www.reddit.com/api/v1/access_token',
  256. {
  257. method: 'POST',
  258. headers: {
  259. Authorization: `Basic ${credentials}`,
  260. 'Content-Type': 'application/x-www-form-urlencoded',
  261. },
  262. body: 'grant_type=client_credentials',
  263. },
  264. );
  265. if (!response.ok) {
  266. console.error('Failed to obtain access token:', response.statusText);
  267. return null;
  268. }
  269. const data = await response.json();
  270. accessToken = data.access_token;
  271. const expiration = Date.now() + data.expires_in * 1000;
  272. localStorage.setItem('accessToken', accessToken);
  273. localStorage.setItem('tokenExpiration', expiration.toString());
  274. console.log('Access token obtained and saved');
  275. return accessToken;
  276. } catch (error) {
  277. console.error('Error obtaining access token:', error);
  278. return null;
  279. }
  280. }
  281.  
  282. // Fetch profile pictures for a list of usernames
  283. /**
  284. * Fetches profile pictures for a list of Reddit usernames using Reddit's OAuth API
  285. * @async
  286. * @param {string[]} usernames - Array of Reddit usernames to fetch profile pictures for
  287. * @returns {Promise<(string|null)[]>} Array of profile picture URLs corresponding to the input usernames. Returns null for usernames where fetching failed
  288. * @description
  289. * - Handles rate limiting by waiting when limit is reached
  290. * - Manages OAuth token refresh when expired
  291. * - Caches profile pictures to avoid redundant API calls
  292. * - Filters out [deleted] and [removed] usernames
  293. * - Updates rate limit tracking based on API response headers
  294. * - Saves fetched profile pictures to cache
  295. * @throws {Error} Possible network or API errors during fetch operations
  296. */
  297. async function fetchProfilePictures(usernames) {
  298. console.log('Fetching profile pictures');
  299. const now = Date.now();
  300. const tokenExpiration = parseInt(
  301. localStorage.getItem('tokenExpiration'),
  302. 10,
  303. );
  304.  
  305. // Check rate limit
  306. if (rateLimitRemaining <= 0 && now < rateLimitResetTime) {
  307. console.warn('Rate limit reached. Waiting until reset...');
  308.  
  309. const timeRemaining = rateLimitResetTime - now;
  310. const minutesRemaining = Math.floor(timeRemaining / 60000);
  311. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  312.  
  313. console.log(
  314. `Rate limit will reset in ${minutesRemaining} minutes and ${secondsRemaining} seconds.`,
  315. );
  316. await new Promise((resolve) =>
  317. setTimeout(resolve, rateLimitResetTime - now),
  318. );
  319. }
  320.  
  321. // Refresh access token if expired
  322. if (!accessToken || now > tokenExpiration) {
  323. accessToken = await getAccessToken();
  324. if (!accessToken) return null;
  325. }
  326.  
  327. // Filter out cached usernames
  328. const uncachedUsernames = usernames.filter(
  329. (username) =>
  330. !profilePictureCache[username] &&
  331. username !== '[deleted]' &&
  332. username !== '[removed]',
  333. );
  334. if (uncachedUsernames.length === 0) {
  335. console.log('All usernames are cached');
  336. return usernames.map((username) => profilePictureCache[username]);
  337. }
  338.  
  339. // Fetch profile pictures for uncached usernames
  340. /**
  341. * Array of promises that fetch profile pictures for uncached Reddit usernames using Reddit's OAuth API
  342. * @type {Promise<(string|null)>[]}
  343. * @description Each promise attempts to:
  344. * 1. Fetch user data from Reddit's OAuth API
  345. * 2. Extract and cache the profile picture URL
  346. * 3. Update rate limit tracking
  347. * 4. Handle errors gracefully
  348. * @returns {Promise<(string|null)>[]} Array of promises that resolve to either:
  349. * - Profile picture URLs (string) for successful fetches
  350. * - null for failed fetches or users without profile pictures
  351. * @throws {Error} Individual promises may throw network or API errors, but these are caught and handled
  352. */
  353. const fetchPromises = uncachedUsernames.map(async (username) => {
  354. try {
  355. const response = await fetch(
  356. `https://oauth.reddit.com/user/${username}/about`,
  357. {
  358. headers: {
  359. Authorization: `Bearer ${accessToken}`,
  360. 'User-Agent': USER_AGENT,
  361. },
  362. },
  363. );
  364.  
  365. // Update rate limit
  366. rateLimitRemaining =
  367. parseInt(response.headers.get('x-ratelimit-remaining')) ||
  368. rateLimitRemaining;
  369. rateLimitResetTime =
  370. now + parseInt(response.headers.get('x-ratelimit-reset')) * 1000 ||
  371. rateLimitResetTime;
  372.  
  373. // Log rate limit information
  374. const timeRemaining = rateLimitResetTime - now;
  375. const minutesRemaining = Math.floor(timeRemaining / 60000);
  376. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  377.  
  378. console.log(
  379. `Rate Limit Requests Remaining: ${rateLimitRemaining}, 1000 more requests will be added in ${minutesRemaining} minutes and ${secondsRemaining} seconds`,
  380. );
  381.  
  382. if (!response.ok) {
  383. console.error(
  384. `Error fetching profile picture for ${username}: ${response.statusText}`,
  385. );
  386. return null;
  387. }
  388. const data = await response.json();
  389. if (data.data && data.data.icon_img) {
  390. const profilePictureUrl = data.data.icon_img.split('?')[0];
  391. profilePictureCache[username] = profilePictureUrl;
  392. cacheTimestamps[username] = Date.now();
  393. saveCache();
  394. console.log(`Fetched profile picture: ${username}`);
  395. return profilePictureUrl;
  396. } else {
  397. console.warn(`No profile picture found for: ${username}`);
  398. return null;
  399. }
  400. } catch (error) {
  401. console.error(`Error fetching profile picture for ${username}:`, error);
  402. return null;
  403. }
  404. });
  405.  
  406. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  407. const results = await Promise.all(fetchPromises);
  408. limitCacheSize();
  409. return usernames.map((username) => profilePictureCache[username]);
  410. }
  411.  
  412. /**
  413. * Injects profile pictures next to user comments and adds hover functionality for enlarged views
  414. * @async
  415. * @param {NodeList} comments - NodeList of comment elements to process
  416. * @returns {Promise<void>}
  417. *
  418. * @description
  419. * This function:
  420. * 1. Extracts usernames from comments, filtering out deleted/removed users
  421. * 2. Fetches profile picture URLs for valid usernames
  422. * 3. Creates and injects profile picture elements before each comment
  423. * 4. Adds click handlers to open full-size images in new tabs
  424. * 5. Implements hover functionality to show enlarged previews
  425. * 6. Tracks injection count and logs cache statistics
  426. * 7. Reports rate limit status for API calls
  427. *
  428. * @requires fetchProfilePictures - External function to retrieve profile picture URLs
  429. * @requires cacheEntries - Global array tracking cached URLs
  430. * @requires MAX_CACHE_SIZE - Global constant for maximum cache size
  431. * @requires rateLimitResetTime - Global variable tracking API rate limit reset time
  432. * @requires rateLimitRemaining - Global variable tracking remaining API calls
  433. * @requires getCacheSizeInMB - Function to calculate cache size in megabytes
  434. * @requires getCacheSizeInKB - Function to calculate cache size in kilobytes
  435. */
  436. async function injectProfilePictures(comments) {
  437. console.log(`Comments found: ${comments.length}`);
  438. const usernames = Array.from(comments)
  439. .map((comment) => comment.textContent.trim())
  440. .filter(
  441. (username) => username !== '[deleted]' && username !== '[removed]',
  442. );
  443. const profilePictureUrls = await fetchProfilePictures(usernames);
  444.  
  445. let injectedCount = 0; // Counter for injected profile pictures
  446.  
  447. comments.forEach((comment, index) => {
  448. const username = usernames[index];
  449. const profilePictureUrl = profilePictureUrls[index];
  450. if (
  451. profilePictureUrl &&
  452. !comment.previousElementSibling?.classList.contains('profile-picture')
  453. ) {
  454. console.log(`Injecting profile picture: ${username}`);
  455. const img = document.createElement('img');
  456. img.src = profilePictureUrl;
  457. img.classList.add('profile-picture');
  458. img.onerror = () => {
  459. img.style.display = 'none';
  460. };
  461. img.addEventListener('click', () => {
  462. window.open(profilePictureUrl, '_blank');
  463. });
  464. comment.insertAdjacentElement('beforebegin', img);
  465.  
  466. const enlargedImg = document.createElement('img');
  467. enlargedImg.src = profilePictureUrl;
  468. enlargedImg.classList.add('enlarged-profile-picture');
  469. document.body.appendChild(enlargedImg);
  470. img.addEventListener('mouseover', () => {
  471. enlargedImg.style.display = 'block';
  472. const rect = img.getBoundingClientRect();
  473. enlargedImg.style.top = `${rect.top + window.scrollY + 20}px`;
  474. enlargedImg.style.left = `${rect.left + window.scrollX + 20}px`;
  475. });
  476. img.addEventListener('mouseout', () => {
  477. enlargedImg.style.display = 'none';
  478. });
  479.  
  480. injectedCount++; // Increment count after successful injection
  481. }
  482. });
  483.  
  484. console.log(`Profile pictures injected this run: ${injectedCount}`);
  485. console.log(`Current cache size: ${cacheEntries.length}`);
  486. console.log(
  487. `Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`,
  488. );
  489. const currentCacheSizeMB = getCacheSizeInMB();
  490. const currentCacheSizeKB = getCacheSizeInKB();
  491. console.log(
  492. `Current cache size: ${currentCacheSizeMB.toFixed(2)} MB or ${currentCacheSizeKB.toFixed(2)} KB`,
  493. );
  494.  
  495. const timeRemaining = rateLimitResetTime - Date.now();
  496. const minutesRemaining = Math.floor(timeRemaining / 60000);
  497. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  498. console.log(
  499. `Rate Limit Requests Remaining: ${rateLimitRemaining} requests, refresh in ${minutesRemaining} minutes and ${secondsRemaining} seconds`,
  500. );
  501. }
  502.  
  503. /**
  504. * Sets up a MutationObserver to watch for new comments on Reddit.
  505. * The observer looks for elements with class 'author' or 'c-username'.
  506. * When new comments are detected, it disconnects the observer and
  507. * injects profile pictures into the found elements.
  508. *
  509. * The observer monitors the entire document body for DOM changes,
  510. * including nested elements.
  511. *
  512. * @function setupObserver
  513. */
  514. function setupObserver() {
  515. console.log('Setting up observer');
  516. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  517. const observer = new MutationObserver((mutations) => {
  518. const comments = document.querySelectorAll('.author, .c-username');
  519. if (comments.length > 0) {
  520. console.log('New comments detected');
  521. observer.disconnect();
  522. injectProfilePictures(comments);
  523. }
  524. });
  525. observer.observe(document.body, {
  526. childList: true,
  527. subtree: true,
  528. });
  529. console.log('Observer initialized');
  530. }
  531.  
  532. // Run the script
  533. /**
  534. * Initializes and runs the main script functionality.
  535. * This function performs the following operations:
  536. * 1. Clears outdated cache entries
  537. * 2. Logs the current state of the profile picture cache
  538. * 3. Initializes the DOM observer
  539. * @function runScript
  540. */
  541. function runScript() {
  542. flushOldCache();
  543. console.log('Cache loaded:', profilePictureCache);
  544. setupObserver();
  545. }
  546.  
  547. window.addEventListener('load', () => {
  548. console.log('Page loaded');
  549. runScript();
  550. });
  551.  
  552. // Add CSS styles for profile pictures
  553. /**
  554. * Creates a new style element to be injected into the document
  555. * @type {HTMLStyleElement}
  556. */
  557. const style = document.createElement('style');
  558. style.textContent = `
  559. .profile-picture {
  560. width: 20px;
  561. height: 20px;
  562. border-radius: 50%;
  563. margin-right: 5px;
  564. transition: transform 0.2s ease-in-out;
  565. position: relative;
  566. z-index: 1;
  567. cursor: pointer;
  568. }
  569. .enlarged-profile-picture {
  570. width: 250px;
  571. height: 250px;
  572. border-radius: 50%;
  573. position: absolute;
  574. display: none;
  575. z-index: 1000;
  576. pointer-events: none;
  577. outline: 3px solid #000;
  578. box-shadow: 0 4px 8px rgba(0, 0, 0, 1);
  579. background-color: rgba(0, 0, 0, 1);
  580. }
  581. `;
  582. document.head.appendChild(style);
  583. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址