Skip to content

Instantly share code, notes, and snippets.

@PluieElectrique
Created April 20, 2021 09:18
Show Gist options
  • Save PluieElectrique/5080cd1614ce47464c8419ef2972c927 to your computer and use it in GitHub Desktop.
Save PluieElectrique/5080cd1614ce47464c8419ef2972c927 to your computer and use it in GitHub Desktop.
Make a dead thread look like it's still alive.
/* Thread Reanimater
* =================
*
* Make a dead thread look like it's still alive.
*
*
* How to use
* ==========
*
* If you want to use a board other than /mlp/ or an archive other than Desu,
* see "Configuration" below.
*
* 1. Open any live thread (i.e. https://boards.4channel.org/mlp/thread/...)
* 2. On that page, open the JavaScript console of your browser (look it up if
* you don't know how), paste this entire file, and press enter.
* 3. Find the thread you want on Desuarchive and copy the ID.
* 4. Run the following command in the console, substituting THREAD_ID for your
* thread ID:
*
* reanimateThread(THREAD_ID)
*
* If you want to show deleted posts, pass true as the second argument:
*
* reanimateThread(THREAD_ID, true)
*
* You can do this as many times as you want, but if you refresh the page,
* you'll have to start over from step 2.
*
*
* Configuration
* =============
*
* This script is designed for /mlp/ and Desu, but it may work with other
* boards and archives.
* Change "BOARD" to the board you want. You can find "bump_limit",
* "image_limit", and "custom_spoilers" here: <https://a.4cdn.org/boards.json>.
* "ARCHIVE" must be the URL of a FoolFuuka archive (it should say so in the
* footer or somewhere on the main page).
*
*/
var BOARD = "mlp",
BUMP_LIMIT = 500,
IMAGE_LIMIT = 300,
CUSTOM_SPOILERS = 1,
ARCHIVE = "https://desuarchive.org";
/* Security PSA
* ============
*
* It's a bad idea to paste random scripts into your browser's dev console.
* There are no passwords to steal on 4chan, but scripts could still mine
* crypto, record your IP address/posts, delete settings/hidden
* threads/filters, steal CAPTCHAs, etc.
*
* This script doesn't do any of that (which is also exactly what a liar would
* say), but in general, never paste scripts into sites that are actually
* important (e.g. email or bank account). Okay, I'll shut up now.
*
*
* Limitations
* ===========
*
* - The archive doesn't say if a file was deleted, so deleted files will
* appear as normal.
* - Quotelinks to deleted posts appear as cross-thread links and are crossed
* out when you hover over them.
* - since4pass is not supported.
*
*/
function reanimateThread(threadId, showDeleted) {
// https://foolfuuka.readthedocs.io/en/latest/code_guide/documentation/api.html#thread
$.get(`${ARCHIVE}/_/api/chan/thread/?board=${BOARD}&num=${threadId}`, {
onload: function () {
_reanimateThread(this.responseText, threadId, showDeleted);
},
onerror: function () {
console.log("Could not fetch thread:", this.status, this.statusText);
}
});
}
function _reanimateThread(responseText, threadId, showDeleted) {
let response = JSON.parse(responseText);
if ("error" in response) {
console.log("Error:", response.error);
return;
}
let thread = response[threadId],
fragment = document.createDocumentFragment(),
replies = 0,
images = 0,
uniqueIps;
// Prevent boardBlock from showing up
Main.board = BOARD;
if ("exif" in thread.op && thread.op.exif !== null) {
// Desuarchive
let exif = JSON.parse(thread.op.exif);
if ("uniqueIps" in exif) {
uniqueIps = exif.uniqueIps;
}
} else if (
"extra_data" in thread.op &&
thread.op.extra_data !== null &&
"uniqueIps" in thread.op.extra_data
) {
// 4plebs
uniqueIps = thread.op.extra_data.uniqueIps;
} else if (
thread.op.media !== null &&
"exif" in thread.op.media &&
thread.op.media.exif !== null
) {
// Archive of Sins
uniqueIps = JSON.parse(thread.op.media.exif).uniqueIps;
}
// Setup custom spoiler
Parser.setCustomSpoiler(BOARD, CUSTOM_SPOILERS);
// Add OP (not counted in the reply and image counts)
fragment.appendChild(parsePost(thread.op));
// Add replies
for (let postId in thread.posts) {
let post = thread.posts[postId];
if (!showDeleted && post.deleted === "1") continue;
// Always ignore ghost posts
if (post.subnum !== "0") continue;
fragment.appendChild(parsePost(post));
replies++;
if (post.media !== null) images++;
}
// Insert posts
let threadEl = document.querySelector(".thread");
threadEl.id = "t" + threadId;
threadEl.textContent = "";
threadEl.appendChild(fragment);
// Add post menus, backlinks, etc.
Parser.parseThread(threadId, -(replies + 1));
// Update thread stats
if (Config.threadStats) {
ThreadStats.update(
replies,
images,
uniqueIps,
replies >= BUMP_LIMIT,
images >= IMAGE_LIMIT
);
}
}
// admin_highlight is not supported as it's indistinguishable from admin
var CAPCODES = {
A: "admin",
D: "developer",
F: "founder",
G: "manager",
M: "mod",
N: undefined
};
// Create a post element from an archive object
function parsePost(archive) {
let post = {
no: parseInt(archive.num),
now: archive.fourchan_date + ":" + ("0" + archive.timestamp % 60).slice(-2),
name: archive.name,
trip: archive.trip,
com: processComment(archive.comment_processed),
time: archive.timestamp
};
let isOP = archive.op === "1";
if (isOP) {
post.sub = archive.title || "";
post.resto = 0;
post.sticky = parseInt(archive.sticky);
post.closed = parseInt(archive.locked);
post.id = archive.poster_hash;
post.capcode = CAPCODES[archive.capcode];
post.country = archive.poster_country;
post.country_name = archive.poster_country_name || "";
// Only /f/ has tags. This works for 4plebs, at least.
if (
archive.media !== null &&
"exif" in archive.media &&
archive.media.exif !== null
) {
let exif = JSON.parse(archive.media.exif);
if ("Tag" in exif) {
post.tag = exif.Tag;
}
}
} else {
post.resto = parseInt(archive.thread_num);
}
if (archive.media !== null) {
let mediaFilename = archive.media.media_filename.split(".");
post.filename = mediaFilename[0];
post.ext = "." + mediaFilename[1];
post.w = parseInt(archive.media.media_w);
post.h = parseInt(archive.media.media_h);
post.tn_w = parseInt(archive.media.preview_w);
post.tn_h = parseInt(archive.media.preview_h);
post.tim = parseInt(archive.media.preview_orig.slice(0, -5));
post.md5 = archive.media.media_hash;
post.fsize = parseInt(archive.media.media_size);
post.spoiler = parseInt(archive.media.spoiler);
}
let postEl = Parser.buildHTMLFromJSON(post, BOARD, isOP);
if (isOP) {
// We use standalone = true so that postType = "op", but this means we have
// to remove replySpan (and the empty text node before it).
let replySpan = postEl.querySelector(".replylink").parentNode;
replySpan.parentNode.removeChild(replySpan.previousSibling);
replySpan.parentNode.removeChild(replySpan);
}
if (archive.media !== null) {
// Fix links to full image
for (let link of postEl.querySelectorAll(".file a")) {
link.href = archive.media.media_link;
}
// Fix thumbnail link if the image isn't spoilered
let thumbnail = postEl.querySelector(".fileThumb:not(.imgspoiler) img");
if (thumbnail !== null) {
thumbnail.src = archive.media.thumb_link;
}
}
return postEl;
}
function processComment(com) {
// Slow, but it should be good enough.
return com
// Greentext
.replace(/<span class="greentext"/g, '<span class="quote"')
// Quotelinks
.replace(/<a href=".+?(\d+)\/?" class="backlink( op)?"/g, '<a href="#p$1" class="quotelink"')
// Cross-board links
.replace(/>&gt;&gt;&gt;/g, ' class="quotelink">&gt;&gt;&gt;')
// Clean regular links
.replace(/<a href="([^"]+)" target="_blank" rel="nofollow">[^>]+<\/a>/g, "$1");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment