Advertisement
Guest User

Untitled

a guest
Apr 20th, 2022
277
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name        4chan GhostPostMixer
  3. // @namespace   Violentmonkey Scripts
  4. // @match       https://boards.4channel.org/a/thread/*
  5. // @match       https://boards.4chan.org/aco/thread/*
  6. // @match       https://boards.4channel.org/an/thread/*
  7. // @match       https://boards.4channel.org/c/thread/*
  8. // @match       https://boards.4channel.org/cgl/thread/*
  9. // @match       https://boards.4channel.org/co/thread/*
  10. // @match       https://boards.4chan.org/d/thread/*
  11. // @match       https://boards.4channel.org/fit/thread/*
  12. // @match       https://boards.4channel.org/g/thread/*
  13. // @match       https://boards.4chan.org/gif/thread/*
  14. // @match       https://boards.4channel.org/his/thread/*
  15. // @match       https://boards.4channel.org/int/thread/*
  16. // @match       https://boards.4channel.org/k/thread/*
  17. // @match       https://boards.4channel.org/m/thread/*
  18. // @match       https://boards.4channel.org/mlp/thread/*
  19. // @match       https://boards.4channel.org/mu/thread/*
  20. // @match       https://boards.4channel.org/q/thread/*
  21. // @match       https://boards.4channel.org/qa/thread/*
  22. // @match       https://boards.4chan.org/r9k/thread/*
  23. // @match       https://boards.4channel.org/tg/thread/*
  24. // @match       https://boards.4chan.org/trash/thread/*
  25. // @match       https://boards.4channel.org/vr/thread/*
  26. // @match       https://boards.4channel.org/wsg/thread/*
  27. // @version     1.1
  28. // @author      anon && a random husky lover from /an/
  29. // @grant       GM_xmlhttpRequest
  30. // @grant       GM.xmlHttpRequest
  31. // @description Interleave ghost posts from the archives into 4chan threads.  This is a prototype.
  32. // ==/UserScript==
  33.  
  34. /*
  35.  * The idea needs to be expanded further.
  36.  * - More boards need to be supported.
  37.  * - It could use more error checking.
  38.  */
  39.  
  40. // Based on https://gist.github.com/g-gundam/8f9985e6aaa0dab6eecc556ddcbca370
  41.  
  42. const template = `<div class="postContainer replyContainer {{postClass}}" id="pc{{postId}}">
  43. <div class="sideArrows" id="sa{{postId}}">&gt;&gt;</div>
  44. <div id="p{{postId}}" class="post reply {{replyClass}}">
  45.     <div class="postInfoM mobile" id="pim{{postId}}">
  46.         <span class="nameBlock"><span class="name">{{authorName}}</span><br /></span>
  47.         <span class="dateTime postNum" data-utc="unixTime">{{formattedDate}} <a href="#p{{postId}}" title="Link to this post">No.</a><a href="javascript:quote('{{postId}}');" title="Reply to this post">{{postId}}</a></span>
  48.     </div>
  49.     <div class="postInfo desktop" id="pi{{postId}}">
  50.         <span class="nameBlock"><span class="name">{{authorName}}</span> </span> {{timeHtml}}
  51.         <span class="postNum desktop"><a href="#p{{postId}}" title="Link to this post">No.</a><a href="javascript:quote('{{postId}}');" title="Reply to this post">{{postId}}</a></span>
  52.     </div>
  53.     {{fileBlock}}
  54.     <blockquote class="postMessage" id="m{{postId}}">{{contentHtml}}</blockquote>
  55. </div>
  56. </div>`;
  57.  
  58. const fileTemplate = `<div class="file" id="f{{postId}}">
  59. <div class="fileText" id="fT{{postId}}">File: <a title="{{fileName}}" href="{{fileUrl}}" target="_blank">{{fileName}}</a> ({{fileMeta}})</div>
  60. <a class="fileThumb" href="{{fileUrl}}" target="_blank">
  61.     <img src="{{fileUrl}}" width="{{fileWidth}}" height="{{fileHeight}}"/>
  62.     <div data-tip="" data-tip-cb="mShowFull" class="mFileInfo mobile">{{fileMeta}}</div>
  63. </a>
  64. </div>`
  65.  
  66. const backLinkTemplate = `<a href="#p{{postId}}" class="quotelink" data-function="highlight" data-backlink="true" data-board="an" data-post="{{postId}}">&gt;&gt;{{postId}}</a>`;
  67.  
  68. // https://gist.github.com/GitHub30/59e002a5f57a4df0decbd7ac23290f77
  69. async function get(url) {
  70.     return new Promise((resolve) => {
  71.         GM.xmlHttpRequest({
  72.             method: "GET",
  73.             url,
  74.             onload: resolve,
  75.         });
  76.     });
  77. }
  78.  
  79. function renderTemplate(template, data) {
  80.     return template.replace(/{{([^}]+)}}/g, (match, key) => data[key] ?? '');
  81. }
  82.  
  83. function htmlToElement(html) {
  84.     var template = document.createElement('template');
  85.     html = html.trim(); // Never return a text node of whitespace as the result
  86.     template.innerHTML = html;
  87.     return template.content.firstChild;
  88. }
  89.  
  90. function extractPostVariables(post) {
  91.     const postId = post.id;
  92.  
  93.     const content = post.querySelector(".text");
  94.     const contentText = content?.innerText ?? "";
  95.     const contentHtml = content?.innerHTML?.replace(/https:\/\/desuarchive\.org\/\w+\/thread\/\d+\/#(\d+)/gi, '#p$1').replace(/backlink/gi, 'quotelink').replace(/\n/g, "<br />") ?? "";
  96.     const file = post.querySelector(".post_file");
  97.     const fileName = file?.querySelector(".post_file_filename")?.innerText ?? "";
  98.     const fileUrl = file?.querySelector(".post_file_filename")?.getAttribute('href') ?? "";
  99.     const fileMeta = (file?.querySelector(".post_file_metadata")?.innerText ?? "").trim();
  100.     const fileWidth = file ? post.querySelector(".thread_image_box img").width : 0;
  101.     const fileHeight = file ? post.querySelector(".thread_image_box img").height : 0;
  102.  
  103.     const authorName = `${post.querySelector(".post_author").innerText} ${post.querySelector(".post_tripcode").innerText}`.trim();
  104.     const timeHtml = post.querySelector(".time_wrap").innerHTML;
  105.  
  106.     return {
  107.         content: contentText,
  108.         contentHtml,
  109.         fileName,
  110.         fileUrl,
  111.         fileMeta,
  112.         fileWidth,
  113.         fileHeight,
  114.         postId,
  115.         authorName,
  116.         timeHtml,
  117.     };
  118. }
  119.  
  120. // Create a new DOM element suitable for insertion into a 4chan thread.
  121. function postTemplate(post, vars) {
  122.     const data = extractPostVariables(post);
  123.  
  124.     // set some conditional parameters (we could extract these from the posts but the original code does it this way)
  125.     data.postId = vars.n ? `${vars.parentId}_${vars.n}` : vars.parentId;
  126.     data.replyClass = vars.deleted ? 'del' : 'ghost';
  127.     data.postClass = vars.deleted ? 'post-deleted': 'post-ghost';
  128.  
  129.     if (data.fileUrl) {
  130.         data.fileBlock = renderTemplate(fileTemplate, data);
  131.     }
  132.  
  133.     return htmlToElement(renderTemplate(template, data));
  134. }
  135.  
  136. // Go throught the entire thread and fix all dead links if we inserted a deleted posts from the archive
  137. // This works with the built in extension
  138. function fixDeadLinks(postId) {
  139.     const deadLinks = Array.from(document.body.querySelectorAll('.thread .deadlink')).filter(e => e.innerText == `>>${postId}`);
  140.     for (const deadLink of deadLinks) {
  141.         deadLink.replaceWith(htmlToElement(renderTemplate(backLinkTemplate, { postId })));
  142.     }
  143. }
  144.  
  145.  
  146. function insertGhost(post, threadId) {
  147.     const [parentId, n] = post.id.split("_").map((x) => parseInt(x, 10));
  148.     //console.log('ag', {parentId, n})
  149.     let parent = document.getElementById(`pc${parentId}`);
  150.     if (n > 1) {
  151.         // if the n is higher than 1, we need to find the parent ghost post
  152.         // but in some cases, the parent ghost post was deleted, in which case we insert the
  153.         // ghost post after the main parent post.
  154.         parent = document.getElementById(`pc${parentId}_${n - 1}`) || parent;
  155.     }
  156.     if (parent) {
  157.         const newPost = postTemplate(post, { parentId, n });
  158.         parent.append(newPost);
  159.     }
  160. }
  161.  
  162. function insertDeleted(post, posts) {
  163.     const postId = parseInt(post.id, 10);
  164.     let i = Array.prototype.findIndex.call(
  165.         posts,
  166.         (p) => parseInt(p.id) === postId
  167.     );
  168.     if (i === -1) return;
  169.     if (i === 0) {
  170.         // Deleted post is the first post in the thread. This would require recreating the entire thread.
  171.         // TODO: Just redirect to the archives, idk.
  172.         console.error('deleted post is first post in thread');
  173.         return;
  174.     }
  175.     const newPost = postTemplate(post, { parentId: postId, n: 0, deleted: true });
  176.     let before = posts[i - 1];
  177.     let target;
  178.     if (before) target = document.getElementById(`pc${before.id}`);
  179.     if (target) {
  180.         target.after(newPost);
  181.     } else {
  182.         // XXX - This is a terrible hack that I wish I didn't have to do.
  183.         // If I can find a way around it, I will.
  184.         // Apparently target.after(newPost) isn't as synchronous as it looks.
  185.  
  186.         // I'm not sure why was it an issue, help?
  187.         console.log("f", postId, i);
  188.         setTimeout(() => {
  189.             const target = document.getElementById(`pc${before.id}`);
  190.             if (!target) return;
  191.             target.after(newPost);
  192.         }, 100);
  193.     }
  194.  
  195.     fixDeadLinks(postId);
  196. }
  197.  
  198. async function main() {
  199.     // Get thread id
  200.     const parts = window.location.pathname.split("/");
  201.     const threadId = parseInt(parts[3]);
  202.     const boardId = parts[1];
  203.  
  204.     console.log('interlacing posts');
  205.     document.body.classList.add('interlacing-loader');
  206.     // Fetch thread from archives
  207.     const archiveUrl = `https://desuarchive.org/${boardId}/thread/${threadId}/`;
  208.     const res = await get(archiveUrl);
  209.     // TODO: Check if the thread actually exists on the archive
  210.     const parser = new DOMParser();
  211.     const doc = parser.parseFromString(res.responseText, "text/html");
  212.     const posts = doc.querySelectorAll("article.post");
  213.     const ghosts = doc.querySelectorAll("article.post.post_ghost");
  214.     const trash = doc.querySelectorAll(".icon-trash");
  215.     const deleted = Array.prototype.map.call(trash, (t) => t.closest("article"));
  216.  
  217.     deleted.forEach((post) => insertDeleted(post, posts));
  218.     ghosts.forEach((post) => insertGhost(post, threadId));
  219.  
  220.     // Update the thread stats with what we interlaced
  221.     console.log(`interlaced ${deleted.length} deleted posts and ${ghosts.length} ghost posts`);
  222.     document.body.querySelectorAll('.thread-stats .ts-replies').forEach(e => e.insertAdjacentElement('afterend', htmlToElement(`<span class="text-muted">&nbsp;[d: ${deleted.length}, g: ${ghosts.length}]</span>`)));
  223.     document.body.classList.remove('interlacing-loader');
  224. }
  225.  
  226. // Add CSS
  227. const css = `
  228. div.post.ghost {
  229.     background-color: #ddd;
  230. }
  231. div.post.del {
  232.     background-color: #eab3b3;
  233. }
  234.  
  235. .text-muted {
  236.     color: #6c757d!important;
  237. }
  238.  
  239. .post-ghost {
  240.     margin-left: 2em;
  241. }
  242.  
  243. body.interlacing-loader::before {
  244.     content: '';
  245.     position: fixed;
  246.     bottom: 0;
  247.     left: 0;
  248.     border-bottom: 0.4rem solid red;
  249.     animation: loading 2s linear infinite;
  250. }
  251.  
  252. @keyframes loading {
  253.     0% {
  254.         left:0%;
  255.         right:100%;
  256.         width:0%;
  257.     }
  258.     10% {
  259.         left:0%;
  260.         right:75%;
  261.         width:25%;
  262.     }
  263.     90% {
  264.         right:0%;
  265.         left:75%;
  266.         width:25%;
  267.     }
  268.     100% {
  269.         left:100%;
  270.         right:0%;
  271.         width:0%;
  272.     }
  273. }
  274. `;
  275. const style = document.createElement("style");
  276. style.type = "text/css";
  277. style.appendChild(document.createTextNode(css));
  278. document.head.appendChild(style);
  279.  
  280. // Run main code
  281. main();
  282.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement