Reddit Multi Code Decoder for obfuscated content

Detects obfuscated text in Reddit comments as: binary and NATO (usually in NSFW); convert it back to human readable string. Implement a text selection popup for Google search.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Reddit Multi Code Decoder for obfuscated content
// @namespace    [email protected]
// @version      0.3.2
// @description  Detects obfuscated text in Reddit comments as: binary and NATO (usually in NSFW); convert it back to human readable string. Implement a text selection popup for Google search.
// @author       [email protected]
// @license      MIT
// @match        https://www.reddit.com/*
// @match        https://old.reddit.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // ===== COMMON =====
    // Centralized comment selectors for both selection popup and binary decoder
    const COMMENT_SELECTORS = [
        // New Reddit
        '[data-testid="comment"]',
        'shreddit-comment',
        '.Comment',
        '[data-click-id="text"]',

        // Old Reddit
        '.usertext-body',
        '.md',

        // Generic fallbacks
        '.comment',
        '[class*="comment"]'
    ];

    // Centralized search provider configuration
    const SEARCH_CONFIG = {
        // Default search provider templates
        providers: {
            google: {
                name: "Google",
                webSearch: "https://www.google.com/search?q={{QUERY}}",
                imageSearch: "https://www.google.com/search?q={{QUERY}}&tbm=isch",
                videoSearch: "https://www.google.com/search?q={{QUERY}}&tbm=vid"
            },
            bing: {
                name: "Bing",
                webSearch: "https://www.bing.com/search?q={{QUERY}}",
                imageSearch: "https://www.bing.com/images/search?q={{QUERY}}"
            },
            duckduckgo: {
                name: "DuckDuckGo",
                webSearch: "https://duckduckgo.com/?q={{QUERY}}",
                imageSearch: "https://duckduckgo.com/?q={{QUERY}}&iax=images&ia=images"
            }
        },

        // Active configuration
        active: {
            provider: "google",
            defaultSearchType: "imageSearch"  // Default to images as specified
        }
    };

    // Search utility functions
    function buildSearchURL(query, searchType = null) {
        try {
            const provider = SEARCH_CONFIG.providers[SEARCH_CONFIG.active.provider];
            if (!provider) {
                // Fallback to Google if current provider is invalid
                SEARCH_CONFIG.active.provider = "google";
                const fallbackProvider = SEARCH_CONFIG.providers.google;
                const type = searchType || SEARCH_CONFIG.active.defaultSearchType;
                const template = fallbackProvider[type] || fallbackProvider.webSearch;
                return template.replace('{{QUERY}}', encodeURIComponent(query));
            }

            const type = searchType || SEARCH_CONFIG.active.defaultSearchType;
            const template = provider[type] || provider.webSearch;

            return template.replace('{{QUERY}}', encodeURIComponent(query));
        } catch (error) {
            // Ultimate fallback to Google web search
            return `https://www.google.com/search?q=${encodeURIComponent(query)}`;
        }
    }

    function validateProvider(providerName) {
        return SEARCH_CONFIG.providers.hasOwnProperty(providerName);
    }

    function setSearchProvider(providerName) {
        if (validateProvider(providerName)) {
            SEARCH_CONFIG.active.provider = providerName;
            return true;
        }
        return false;
    }

    // NATO phonetic alphabet mapping
    const NATO_ALPHABET = {
        'apple': 'A', 'alpha': 'A', 'alfa': 'A', 'bravo': 'B', 'charlie': 'C', 'delta': 'D',
        'echo': 'E', 'foxtrot': 'F', 'golf': 'G', 'hotel': 'H', 'india': 'I',
        'juliet': 'J', 'kilo': 'K', 'lima': 'L', 'mike': 'M', 'mira': 'M', 'november': 'N',
        'oscar': 'O', 'papa': 'P', 'quebec': 'Q', 'romeo': 'R', 'sierra': 'S',
        'tango': 'T', 'uniform': 'U', 'victor': 'V', 'whiskey': 'W',
        'xray': 'X', 'x-ray': 'X', 'yankee': 'Y', 'zulu': 'Z', 'space': ' '
    };

    // NATO phonetic pattern: sequences of NATO words with optional separators
    const natoPattern = new RegExp(
        `\\b(?:${Object.keys(NATO_ALPHABET).join('|')})(?:[\\s,.-]+(?:${Object.keys(NATO_ALPHABET).join('|')}))*\\b`,
        'gi'
    );

    // Function to convert NATO phonetic string to text
    function natoToText(natoString) {
        try {
            // First, normalize the string to handle x-ray properly
            let normalizedString = natoString.toLowerCase();

            // Create a list of NATO words sorted by length (descending) to match longer words first
            const natoWords = Object.keys(NATO_ALPHABET).sort((a, b) => b.length - a.length);

            let result = '';
            let position = 0;
            let validWordsCount = 0;

            while (position < normalizedString.length) {
                // Skip whitespace and separators
                while (position < normalizedString.length && /[\s,.-]/.test(normalizedString[position])) {
                    position++;
                }

                if (position >= normalizedString.length) break;

                let matched = false;

                // Try to match each NATO word starting from current position
                for (const word of natoWords) {
                    if (normalizedString.substr(position, word.length) === word) {
                        // Check if this is a complete word (not part of a larger word)
                        const nextChar = normalizedString[position + word.length];
                        if (!nextChar || /[\s,.-]/.test(nextChar)) {
                            result += NATO_ALPHABET[word];
                            position += word.length;
                            validWordsCount++;
                            matched = true;
                            break;
                        }
                    }
                }

                if (!matched) {
                    // Skip the current character if no NATO word was found
                    position++;
                }
            }

            // Only return result if we have at least 2 valid NATO words
            if (validWordsCount >= 2) {
                return result;
            }

            return null;
        } catch (e) {
            return null;
        }
    }

    // ===== TEXT SELECTION POPUP FEATURE =====
    let selectionPopup = null;

    function createSelectionPopup() {
        const popup = document.createElement('div');
        popup.id = 'text-selection-popup';
        popup.style.cssText = `
            position: fixed;
            background: #ffffff;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            padding: 8px;
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            display: none;
            max-width: 200px;
        `;

        const searchButton = document.createElement('button');
        searchButton.textContent = '🔍 Search on Google';
        searchButton.style.cssText = `
            background: #4285f4;
            color: white;
            border: none;
            padding: 8px 12px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 13px;
            width: 100%;
            transition: background-color 0.2s;
            display: flex;
            align-items: center;
            justify-content: center;
            line-height: 1;
        `;

        searchButton.onmouseover = () => searchButton.style.backgroundColor = '#3367d6';
        searchButton.onmouseout = () => searchButton.style.backgroundColor = '#4285f4';

        popup.appendChild(searchButton);
        document.body.appendChild(popup);

        return { popup, searchButton };
    }

    function hideSelectionPopup() {
        if (selectionPopup && selectionPopup.popup) {
            selectionPopup.popup.style.display = 'none';
        }
    }

    function showSelectionPopup(x, y, selectedText) {
        if (!selectionPopup) {
            selectionPopup = createSelectionPopup();
        }

        const { popup, searchButton } = selectionPopup;

        // Update button click handler with current selected text
        searchButton.onclick = () => {
            const searchURL = buildSearchURL(selectedText.trim());
            window.open(searchURL, '_blank');
            hideSelectionPopup();
        };

        // Position popup near mouse cursor (using clientX/clientY with fixed positioning)
        popup.style.left = `${x + 10}px`;
        popup.style.top = `${y + 10}px`;
        popup.style.display = 'block';

        // Adjust position if popup goes off screen
        const rect = popup.getBoundingClientRect();
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        if (rect.right > viewportWidth) {
            popup.style.left = `${x - rect.width - 10}px`;
        }
        if (rect.bottom > viewportHeight) {
            popup.style.top = `${y - rect.height - 10}px`;
        }
    }

    // Check if the target element is within a comment
    function isWithinComment(element) {
        return COMMENT_SELECTORS.some(selector => element.closest(selector));
    }

    // Handle text selection events
    document.addEventListener('mouseup', (e) => {
        setTimeout(() => {
            const selection = window.getSelection();
            const selectedText = selection.toString().trim();

            if (selectedText.length > 0 && isWithinComment(e.target)) {
                // Use clientX/clientY with fixed positioning for proper viewport positioning
                showSelectionPopup(e.clientX, e.clientY, selectedText);
            } else {
                hideSelectionPopup();
            }
        }, 10);
    });

    // Hide popup when clicking elsewhere or on escape
    document.addEventListener('mousedown', (e) => {
        if (selectionPopup && selectionPopup.popup && !selectionPopup.popup.contains(e.target)) {
            hideSelectionPopup();
        }
    });

    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            hideSelectionPopup();
        }
    });

    // ===== BINARY DECODER FUNCTIONALITY =====

    // Binary pattern: sequences of 0s and 1s, typically in groups of 8 (bytes)
    // Matches patterns like: 01001000 01100101 01101100 01101100 01101111
    const binaryPattern = /(?:\b[01]{8}\b[\s]*){2,}/g;

    // Function to convert binary string to text
    function binaryToText(binaryString) {
        try {
            // Remove all whitespace and split into 8-bit chunks
            const cleanBinary = binaryString.replace(/\s/g, '');
            if (cleanBinary.length % 8 !== 0) {
                return null; // Invalid binary string
            }

            let result = '';
            for (let i = 0; i < cleanBinary.length; i += 8) {
                const byte = cleanBinary.substr(i, 8);
                const charCode = parseInt(byte, 2);

                // Only convert printable ASCII characters (32-126) and common whitespace
                if ((charCode >= 32 && charCode <= 126) || charCode === 9 || charCode === 10 || charCode === 13) {
                    result += String.fromCharCode(charCode);
                } else {
                    return null; // Contains non-printable characters, probably not text
                }
            }
            return result;
        } catch (e) {
            return null;
        }
    }

    // Function to create translation component for multiple decoder types
    function createTranslationComponent(type, originalText, translatedText) {
        const container = document.createElement('div');
        container.style.cssText = `
            margin: 8px 0;
            padding: 12px;
            background: #f8f9fa;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            font-family: monospace;
            font-size: 14px;
            position: relative;
            display: flex;
            align-items: center;
            gap: 12px;
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            font-weight: bold;
            color: #1a73e8;
            font-size: 12px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            flex-shrink: 0;
        `;

        // Set header text based on decoder type
        if (type === 'nato') {
            header.textContent = '📻 NATO:';
        } else {
            header.textContent = '🔢 Binary:';
        }

        const translationDiv = document.createElement('div');
        translationDiv.style.cssText = `
            background: #e8f5e8;
            padding: 8px;
            border-radius: 4px;
            font-weight: bold;
            color: #2d5a2d;
            flex-grow: 1;
        `;
        translationDiv.textContent = `${translatedText}`;

        const searchLink = document.createElement('a');
        searchLink.href = buildSearchURL(translatedText);
        searchLink.target = '_blank';
        searchLink.rel = 'noopener noreferrer';
        searchLink.style.cssText = `
            background: #4285f4;
            color: white;
            padding: 6px 8px;
            border-radius: 4px;
            text-decoration: none;
            display: flex;
            align-items: center;
            gap: 4px;
            font-size: 12px;
            flex-shrink: 0;
            transition: background-color 0.2s;
        `;
        searchLink.onmouseover = () => searchLink.style.backgroundColor = '#3367d6';
        searchLink.onmouseout = () => searchLink.style.backgroundColor = '#4285f4';

        // Search icon (SVG)
        const searchIcon = document.createElement('span');
        searchIcon.innerHTML = `
            <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
                <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
            </svg>
        `;

        searchLink.appendChild(searchIcon);
        searchLink.appendChild(document.createTextNode('Search'));

        container.appendChild(header);
        container.appendChild(translationDiv);
        container.appendChild(searchLink);

        return container;
    }

    // Function to process text nodes and detect binary and NATO patterns
    function processTextNode(textNode) {
        const text = textNode.textContent;
        let hasTranslations = false;

        // Check for binary patterns
        const binaryMatches = text.match(binaryPattern);
        if (binaryMatches) {
            binaryMatches.forEach(match => {
                const translated = binaryToText(match);
                if (translated && translated.trim().length > 0) {
                    if (!hasTranslations) {
                        // Create a wrapper for the original text node
                        const wrapper = document.createElement('span');
                        textNode.parentNode.insertBefore(wrapper, textNode);
                        wrapper.appendChild(textNode);
                        hasTranslations = true;
                    }

                    // Add binary translation component after the text
                    const translationComponent = createTranslationComponent('binary', match, translated);
                    textNode.parentNode.insertBefore(translationComponent, textNode.nextSibling);
                }
            });
        }

        // Check for NATO phonetic patterns and combine consecutive sequences
        const natoMatches = text.match(natoPattern);
        if (natoMatches) {
            // Combine all NATO matches into a single sequence
            const allNatoWords = natoMatches.join(' ');
            const combinedTranslated = natoToText(allNatoWords);

            if (combinedTranslated && combinedTranslated.trim().length > 0) {
                if (!hasTranslations) {
                    // Create a wrapper for the original text node
                    const wrapper = document.createElement('span');
                    textNode.parentNode.insertBefore(wrapper, textNode);
                    wrapper.appendChild(textNode);
                    hasTranslations = true;
                }

                // Add single NATO translation component for all matches
                const translationComponent = createTranslationComponent('nato', allNatoWords, combinedTranslated);
                textNode.parentNode.insertBefore(translationComponent, textNode.nextSibling);
            }
        }
    }

    // Function to scan for decodable patterns in comments
    function scanForPatterns() {
        // Create selectors for paragraph elements within comments
        const commentParagraphSelectors = COMMENT_SELECTORS.map(selector => `${selector} p`);

        commentParagraphSelectors.forEach(selector => {
            const elements = document.querySelectorAll(selector);
            elements.forEach(element => {
                // Skip if already processed
                if (element.hasAttribute('data-processed')) return;
                element.setAttribute('data-processed', 'true');

                // Process all text nodes in the element
                const walker = document.createTreeWalker(
                    element,
                    NodeFilter.SHOW_TEXT,
                    null,
                    false
                );

                const textNodes = [];
                let node;
                while (node = walker.nextNode()) {
                    textNodes.push(node);
                }

                textNodes.forEach(processTextNode);
            });
        });
    }

    // Initial scan
    setTimeout(scanForPatterns, 1000);

    // Watch for dynamically loaded content
    const observer = new MutationObserver((mutations) => {
        let shouldScan = false;
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    // Check if the added node contains comments using centralized selectors
                    if (node.querySelector && COMMENT_SELECTORS.some(selector =>
                        node.querySelector(selector) || node.matches(selector)
                    )) {
                        shouldScan = true;
                    }
                }
            });
        });

        if (shouldScan) {
            setTimeout(scanForPatterns, 500);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Scan when comments are expanded or loaded
    document.addEventListener('click', (e) => {
        // Check for comment expansion buttons
        if (e.target.matches('[aria-expanded]') ||
            e.target.closest('[aria-expanded]') ||
            e.target.textContent.includes('more replies') ||
            e.target.textContent.includes('comments')) {
            setTimeout(scanForPatterns, 1000);
        }
    });

    console.log('Reddit Binary Decoder v0.3.1 loaded');
})();