Greasy Fork is available in English.

Old Reddit with New Reddit Profile Pictures - API Key Version - Reddit Only 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 - Reddit Only 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. // @connect reddit.com
  9. // @connect reddit-stream.com
  10. // @grant GM_xmlhttpRequest
  11. // @homepageURL https://github.com/Nick2bad4u/UserStyles
  12. // @license Unlicense
  13. // @resource https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  15. // @icon64 https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  16. // @run-at document-start
  17. // @tag reddit
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. 'use strict';
  22. console.log('Reddit Profile Picture Injector Script loaded');
  23.  
  24. // Reddit API credentials
  25. const CLIENT_ID = 'EnterClientIDHere';
  26. const CLIENT_SECRET = 'EnterClientSecretHere';
  27. const USER_AGENT = 'ProfilePictureInjector/7.0.6 by Nick2bad4u';
  28. let accessToken = localStorage.getItem('accessToken');
  29.  
  30. // Retrieve cached profile pictures and timestamps from localStorage
  31. let profilePictureCache = JSON.parse(
  32. localStorage.getItem('profilePictureCache') || '{}',
  33. );
  34. let cacheTimestamps = JSON.parse(
  35. localStorage.getItem('cacheTimestamps') || '{}',
  36. );
  37. const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
  38. const MAX_CACHE_SIZE = 100000; // Maximum number of cache entries
  39. const cacheEntries = Object.keys(profilePictureCache);
  40.  
  41. // Rate limit variables
  42. let rateLimitRemaining = 1000;
  43. let rateLimitResetTime = 0;
  44. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  45. const resetDate = new Date(rateLimitResetTime);
  46. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  47. const now = Date.now();
  48.  
  49. // Save the cache to localStorage
  50. function saveCache() {
  51. localStorage.setItem(
  52. 'profilePictureCache',
  53. JSON.stringify(profilePictureCache),
  54. );
  55. localStorage.setItem('cacheTimestamps', JSON.stringify(cacheTimestamps));
  56. }
  57.  
  58. // Remove old cache entries
  59. function flushOldCache() {
  60. console.log('Flushing old Reddit profile picture URL cache');
  61. const now = Date.now();
  62. for (const username in cacheTimestamps) {
  63. if (now - cacheTimestamps[username] > CACHE_DURATION) {
  64. console.log(`Deleting cache for Reddit user - ${username}`);
  65. delete profilePictureCache[username];
  66. delete cacheTimestamps[username];
  67. }
  68. }
  69. saveCache();
  70. console.log('Old cache entries flushed');
  71. }
  72.  
  73. // Limit the size of the cache to the maximum allowed entries
  74. function limitCacheSize() {
  75. const cacheEntries = Object.keys(profilePictureCache);
  76. if (cacheEntries.length > MAX_CACHE_SIZE) {
  77. console.log(`Current cache size: ${cacheEntries.length} URLs`);
  78. console.log('Cache size exceeded, removing oldest entries');
  79. const sortedEntries = cacheEntries.sort(
  80. (a, b) => cacheTimestamps[a] - cacheTimestamps[b],
  81. );
  82. const entriesToRemove = sortedEntries.slice(
  83. 0,
  84. cacheEntries.length - MAX_CACHE_SIZE,
  85. );
  86. entriesToRemove.forEach((username) => {
  87. delete profilePictureCache[username];
  88. delete cacheTimestamps[username];
  89. });
  90. saveCache();
  91. console.log(
  92. `Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`,
  93. );
  94. }
  95. }
  96.  
  97. function getCacheSizeInBytes() {
  98. const cacheEntries = Object.keys(profilePictureCache);
  99. let totalSize = 0;
  100.  
  101. // Calculate size of profilePictureCache
  102. cacheEntries.forEach((username) => {
  103. const pictureData = profilePictureCache[username];
  104. const timestampData = cacheTimestamps[username];
  105.  
  106. // Estimate size of data by serializing to JSON and getting the length
  107. totalSize += new TextEncoder().encode(JSON.stringify(pictureData)).length;
  108. totalSize += new TextEncoder().encode(
  109. JSON.stringify(timestampData),
  110. ).length;
  111. });
  112.  
  113. return totalSize; // in bytes
  114. }
  115.  
  116. function getCacheSizeInMB() {
  117. return getCacheSizeInBytes() / (1024 * 1024); // Convert bytes to MB
  118. }
  119.  
  120. function getCacheSizeInKB() {
  121. return getCacheSizeInBytes() / 1024; // Convert bytes to KB
  122. }
  123.  
  124. // Obtain an access token from Reddit API
  125. async function getAccessToken() {
  126. console.log('Obtaining access token');
  127. const credentials = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
  128. try {
  129. const response = await fetch(
  130. 'https://www.reddit.com/api/v1/access_token',
  131. {
  132. method: 'POST',
  133. headers: {
  134. Authorization: `Basic ${credentials}`,
  135. 'Content-Type': 'application/x-www-form-urlencoded',
  136. },
  137. body: 'grant_type=client_credentials',
  138. },
  139. );
  140. if (!response.ok) {
  141. console.error('Failed to obtain access token:', response.statusText);
  142. return null;
  143. }
  144. const data = await response.json();
  145. accessToken = data.access_token;
  146. const expiration = Date.now() + data.expires_in * 1000;
  147. localStorage.setItem('accessToken', accessToken);
  148. localStorage.setItem('tokenExpiration', expiration.toString());
  149. console.log('Access token obtained and saved');
  150. return accessToken;
  151. } catch (error) {
  152. console.error('Error obtaining access token:', error);
  153. return null;
  154. }
  155. }
  156.  
  157. // Fetch profile pictures for a list of usernames
  158. async function fetchProfilePictures(usernames) {
  159. console.log('Fetching profile pictures');
  160. const now = Date.now();
  161. const tokenExpiration = parseInt(
  162. localStorage.getItem('tokenExpiration'),
  163. 10,
  164. );
  165.  
  166. // Check rate limit
  167. if (rateLimitRemaining <= 0 && now < rateLimitResetTime) {
  168. console.warn('Rate limit reached. Waiting until reset...');
  169.  
  170. const timeRemaining = rateLimitResetTime - now;
  171. const minutesRemaining = Math.floor(timeRemaining / 60000);
  172. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  173.  
  174. console.log(
  175. `Rate limit will reset in ${minutesRemaining} minutes and ${secondsRemaining} seconds.`,
  176. );
  177. await new Promise((resolve) =>
  178. setTimeout(resolve, rateLimitResetTime - now),
  179. );
  180. }
  181.  
  182. // Refresh access token if expired
  183. if (!accessToken || now > tokenExpiration) {
  184. accessToken = await getAccessToken();
  185. if (!accessToken) return null;
  186. }
  187.  
  188. // Filter out cached usernames
  189. const uncachedUsernames = usernames.filter(
  190. (username) =>
  191. !profilePictureCache[username] &&
  192. username !== '[deleted]' &&
  193. username !== '[removed]',
  194. );
  195. if (uncachedUsernames.length === 0) {
  196. console.log('All usernames are cached');
  197. return usernames.map((username) => profilePictureCache[username]);
  198. }
  199.  
  200. // Fetch profile pictures for uncached usernames
  201. const fetchPromises = uncachedUsernames.map(async (username) => {
  202. try {
  203. const response = await fetch(
  204. `https://oauth.reddit.com/user/${username}/about`,
  205. {
  206. headers: {
  207. Authorization: `Bearer ${accessToken}`,
  208. 'User-Agent': USER_AGENT,
  209. },
  210. },
  211. );
  212.  
  213. // Update rate limit
  214. rateLimitRemaining =
  215. parseInt(response.headers.get('x-ratelimit-remaining')) ||
  216. rateLimitRemaining;
  217. rateLimitResetTime =
  218. now + parseInt(response.headers.get('x-ratelimit-reset')) * 1000 ||
  219. rateLimitResetTime;
  220.  
  221. // Log rate limit information
  222. const timeRemaining = rateLimitResetTime - now;
  223. const minutesRemaining = Math.floor(timeRemaining / 60000);
  224. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  225.  
  226. console.log(
  227. `Rate Limit Requests Remaining: ${rateLimitRemaining} requests, reset in ${minutesRemaining} minutes and ${secondsRemaining} seconds`,
  228. );
  229.  
  230. if (!response.ok) {
  231. console.error(
  232. `Error fetching profile picture for ${username}: ${response.statusText}`,
  233. );
  234. return null;
  235. }
  236. const data = await response.json();
  237. if (data.data && data.data.icon_img) {
  238. const profilePictureUrl = data.data.icon_img.split('?')[0];
  239. profilePictureCache[username] = profilePictureUrl;
  240. cacheTimestamps[username] = Date.now();
  241. saveCache();
  242. console.log(`Fetched profile picture: ${username}`);
  243. return profilePictureUrl;
  244. } else {
  245. console.warn(`No profile picture found for: ${username}`);
  246. return null;
  247. }
  248. } catch (error) {
  249. console.error(`Error fetching profile picture for ${username}:`, error);
  250. return null;
  251. }
  252. });
  253.  
  254. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  255. const results = await Promise.all(fetchPromises);
  256. limitCacheSize();
  257. return usernames.map((username) => profilePictureCache[username]);
  258. }
  259.  
  260. // Inject profile pictures into comments
  261. async function injectProfilePictures(comments) {
  262. console.log(`Comments found: ${comments.length}`);
  263. const usernames = Array.from(comments)
  264. .map((comment) => comment.textContent.trim())
  265. .filter(
  266. (username) => username !== '[deleted]' && username !== '[removed]',
  267. );
  268. const profilePictureUrls = await fetchProfilePictures(usernames);
  269.  
  270. let injectedCount = 0; // Counter for injected profile pictures
  271.  
  272. comments.forEach((comment, index) => {
  273. const username = usernames[index];
  274. const profilePictureUrl = profilePictureUrls[index];
  275. if (
  276. profilePictureUrl &&
  277. !comment.previousElementSibling?.classList.contains('profile-picture')
  278. ) {
  279. console.log(`Injecting profile picture: ${username}`);
  280. const img = document.createElement('img');
  281. img.src = profilePictureUrl;
  282. img.classList.add('profile-picture');
  283. img.onerror = () => {
  284. img.style.display = 'none';
  285. };
  286. img.addEventListener('click', () => {
  287. window.open(profilePictureUrl, '_blank');
  288. });
  289. comment.insertAdjacentElement('beforebegin', img);
  290.  
  291. const enlargedImg = document.createElement('img');
  292. enlargedImg.src = profilePictureUrl;
  293. enlargedImg.classList.add('enlarged-profile-picture');
  294. document.body.appendChild(enlargedImg);
  295. img.addEventListener('mouseover', () => {
  296. enlargedImg.style.display = 'block';
  297. const rect = img.getBoundingClientRect();
  298. enlargedImg.style.top = `${rect.top + window.scrollY + 20}px`;
  299. enlargedImg.style.left = `${rect.left + window.scrollX + 20}px`;
  300. });
  301. img.addEventListener('mouseout', () => {
  302. enlargedImg.style.display = 'none';
  303. });
  304.  
  305. injectedCount++; // Increment count after successful injection
  306. }
  307. });
  308.  
  309. console.log(`Profile pictures injected this run: ${injectedCount}`);
  310. console.log(`Current cache size: ${cacheEntries.length}`);
  311. console.log(
  312. `Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`,
  313. );
  314. const currentCacheSizeMB = getCacheSizeInMB();
  315. const currentCacheSizeKB = getCacheSizeInKB();
  316. console.log(
  317. `Current cache size: ${currentCacheSizeMB.toFixed(2)} MB or ${currentCacheSizeKB.toFixed(2)} KB`,
  318. );
  319.  
  320. const timeRemaining = rateLimitResetTime - Date.now();
  321. const minutesRemaining = Math.floor(timeRemaining / 60000);
  322. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  323. console.log(
  324. `Rate Limit Requests Remaining: ${rateLimitRemaining} requests, refresh in ${minutesRemaining} minutes and ${secondsRemaining} seconds`,
  325. );
  326. }
  327.  
  328. function setupObserver() {
  329. console.log('Setting up observer');
  330. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  331. const observer = new MutationObserver((mutations) => {
  332. const comments = document.querySelectorAll('.author, .c-username');
  333. if (comments.length > 0) {
  334. console.log('New comments detected');
  335. observer.disconnect();
  336. injectProfilePictures(comments);
  337. }
  338. });
  339. observer.observe(document.body, {
  340. childList: true,
  341. subtree: true,
  342. });
  343. console.log('Observer initialized');
  344. }
  345.  
  346. // Run the script
  347. function runScript() {
  348. flushOldCache();
  349. console.log('Cache loaded:', profilePictureCache);
  350. setupObserver();
  351. }
  352.  
  353. window.addEventListener('load', () => {
  354. console.log('Page loaded');
  355. runScript();
  356. });
  357.  
  358. // Add CSS styles for profile pictures
  359. const style = document.createElement('style');
  360. style.textContent = `
  361. .profile-picture {
  362. width: 20px;
  363. height: 20px;
  364. border-radius: 50%;
  365. margin-right: 5px;
  366. transition: transform 0.2s ease-in-out;
  367. position: relative;
  368. z-index: 1;
  369. cursor: pointer;
  370. }
  371. .enlarged-profile-picture {
  372. width: 250px;
  373. height: 250px;
  374. border-radius: 50%;
  375. position: absolute;
  376. display: none;
  377. z-index: 1000;
  378. pointer-events: none;
  379. outline: 3px solid #000;
  380. box-shadow: 0 4px 8px rgba(0, 0, 0, 1);
  381. background-color: rgba(0, 0, 0, 1);
  382. }
  383. `;
  384. document.head.appendChild(style);
  385. })();