MM Rooster Filter

Replaces EACH message character in mattermost with an emoji (1 char = 1 emoji)

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         MM Rooster Filter
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      3.2.1
// @description  Replaces EACH message character in mattermost with an emoji (1 char = 1 emoji)
// @match        https://community.mattermost.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    const DEFAULT_REMOTE_CONFIG_URL = "https://gist.githubusercontent.com/rift0nix/b6a363e5c49d50ad44e72c6a94be8881/raw/91fd145b3838087e30c970915f0bdc70ce96a6dc/gistfile1.txt";
    const CACHE_KEY = 'mm-rooster-config-cache-v1';
    const CONFIG_URL_STORAGE_KEY = 'mm-rooster-remote-config-url';

    const DEFAULT_CONFIG = {
        configVersion: 1,
        remoteConfigUrl: DEFAULT_REMOTE_CONFIG_URL,
        remoteFetchTimeoutMs: 3000,
        remoteCacheTtlMs: 10 * 60 * 1000,
        replacementEmoji: '🐓',
        targets: {
            "username1": "2026-03-12",
            "username2": "1970-01-01"
        },
        selectors: {
            postContainer: ['div.post__content'],
            postRoot: ['.post'],
            avatar: ['img.Avatar'],
            time: ['time.post__time[datetime]'],
            messageBody: ['.post-message__text'],
            sameUserClass: 'same--user',
        },
    };

    let activeConfig = normalizeConfig(DEFAULT_CONFIG);
    let targetUsers = buildTargetUsersMap(activeConfig.targets);
    let replacementEmoji = activeConfig.replacementEmoji;

    // Cache for consecutive messages (avatar / timestamp)
    let lastKnownUser = null;
    const userStateByName = new Map(); // Map<string, { avatar?: Element, timestamp?: Date }>

    console.log('[🐓 Rooster] Configurable mode loaded');

    function toArray(value) {
        if (Array.isArray(value)) return value.filter(Boolean);
        if (typeof value === 'string' && value.trim()) return [value.trim()];
        return [];
    }

    function getSavedRemoteConfigUrl() {
        try {
            if (typeof GM_getValue !== 'function') return null;
            const value = String(GM_getValue(CONFIG_URL_STORAGE_KEY, '') || '').trim();
            return value || null;
        } catch {
            return null;
        }
    }

    function getEffectiveRemoteConfigUrl() {
        return getSavedRemoteConfigUrl() || DEFAULT_REMOTE_CONFIG_URL;
    }

    function saveRemoteConfigUrl(url) {
        if (typeof GM_setValue !== 'function') {
            throw new Error('GM_setValue is unavailable');
        }
        GM_setValue(CONFIG_URL_STORAGE_KEY, url.trim());
    }

    function withStoredRemoteConfigUrl(config) {
        return {
            ...(config || {}),
            remoteConfigUrl: getEffectiveRemoteConfigUrl(),
        };
    }

    function normalizeConfig(raw) {
        const merged = {
            ...DEFAULT_CONFIG,
            ...(raw || {}),
            targets: {
                ...DEFAULT_CONFIG.targets,
                ...((raw && raw.targets) || {}),
            },
            selectors: {
                ...DEFAULT_CONFIG.selectors,
                ...((raw && raw.selectors) || {}),
            },
        };

        merged.replacementEmoji = typeof merged.replacementEmoji === 'string' && merged.replacementEmoji.trim()
            ? merged.replacementEmoji
            : DEFAULT_CONFIG.replacementEmoji;

        merged.remoteConfigUrl = typeof merged.remoteConfigUrl === 'string' && merged.remoteConfigUrl.trim()
            ? merged.remoteConfigUrl
            : DEFAULT_CONFIG.remoteConfigUrl;

        merged.remoteFetchTimeoutMs = Number.isFinite(merged.remoteFetchTimeoutMs)
            ? Math.max(1000, merged.remoteFetchTimeoutMs)
            : DEFAULT_CONFIG.remoteFetchTimeoutMs;

        merged.remoteCacheTtlMs = Number.isFinite(merged.remoteCacheTtlMs)
            ? Math.max(10 * 1000, merged.remoteCacheTtlMs)
            : DEFAULT_CONFIG.remoteCacheTtlMs;

        merged.selectors = {
            postContainer: toArray(merged.selectors.postContainer),
            postRoot: toArray(merged.selectors.postRoot),
            avatar: toArray(merged.selectors.avatar),
            time: toArray(merged.selectors.time),
            messageBody: toArray(merged.selectors.messageBody),
            sameUserClass: typeof merged.selectors.sameUserClass === 'string' && merged.selectors.sameUserClass.trim()
                ? merged.selectors.sameUserClass.trim()
                : DEFAULT_CONFIG.selectors.sameUserClass,
        };

        return merged;
    }

    function buildTargetUsersMap(targetsObj) {
        const result = new Map();
        Object.entries(targetsObj || {}).forEach(([username, dateIso]) => {
            const normalizedUser = String(username || '').toLowerCase().trim();
            const d = new Date(`${dateIso}T00:00:00`);
            if (normalizedUser && !Number.isNaN(d.getTime())) {
                result.set(normalizedUser, d);
            }
        });
        return result;
    }

    function applyConfig(config) {
        activeConfig = normalizeConfig(config);
        targetUsers = buildTargetUsersMap(activeConfig.targets);
        replacementEmoji = activeConfig.replacementEmoji;
        console.log('[🐓] Config applied', {
            users: targetUsers.size,
            emoji: replacementEmoji,
            postContainerSelectors: activeConfig.selectors.postContainer,
        });
    }

    function getCachedConfigEntry() {
        try {
            const raw = localStorage.getItem(CACHE_KEY);
            if (!raw) return null;
            const parsed = JSON.parse(raw);
            if (!parsed || typeof parsed !== 'object') return null;
            if (!parsed.config || typeof parsed.fetchedAt !== 'number') return null;
            return parsed;
        } catch {
            return null;
        }
    }

    function setCachedConfigEntry(config) {
        try {
            localStorage.setItem(CACHE_KEY, JSON.stringify({
                fetchedAt: Date.now(),
                config,
            }));
        } catch {
            // Ignore storage failures
        }
    }

    function shouldRefreshConfig(cachedEntry, ttlMs) {
        if (!cachedEntry) return true;
        return Date.now() - cachedEntry.fetchedAt > ttlMs;
    }

    async function fetchRemoteConfig(url, timeoutMs) {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), timeoutMs);
        try {
            const response = await fetch(url, {
                method: 'GET',
                cache: 'no-store',
                signal: controller.signal,
            });
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            const json = await response.json();
            if (!json || typeof json !== 'object') {
                throw new Error('Invalid config payload');
            }
            return json;
        } finally {
            clearTimeout(timer);
        }
    }

    async function loadAndApplyConfig() {
        const cachedEntry = getCachedConfigEntry();
        if (cachedEntry) {
            applyConfig(withStoredRemoteConfigUrl(cachedEntry.config));
            console.log('[🐓] Using cached config');
        } else {
            applyConfig(withStoredRemoteConfigUrl(DEFAULT_CONFIG));
            console.log('[🐓] Using default config');
        }

        const ttl = activeConfig.remoteCacheTtlMs;
        if (!shouldRefreshConfig(cachedEntry, ttl)) {
            return;
        }

        try {
            const remote = await fetchRemoteConfig(
                activeConfig.remoteConfigUrl,
                activeConfig.remoteFetchTimeoutMs
            );
            const merged = normalizeConfig(remote);
            applyConfig(withStoredRemoteConfigUrl(merged));
            setCachedConfigEntry(merged);
            console.log('[🐓] Remote config loaded');
        } catch (error) {
            console.warn('[🐓] Remote config load failed, keep active config:', error);
        }
    }

    function registerMenuCommands() {
        if (typeof GM_registerMenuCommand !== 'function') return;

        GM_registerMenuCommand('Set remote config URL', async function() {
            const current = getEffectiveRemoteConfigUrl();
            const next = window.prompt('Enter remote config URL', current);
            if (next === null) return;

            const value = next.trim();
            if (!value) {
                console.warn('[🐓] Remote config URL is empty, keeping current value');
                return;
            }

            try {
                new URL(value);
                saveRemoteConfigUrl(value);
                localStorage.removeItem(CACHE_KEY);
                await loadAndApplyConfig();
                processPosts();
                console.log('[🐓] Remote config URL updated');
            } catch (error) {
                console.warn('[🐓] Failed to update remote config URL:', error);
            }
        });
    }

    function extractUsername(avatarImg) {
        const alt = avatarImg?.getAttribute('alt');
        if (!alt) return null;
        return alt.split(' ')[0].toLowerCase();
    }

    function getUserState(username) {
        if (!userStateByName.has(username)) {
            userStateByName.set(username, {});
        }
        return userStateByName.get(username);
    }

    function stripHtml(text) {
        return (text || '').replace(/<[^>]*>/g, '').trim();
    }

    function queryFirst(root, selectors) {
        for (const selector of selectors) {
            const element = root.querySelector(selector);
            if (element) return element;
        }
        return null;
    }

    function queryAll(root, selectors) {
        for (const selector of selectors) {
            const list = root.querySelectorAll(selector);
            if (list.length > 0) return Array.from(list);
        }
        return [];
    }

    function closestBySelectors(element, selectors) {
        for (const selector of selectors) {
            const found = element.closest(selector);
            if (found) return found;
        }
        return null;
    }

    function extractTimestamp(container) {
        const timeEl = queryFirst(container, activeConfig.selectors.time);
        if (timeEl) {
            const dt = timeEl.getAttribute('datetime');
            return dt ? new Date(dt) : null;
        }
        return null;
    }

    function toRoosterMask(text) {
        const plainText = stripHtml(text);
        return replacementEmoji.repeat(plainText.length);
    }

    function resolveAvatar(container, post) {
        const directAvatar = queryFirst(container, activeConfig.selectors.avatar);
        if (directAvatar) return directAvatar;

        if (post?.classList.contains(activeConfig.selectors.sameUserClass) && lastKnownUser) {
            const cached = userStateByName.get(lastKnownUser);
            if (cached?.avatar) return cached.avatar;
        }

        return null;
    }

    function resolveMessageDate(container, post, username) {
        const directTimestamp = extractTimestamp(container);
        if (directTimestamp) return directTimestamp;

        if (post?.classList.contains(activeConfig.selectors.sameUserClass)) {
            const cached = userStateByName.get(username);
            if (cached?.timestamp) return cached.timestamp;
        }

        return null;
    }

    function replaceMessageBody(messageBody) {
        if (!messageBody.dataset.originalContent) {
            messageBody.dataset.originalContent = messageBody.innerHTML;
            messageBody.dataset.originalText = messageBody.textContent || '';
        }

        const roosterString = toRoosterMask(messageBody.dataset.originalText);
        messageBody.innerHTML = `<span style="line-height: 1.5;">${roosterString}</span>`;
    }

    function processPost(container) {
        if (container.dataset.roosterDone) return false;

        const post = closestBySelectors(container, activeConfig.selectors.postRoot);
        const avatar = resolveAvatar(container, post);
        if (!avatar) return false;

        const username = extractUsername(avatar);
        if (!username) return false;

        const thresholdDate = targetUsers.get(username);
        if (!thresholdDate) return false;

        const directAvatar = queryFirst(container, activeConfig.selectors.avatar);
        if (directAvatar) {
            const state = getUserState(username);
            state.avatar = directAvatar;
            lastKnownUser = username;
        }

        const messageDate = resolveMessageDate(container, post, username);
        if (!messageDate) return false;

        getUserState(username).timestamp = messageDate;

        if (messageDate < thresholdDate) return false;

        const messageBody = queryFirst(container, activeConfig.selectors.messageBody);
        if (!messageBody) return false;

        replaceMessageBody(messageBody);
        container.dataset.roosterDone = 'true';

        console.log(
            `[🐓] Replaced ${username} @ ${messageDate.toISOString().slice(0, 10)} (≥${thresholdDate
                .toISOString()
                .slice(0, 10)})`
        );
        return true;
    }

    function processPosts() {
        const containers = queryAll(document, activeConfig.selectors.postContainer);
        let replaced = 0;

        containers.forEach(container => {
            if (processPost(container)) replaced++;
        });

        if (replaced > 0) {
            console.log(`[🐓] Total: ${replaced} messages processed`);
        }
    }

    function setupObservers() {
        const observer = new MutationObserver(processPosts);
        observer.observe(document.body, { childList: true, subtree: true });

        let scrollTimeout;
        window.addEventListener('scroll', () => {
            clearTimeout(scrollTimeout);
            scrollTimeout = setTimeout(processPosts, 300);
        }, { passive: true });
    }

    // === CONSOLE COMMANDS ===

    // Undo replacements
    window.roosterUndo = function() {
        document.querySelectorAll('[data-original-content]').forEach(el => {
            el.innerHTML = el.dataset.originalContent;
            delete el.dataset.originalContent;
            delete el.dataset.originalText;
        });
        document.querySelectorAll('[data-rooster-done]').forEach(el => {
            delete el.dataset.roosterDone;
        });

        userStateByName.clear();
        lastKnownUser = null;
        console.log('[🐓] Undo completed');
    };

    // Statistics
    window.roosterStats = function() {
        const stats = {};
        queryAll(document, activeConfig.selectors.avatar).forEach(avatar => {
            const user = extractUsername(avatar);
            if (user) stats[user] = (stats[user] || 0) + 1;
        });
        console.table(stats);
    };

    // Test: how many emoji characters would be generated for text
    window.roosterTest = function(text) {
        const result = toRoosterMask(text);
        console.log(`Input: "${text}"\nOutput: ${result}\nLength: ${result.length}`);
    };

    window.roosterConfigInfo = function() {
        console.log('[🐓] Active config:', activeConfig);
        const cache = getCachedConfigEntry();
        if (cache) {
            console.log('[🐓] Cached at:', new Date(cache.fetchedAt).toISOString());
        } else {
            console.log('[🐓] No cached config');
        }
    };

    window.roosterReloadConfig = async function() {
        localStorage.removeItem(CACHE_KEY);
        await loadAndApplyConfig();
        processPosts();
        console.log('[🐓] Config reloaded');
    };

    async function init() {
        registerMenuCommands();
        await loadAndApplyConfig();
        processPosts();
        setTimeout(processPosts, 1500);
        setupObservers();
    }

    init();

    console.log('[🐓] Commands: roosterUndo(), roosterStats(), roosterTest("hello"), roosterConfigInfo(), roosterReloadConfig()');
})();