// ==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');
})();