Greasy Fork is available in English.

Discourse Avatar Replacer (DiceBear Adventurer) v1.2 (Debug Enabled)

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);

})();