4chan Session ID Unbreaker

Tries to detect and un-break Session IDs posted on 4chan

  1. // ==UserScript==
  2. // @name 4chan Session ID Unbreaker
  3. // @license GPLv3
  4. // @namespace https://boards.4chan.org/
  5. // @version 2.4
  6. // @description Tries to detect and un-break Session IDs posted on 4chan
  7. // @author ceodoe
  8. // @match https://boards.4chan.org/*/thread/*
  9. // @match https://boards.4chan.org/*/res/*
  10. // @match https://archived.moe/*/thread/*
  11. // @match https://www.archived.moe/*/thread/*
  12. // @match https://thebarchive.com/*/thread/*
  13. // @match https://www.thebarchive.com/*/thread/*
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=4chan.org
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @grant GM_addStyle
  18. // ==/UserScript==
  19. let rememberCopiedIDs = GM_getValue("rememberCopiedIDs", true);
  20. let rememberedIDs = GM_getValue("rememberedIDs", []);
  21. let site = "4chan";
  22.  
  23. if(location.hostname.includes("archived.moe") || location.hostname.includes("thebarchive.com")) {
  24. site = "foolfuuka";
  25. }
  26.  
  27. GM_addStyle(`
  28. .fcsidu-rememberedID {
  29. color: #aaa;
  30. border-bottom: 1px dotted #aaa;
  31. }
  32. `);
  33.  
  34. function parsePosts() {
  35. let posts = document.querySelectorAll("blockquote.postMessage");
  36.  
  37. if(site == "foolfuuka") {
  38. posts = document.querySelectorAll("article > div.text, div.post_wrapper > div.text");
  39. }
  40.  
  41. for(let i = 0; i < posts.length; i++) {
  42. if(posts[i].getAttribute("data-fcsidu-parsed") !== "1") {
  43. posts[i].setAttribute("data-fcsidu-parsed", "1");
  44.  
  45. // Strip all backlinks as they are likely to contain the magic number 05 that all Session IDs start with
  46. let postText = posts[i].innerText.replace(/\>\>\b[0-9]+\b/g, "");
  47. let idStartIndex = postText.indexOf("05");
  48.  
  49. if(idStartIndex > -1) {
  50. let id = "";
  51.  
  52. // "Smart" detection mechanism removes all non-alphanumeric chars, then ignores words with
  53. // non-hexadecimal chars in them, and tries to build a string exactly 66 chars long
  54. let words = postText.substring(idStartIndex).split(/\s/);
  55. for(let j = 0; j < words.length && id.length < 66; j++) {
  56. let word = words[j].replace(/[^A-Za-z0-9]/g, "");
  57.  
  58. if(!word.match(/[^A-Fa-f0-9]/g)) {
  59. id += word;
  60. }
  61. }
  62.  
  63. if(id.length == 66) { // All IDs are 66 chars; if we didn't get exactly 66, ID is invalid
  64. let opPost = "";
  65. if(posts[i].parentNode.classList.contains("op")) {
  66. opPost = "overflow: auto;";
  67. }
  68.  
  69. let archivePost = "border-top: 1px solid; width: fit-content;";
  70. if(posts[i].parentNode.tagName.toLowerCase() == "article") {
  71. archivePost = "";
  72. }
  73.  
  74. let html = `
  75. <div style="margin-top: 1em; padding: 0.5em; ${archivePost} ${opPost}">
  76. <span style="color: #66cc33; font-weight: bold;">Session ID:</span> <span class="fcsidu-session-id ${rememberCopiedIDs && rememberedIDs.includes(id) ? `fcsidu-rememberedID" title="ID has been previously copied` : ``}">${id}</span>
  77. <input type="button" class="fcsidu-copy-btn" id="fcsidu-copy-btn-${i}" data-fcsidu-session-id="${id}" style="margin-left: 0.5em;" value="Copy">
  78. </div>
  79. `;
  80.  
  81. posts[i].insertAdjacentHTML("beforeend", html);
  82.  
  83. document.getElementById(`fcsidu-copy-btn-${i}`).addEventListener("click", async function() {
  84. let tempInput = document.createElement("input");
  85. let id = this.getAttribute("data-fcsidu-session-id").trim();
  86. tempInput.value = id;
  87. tempInput.select();
  88. tempInput.setSelectionRange(0,66);
  89.  
  90. try {
  91. await navigator.clipboard.writeText(tempInput.value);
  92. this.value = "✓";
  93.  
  94. if(rememberCopiedIDs) {
  95. // Refresh remembered ID list in case it was updated in another tab
  96. rememberedIDs = GM_getValue("rememberedIDs", []);
  97. rememberedIDs.push(id);
  98. GM_setValue("rememberedIDs", rememberedIDs);
  99. updateIDs();
  100. }
  101.  
  102. window.setTimeout(function() {
  103. document.getElementById(`fcsidu-copy-btn-${i}`).value = "Copy";
  104. }, 3000);
  105. } catch (err) {
  106. alert("Failed to copy to clipboard: " + err);
  107. }
  108. });
  109. }
  110. }
  111. }
  112. }
  113.  
  114. if(rememberCopiedIDs) {
  115. updateIDs();
  116. }
  117. }
  118.  
  119. function setupOptions() {
  120. let parent = document.querySelector("div.bottomCtrl");
  121. if(site == "foolfuuka") {
  122. parent = document.querySelector("#footer");
  123. }
  124.  
  125. let html = `
  126. <span id="fcsidu-options">
  127. <input type="checkbox" name="fcsidu-rememberIDs-check" id="fcsidu-rememberIDs-check" ${rememberCopiedIDs ? `checked` : ``}>
  128. <label for="fcsidu-rememberIDs-check" title="Disabling also clears already remembered IDs">Remember copied Session IDs</label> |
  129. </span>
  130. `;
  131. parent.insertAdjacentHTML("afterbegin", html);
  132.  
  133. document.querySelector("#fcsidu-rememberIDs-check").addEventListener("change", function() {
  134. GM_setValue("rememberCopiedIDs", this.checked);
  135. if(!this.checked) {
  136. // Clear already remembered IDs on disable
  137. GM_setValue("rememberedIDs", []);
  138. }
  139.  
  140. location.reload();
  141. });
  142. }
  143.  
  144. function updateIDs() {
  145. rememberedIDs = GM_getValue("rememberedIDs", []);
  146. let idElems = document.querySelectorAll(".fcsidu-session-id");
  147. for(let i = 0; i < idElems.length; i++) {
  148. if(rememberedIDs.includes(idElems[i].innerText.trim())) {
  149. idElems[i].classList.add("fcsidu-rememberedID");
  150. idElems[i].title = "ID has been previously copied";
  151. }
  152. }
  153. }
  154.  
  155. new MutationObserver(function(event) { parsePosts(); }).observe(document.querySelector("div.thread"), {childList: true});
  156. setupOptions();
  157. parsePosts();