Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name 4chan GhostPostMixer
- // @namespace Violentmonkey Scripts
- // @match https://boards.4channel.org/a/thread/*
- // @match https://boards.4chan.org/aco/thread/*
- // @match https://boards.4channel.org/an/thread/*
- // @match https://boards.4channel.org/c/thread/*
- // @match https://boards.4channel.org/cgl/thread/*
- // @match https://boards.4channel.org/co/thread/*
- // @match https://boards.4chan.org/d/thread/*
- // @match https://boards.4channel.org/fit/thread/*
- // @match https://boards.4channel.org/g/thread/*
- // @match https://boards.4chan.org/gif/thread/*
- // @match https://boards.4channel.org/his/thread/*
- // @match https://boards.4channel.org/int/thread/*
- // @match https://boards.4channel.org/k/thread/*
- // @match https://boards.4channel.org/m/thread/*
- // @match https://boards.4channel.org/mlp/thread/*
- // @match https://boards.4channel.org/mu/thread/*
- // @match https://boards.4channel.org/q/thread/*
- // @match https://boards.4channel.org/qa/thread/*
- // @match https://boards.4chan.org/r9k/thread/*
- // @match https://boards.4channel.org/tg/thread/*
- // @match https://boards.4chan.org/trash/thread/*
- // @match https://boards.4channel.org/vr/thread/*
- // @match https://boards.4channel.org/wsg/thread/*
- // @version 1.1
- // @author anon && a random husky lover from /an/
- // @grant GM_xmlhttpRequest
- // @grant GM.xmlHttpRequest
- // @description Interleave ghost posts from the archives into 4chan threads. This is a prototype.
- // ==/UserScript==
- /*
- * The idea needs to be expanded further.
- * - More boards need to be supported.
- * - It could use more error checking.
- */
- // Based on https://gist.github.com/g-gundam/8f9985e6aaa0dab6eecc556ddcbca370
- const template = `<div class="postContainer replyContainer {{postClass}}" id="pc{{postId}}">
- <div class="sideArrows" id="sa{{postId}}">>></div>
- <div id="p{{postId}}" class="post reply {{replyClass}}">
- <div class="postInfoM mobile" id="pim{{postId}}">
- <span class="nameBlock"><span class="name">{{authorName}}</span><br /></span>
- <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>
- </div>
- <div class="postInfo desktop" id="pi{{postId}}">
- <span class="nameBlock"><span class="name">{{authorName}}</span> </span> {{timeHtml}}
- <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>
- </div>
- {{fileBlock}}
- <blockquote class="postMessage" id="m{{postId}}">{{contentHtml}}</blockquote>
- </div>
- </div>`;
- const fileTemplate = `<div class="file" id="f{{postId}}">
- <div class="fileText" id="fT{{postId}}">File: <a title="{{fileName}}" href="{{fileUrl}}" target="_blank">{{fileName}}</a> ({{fileMeta}})</div>
- <a class="fileThumb" href="{{fileUrl}}" target="_blank">
- <img src="{{fileUrl}}" width="{{fileWidth}}" height="{{fileHeight}}"/>
- <div data-tip="" data-tip-cb="mShowFull" class="mFileInfo mobile">{{fileMeta}}</div>
- </a>
- </div>`
- const backLinkTemplate = `<a href="#p{{postId}}" class="quotelink" data-function="highlight" data-backlink="true" data-board="an" data-post="{{postId}}">>>{{postId}}</a>`;
- // https://gist.github.com/GitHub30/59e002a5f57a4df0decbd7ac23290f77
- async function get(url) {
- return new Promise((resolve) => {
- GM.xmlHttpRequest({
- method: "GET",
- url,
- onload: resolve,
- });
- });
- }
- function renderTemplate(template, data) {
- return template.replace(/{{([^}]+)}}/g, (match, key) => data[key] ?? '');
- }
- function htmlToElement(html) {
- var template = document.createElement('template');
- html = html.trim(); // Never return a text node of whitespace as the result
- template.innerHTML = html;
- return template.content.firstChild;
- }
- function extractPostVariables(post) {
- const postId = post.id;
- const content = post.querySelector(".text");
- const contentText = content?.innerText ?? "";
- const contentHtml = content?.innerHTML?.replace(/https:\/\/desuarchive\.org\/\w+\/thread\/\d+\/#(\d+)/gi, '#p$1').replace(/backlink/gi, 'quotelink').replace(/\n/g, "<br />") ?? "";
- const file = post.querySelector(".post_file");
- const fileName = file?.querySelector(".post_file_filename")?.innerText ?? "";
- const fileUrl = file?.querySelector(".post_file_filename")?.getAttribute('href') ?? "";
- const fileMeta = (file?.querySelector(".post_file_metadata")?.innerText ?? "").trim();
- const fileWidth = file ? post.querySelector(".thread_image_box img").width : 0;
- const fileHeight = file ? post.querySelector(".thread_image_box img").height : 0;
- const authorName = `${post.querySelector(".post_author").innerText} ${post.querySelector(".post_tripcode").innerText}`.trim();
- const timeHtml = post.querySelector(".time_wrap").innerHTML;
- return {
- content: contentText,
- contentHtml,
- fileName,
- fileUrl,
- fileMeta,
- fileWidth,
- fileHeight,
- postId,
- authorName,
- timeHtml,
- };
- }
- // Create a new DOM element suitable for insertion into a 4chan thread.
- function postTemplate(post, vars) {
- const data = extractPostVariables(post);
- // set some conditional parameters (we could extract these from the posts but the original code does it this way)
- data.postId = vars.n ? `${vars.parentId}_${vars.n}` : vars.parentId;
- data.replyClass = vars.deleted ? 'del' : 'ghost';
- data.postClass = vars.deleted ? 'post-deleted': 'post-ghost';
- if (data.fileUrl) {
- data.fileBlock = renderTemplate(fileTemplate, data);
- }
- return htmlToElement(renderTemplate(template, data));
- }
- // Go throught the entire thread and fix all dead links if we inserted a deleted posts from the archive
- // This works with the built in extension
- function fixDeadLinks(postId) {
- const deadLinks = Array.from(document.body.querySelectorAll('.thread .deadlink')).filter(e => e.innerText == `>>${postId}`);
- for (const deadLink of deadLinks) {
- deadLink.replaceWith(htmlToElement(renderTemplate(backLinkTemplate, { postId })));
- }
- }
- function insertGhost(post, threadId) {
- const [parentId, n] = post.id.split("_").map((x) => parseInt(x, 10));
- //console.log('ag', {parentId, n})
- let parent = document.getElementById(`pc${parentId}`);
- if (n > 1) {
- // if the n is higher than 1, we need to find the parent ghost post
- // but in some cases, the parent ghost post was deleted, in which case we insert the
- // ghost post after the main parent post.
- parent = document.getElementById(`pc${parentId}_${n - 1}`) || parent;
- }
- if (parent) {
- const newPost = postTemplate(post, { parentId, n });
- parent.append(newPost);
- }
- }
- function insertDeleted(post, posts) {
- const postId = parseInt(post.id, 10);
- let i = Array.prototype.findIndex.call(
- posts,
- (p) => parseInt(p.id) === postId
- );
- if (i === -1) return;
- if (i === 0) {
- // Deleted post is the first post in the thread. This would require recreating the entire thread.
- // TODO: Just redirect to the archives, idk.
- console.error('deleted post is first post in thread');
- return;
- }
- const newPost = postTemplate(post, { parentId: postId, n: 0, deleted: true });
- let before = posts[i - 1];
- let target;
- if (before) target = document.getElementById(`pc${before.id}`);
- if (target) {
- target.after(newPost);
- } else {
- // XXX - This is a terrible hack that I wish I didn't have to do.
- // If I can find a way around it, I will.
- // Apparently target.after(newPost) isn't as synchronous as it looks.
- // I'm not sure why was it an issue, help?
- console.log("f", postId, i);
- setTimeout(() => {
- const target = document.getElementById(`pc${before.id}`);
- if (!target) return;
- target.after(newPost);
- }, 100);
- }
- fixDeadLinks(postId);
- }
- async function main() {
- // Get thread id
- const parts = window.location.pathname.split("/");
- const threadId = parseInt(parts[3]);
- const boardId = parts[1];
- console.log('interlacing posts');
- document.body.classList.add('interlacing-loader');
- // Fetch thread from archives
- const archiveUrl = `https://desuarchive.org/${boardId}/thread/${threadId}/`;
- const res = await get(archiveUrl);
- // TODO: Check if the thread actually exists on the archive
- const parser = new DOMParser();
- const doc = parser.parseFromString(res.responseText, "text/html");
- const posts = doc.querySelectorAll("article.post");
- const ghosts = doc.querySelectorAll("article.post.post_ghost");
- const trash = doc.querySelectorAll(".icon-trash");
- const deleted = Array.prototype.map.call(trash, (t) => t.closest("article"));
- deleted.forEach((post) => insertDeleted(post, posts));
- ghosts.forEach((post) => insertGhost(post, threadId));
- // Update the thread stats with what we interlaced
- console.log(`interlaced ${deleted.length} deleted posts and ${ghosts.length} ghost posts`);
- document.body.querySelectorAll('.thread-stats .ts-replies').forEach(e => e.insertAdjacentElement('afterend', htmlToElement(`<span class="text-muted"> [d: ${deleted.length}, g: ${ghosts.length}]</span>`)));
- document.body.classList.remove('interlacing-loader');
- }
- // Add CSS
- const css = `
- div.post.ghost {
- background-color: #ddd;
- }
- div.post.del {
- background-color: #eab3b3;
- }
- .text-muted {
- color: #6c757d!important;
- }
- .post-ghost {
- margin-left: 2em;
- }
- body.interlacing-loader::before {
- content: '';
- position: fixed;
- bottom: 0;
- left: 0;
- border-bottom: 0.4rem solid red;
- animation: loading 2s linear infinite;
- }
- @keyframes loading {
- 0% {
- left:0%;
- right:100%;
- width:0%;
- }
- 10% {
- left:0%;
- right:75%;
- width:25%;
- }
- 90% {
- right:0%;
- left:75%;
- width:25%;
- }
- 100% {
- left:100%;
- right:0%;
- width:0%;
- }
- }
- `;
- const style = document.createElement("style");
- style.type = "text/css";
- style.appendChild(document.createTextNode(css));
- document.head.appendChild(style);
- // Run main code
- main();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement