DanielBlaze / Reddit Base64 Decoder

// ==UserScript==
// @namespace   https://openuserjs.org/users/DanielBlaze
// @name        Reddit Base64 Decoder
// @description Automatically base64 decode links in reddit posts
// @copyright   2018, DanielBlaze (https://openuserjs.org/users/DanielBlaze)
// @updateURL   https://openuserjs.org/meta/DanielBlaze/Reddit_Base64_Decoder.meta.js
// @license     MIT
// @version     2.0.0
// @author      DanielBlaze
// @grant       none
// @match       https://*.reddit.com/r/*
// ==/UserScript==

// ==OpenUserJS==
// @author DanielBlaze
// ==/OpenUserJS==

/**
 * Wraps HTML in some styling to show its been decoded
 */
function decodedBlock(inner) {
  const css = [
    'border: 1px dashed #e6e6e6',
    'padding: 10px',
    'background-image: linear-gradient(45deg, rgba(199,199,199,.25) 25%, rgba(227,227,227,.25) 25%, rgba(227,227,227,.25) 50%, rgba(199,199,199,.25) 50%, rgba(199,199,199,.25) 75%, rgba(227,227,227,.25) 75%, rgba(227,227,227,.25) 100%)',
    'background-size: 40px 40px'
  ];

  return `<div style='${css.join(';')}'>${inner}</div>`;
}

/**
 * Attempts to decode an array of elements. Styling will be lost on any element which is decoded
 * @param {HTMLElement[]} domElements
 */
function decode(domElements) {
  // for all pararaph tags
  domElements.forEach(domElement => {
    // split on the words we find, and make sure we have something before continuing
    const words = domElement.innerText.split(' ');
    if (words.length === 0) return;

    // decode the string
    let changed = false;
    const transformed = words.map(word => {
      try {
        const decoded = atob(word);

        // if it's valid, change it
        if (decoded.includes('https://') || decoded.includes('http://')) {
          changed = true;
          return decodedBlock(
            decoded
            .replace(/[\r\n]+/g, '<br /><br />')
            .replace(/(https?:\/\/[^\s<]+)/g, `<a href="$1" style='text-decoration: underline #8989ff dotted; text-underline-offset: 2px;'>$1</a>`)
          );
        }
      }
      catch (_) {}

      return word;
    });

    // only if we changed something should we update the HTML - otherwise we lose the original formatting
    if (changed) domElement.innerHTML = transformed.join(' ');
  });
}

(function () {
  'use strict';

  // check we have observers available to us
  if (!MutationObserver) return;

  // observer config - only interested in tree modifications
  const config = {
    childList: true,
    subtree: true
  };

  // the callback to decode the string
  const callback = function (mutationsList, observer) {
    mutationsList.forEach(mutation => {
      if (mutation.target.nodeName === 'DIV' && mutation.addedNodes.length > 0) {
        // check we can find the post contents
        const postContents = mutation.target.querySelectorAll('p');
        if (!postContents) return;

        // decode that bad boy
        decode(postContents);
      }
    });
  };

  // create an observer instance with the callback
  const observer = new MutationObserver(callback);

  // Start observing the target node for mutations
  observer.observe(document, config);

  // run on page load incase no mutations occur, but only on old reddit
  const postContainer = document.querySelector('.expando');
  if (postContainer) {
    const postContents = postContainer.querySelectorAll('p');
    if (postContents) {
      decode(postContents);
    }
  }
})();