Base64 Link Decoder for 4chan and Archives

Decode base64-encoded links in 4chan posts and various archives

  1. // ==UserScript==
  2. // @name Base64 Link Decoder for 4chan and Archives
  3. // @namespace Violentmonkey Scripts
  4. // @match *://boards.4channel.org/*
  5. // @match *://boards.4chan.org/*
  6. // @match *://archived.moe/*
  7. // @match *://archiveofsins.com/*
  8. // @grant none
  9. // @version 1.0
  10. // @description Decode base64-encoded links in 4chan posts and various archives
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const base64Regex = /^[A-Za-z0-9+/]+=*$/;
  18. const isURL = (str) => /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i.test(str);
  19.  
  20. function decodeAndReplace(element) {
  21. const html = element.innerHTML;
  22. const modifiedHtml = html.replace(/<wbr>/g, ''); // Remove <wbr> tags
  23. element.innerHTML = modifiedHtml;
  24.  
  25. const textNodes = [];
  26. const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
  27. let node;
  28. while (node = walker.nextNode()) {
  29. textNodes.push(node);
  30. }
  31.  
  32. textNodes.forEach(node => {
  33. const parts = node.textContent.split(/\s+/);
  34. let changed = false;
  35. for (let i = 0; i < parts.length; i++) {
  36. if (base64Regex.test(parts[i])) {
  37. try {
  38. const decoded = atob(parts[i]);
  39. if (isURL(decoded)) {
  40. parts[i] = `<a href="${decoded}" target="_blank">${decoded}</a>`;
  41. changed = true;
  42. }
  43. } catch (e) {
  44. // Not a valid base64 string, skip
  45. }
  46. }
  47. }
  48. if (changed) {
  49. const span = document.createElement('span');
  50. span.innerHTML = parts.join(' ');
  51. node.parentNode.replaceChild(span, node);
  52. }
  53. });
  54. }
  55.  
  56. function processNewPosts() {
  57. // For 4chan
  58. const posts = document.querySelectorAll('.postMessage');
  59. posts.forEach(decodeAndReplace);
  60.  
  61. // For Archives (ArchiveOfSins, Archived.Moe, etc.)
  62. const archivePosts = document.querySelectorAll('.text');
  63. archivePosts.forEach(decodeAndReplace);
  64. }
  65.  
  66. function initScript() {
  67. // Initial processing
  68. processNewPosts();
  69.  
  70. // Watch for new posts
  71. const observer = new MutationObserver((mutations) => {
  72. mutations.forEach((mutation) => {
  73. if (mutation.type === 'childList') {
  74. mutation.addedNodes.forEach((node) => {
  75. if (node.nodeType === Node.ELEMENT_NODE) {
  76. // For 4chan
  77. if (node.classList.contains('postContainer')) {
  78. const postMessage = node.querySelector('.postMessage');
  79. if (postMessage) decodeAndReplace(postMessage);
  80. }
  81. // For Archives
  82. if (node.classList.contains('post')) {
  83. const textElement = node.querySelector('.text');
  84. if (textElement) decodeAndReplace(textElement);
  85. }
  86. }
  87. });
  88. }
  89. });
  90. });
  91.  
  92. observer.observe(document.body, { childList: true, subtree: true });
  93. }
  94.  
  95. // Delay script execution by 5 seconds
  96. setTimeout(initScript, 5000);
  97. })();