Replaces Discourse user avatars with DiceBear Adventurer SVGs based on user ID or username. (Debug logging enabled)
// ==UserScript==
// @name Discourse Avatar Replacer (DiceBear Adventurer) v1.2 (Debug Enabled)
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Replaces Discourse user avatars with DiceBear Adventurer SVGs based on user ID or username. (Debug logging enabled)
// @author dari & AI Assistant
// @match *://*.discourse.org/*
// @match *://*.linux.do/*
// @icon https://api.dicebear.com/9.x/adventurer/svg?seed=tampermonkey&size=64
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
console.log('[DICEBEAR REPLACER] Script starting...'); // Log script start
const DICEBEAR_API_URL_BASE = 'https://api.dicebear.com/9.x/adventurer/svg?seed=';
const PROCESSED_ATTRIBUTE = 'data-dicebear-avatar-replaced';
function getUserIdentifier(imgElement) {
console.log('[DICEBEAR REPLACER] Trying to get identifier for:', imgElement);
// 1. Check data-user-id on img
let userId = imgElement.getAttribute('data-user-id');
if (userId && userId.trim() !== '') {
console.log(`[DICEBEAR REPLACER] Found User ID on img: ${userId}`);
return userId;
}
// 2. Check data-user-id on closest ancestor
const userElement = imgElement.closest('[data-user-id]');
if (userElement) {
userId = userElement.getAttribute('data-user-id');
if (userId && userId.trim() !== '') {
console.log(`[DICEBEAR REPLACER] Found User ID on ancestor [${userElement.tagName}]: ${userId}`);
return userId;
}
}
// 3. Fallback: Extract username from parent link href
const parentLink = imgElement.closest('a[href*="/u/"]');
if (parentLink && parentLink.href) {
const match = parentLink.href.match(/\/u\/([^\/]+)/);
if (match && match[1]) {
const username = match[1];
console.log(`[DICEBEAR REPLACER] Found Username in link [${parentLink.tagName}]: ${username}`);
return username; // Use username as the seed
} else {
console.log('[DICEBEAR REPLACER] Found parent link, but no username match in href:', parentLink.href);
}
} else {
console.log('[DICEBEAR REPLACER] No parent link with /u/ found.');
}
// 4. Fallback: Username from title/alt (often less reliable)
const usernameFromAttr = imgElement.getAttribute('title') || imgElement.getAttribute('alt');
if (usernameFromAttr && usernameFromAttr.trim() !== '' && !usernameFromAttr.includes('Avatar')) { // Avoid generic "Avatar" alt text
console.log(`[DICEBEAR REPLACER] Found identifier from title/alt: ${usernameFromAttr.trim()}`);
return usernameFromAttr.trim();
}
console.warn('[DICEBEAR REPLACER] Could not determine User ID or Username for:', imgElement);
return null;
}
function replaceAvatars() {
console.log('[DICEBEAR REPLACER] Running replaceAvatars function...');
// Select all images with 'avatar' class NOT already processed
const avatarImages = document.querySelectorAll(`img.avatar:not([${PROCESSED_ATTRIBUTE}])`);
console.log(`[DICEBEAR REPLACER] Found ${avatarImages.length} potential avatar images with class 'avatar' to process.`);
if (avatarImages.length === 0) {
console.log("[DICEBEAR REPLACER] No new images with class 'avatar' found this time.");
// Let's also check if ANY images matching the original src pattern exist, maybe the class is wrong on homepage?
// This is for debugging only:
const allUserImages = document.querySelectorAll('img[src*="/user_avatar/"]');
console.log(`[DICEBEAR REPLACER] DEBUG: Found ${allUserImages.length} images with '/user_avatar/' in src (regardless of class).`);
}
avatarImages.forEach((img, index) => {
console.log(`[DICEBEAR REPLACER] Processing image #${index + 1}:`, img);
img.setAttribute(PROCESSED_ATTRIBUTE, 'true'); // Mark as processed
const identifier = getUserIdentifier(img);
if (identifier && identifier.trim() !== '') {
const seed = identifier.trim();
const newSrc = `${DICEBEAR_API_URL_BASE}${encodeURIComponent(seed)}`;
if (img.src !== newSrc) {
console.log(`[DICEBEAR REPLACER] Replacing src for Identifier: ${seed}. Old src: ${img.src}, New src: ${newSrc}`);
img.src = newSrc; // Replace the source
img.removeAttribute('srcset'); // Remove srcset
// Let's keep width/height for now
} else {
console.log(`[DICEBEAR REPLACER] Identifier ${seed} found, but src ${img.src} is already the target DiceBear URL. Skipping.`);
}
} else {
console.warn('[DICEBEAR REPLACER] No identifier found for image:', img);
}
});
console.log('[DICEBEAR REPLACER] Finished replaceAvatars function run.');
}
// --- MutationObserver ---
const observer = new MutationObserver(mutations => {
let needsUpdate = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches(`img.avatar:not([${PROCESSED_ATTRIBUTE}])`) || node.querySelector(`img.avatar:not([${PROCESSED_ATTRIBUTE}])`)) {
console.log('[DICEBEAR REPLACER] MutationObserver detected added node potentially containing an avatar:', node);
needsUpdate = true;
break;
}
}
}
}
if (needsUpdate) break;
}
if (needsUpdate) {
console.log('[DICEBEAR REPLACER] DOM change detected, scheduling avatar replacement.');
clearTimeout(observer.debounceTimer);
observer.debounceTimer = setTimeout(replaceAvatars, 200); // Slightly longer debounce for dynamic loads
}
});
const config = { childList: true, subtree: true };
observer.observe(document.body, config);
console.log('[DICEBEAR REPLACER] MutationObserver started.');
// --- Initial Run ---
console.log('[DICEBEAR REPLACER] Scheduling initial run.');
// Increased timeout significantly to wait for potentially slow homepage elements
setTimeout(replaceAvatars, 1500);
})();