Created
April 20, 2021 09:18
-
-
Save PluieElectrique/5080cd1614ce47464c8419ef2972c927 to your computer and use it in GitHub Desktop.
Make a dead thread look like it's still alive.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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(/>>>>/g, ' class="quotelink">>>>') | |
// 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