2 // @name Thread Rebuilder
3 // @namespace http://tampermonkey.net/
5 // @description try to take over the world!
6 // @author ECHibiki /qa/
7 // @match https://boards.4chan.org/*/thread/*
8 // @match http://boards.4chan.org/*/thread/*
9 // @grant GM_xmlhttpRequest
10 // @updateURL https://github.com/ECHibiki/4chan-UserScripts/raw/master/Thread-Rebuilder.user.js
11 // @downloadURL https://github.com/ECHibiki/4chan-UserScripts/raw/master/Thread-Rebuilder.user.js
12 // @run-at document-start
16 var thread_data = [['Comment'], ['Image URLs'], ['Image Names'] ,['Post No.']];
18 var semaphore_posts = 1;
21 var use_offsite_archive = false;
22 var window_displayed = false;
23 var in_sequence = false;
26 //set listener to build interface in 4chanX
27 //set listeners to build interface in 4chanX
28 document.addEventListener("4chanXInitFinished", function(e){
29 document.addEventListener("QRDialogCreation", enhance4ChanX);
34 use_offsite_archive = localStorage.getItem("ArchiveType") == 0 ? true : false;
35 if(use_offsite_archive) document.getElementById("OffsiteArchive").checked = true;
36 else document.getElementById("OnsiteArchive").checked = true;
42 function storageAvailable(type) {
44 var storage = window[type],
45 x = '__storage_test__';
46 storage.setItem(x, x);
47 storage.removeItem(x);
51 //From https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
52 return e instanceof DOMException && (
53 // everything except Firefox
57 // test name field too, because code might not be present
58 // everything except Firefox
59 e.name === 'QuotaExceededError' ||
61 e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
62 // acknowledge QuotaExceededError only if there's something already stored
67 //settings for time expiration on image hiding
68 function rebuildWindow(){
69 var style = document.createElement('style');
70 style.innerHTML = ".inputs{background-color:rgb(200,200,200);margin:5px 7px;width:100px;}";
71 document.body.appendChild(style);
73 var background_div = document.createElement("div");
74 background_div.setAttribute("style", "border:solid 1px black;position:fixed;width:100%;height:100%;background-color:rgba(200,200,200,0.3);top:0;left:0;display:none; z-index:9");
75 background_div.setAttribute("id", "rebuildBackground");
76 document.body.appendChild(background_div);
77 background_div.addEventListener("click", rebuildToggle);
79 var window_div = document.createElement("div");
80 window_div.setAttribute("style", "border:solid 1px black;position:fixed;width:400px;background-color:rgb(200,200,200);left:40%;top:20%;margin-bottom:0; display:none; z-index:10");
81 window_div.setAttribute("id", "rebuildWindow");
83 var close_div = document.createElement("div");
84 close_div.setAttribute("style", "border:solid 1px black;position:absolute;width:25px;height:25px;background-color:rgba(255,100,90,0.9); right:3px;top:3px; z-index:10");
85 close_div.addEventListener("click", rebuildToggle);
86 window_div.appendChild(close_div);
88 var title_para = document.createElement("p");
89 title_para.setAttribute("style", "margin-left:5px;margin-top:5px");
90 var title_text = document.createTextNode("Rebuild Settings");
91 title_para.appendChild(title_text);
92 window_div.appendChild(title_para);
94 var container_div = document.createElement("div");
95 container_div.setAttribute("style","background-color:white;margin:0 0;padding:5px;");
96 window_div.appendChild(container_div);
98 var rebuild_label_local = document.createElement("label");
99 var rebuild_text_local = document.createTextNode("Use 4chan Archives: ");
100 rebuild_label_local.appendChild(rebuild_text_local);
101 container_div.appendChild(rebuild_label_local);
102 var rebuild_input_local = document.createElement("input");
103 rebuild_input_local.setAttribute("type", "radio");
104 rebuild_input_local.setAttribute("name", "ArchiveSettings");
105 rebuild_input_local.setAttribute("id", "OnsiteArchive");
106 container_div.appendChild(rebuild_input_local);
107 container_div.appendChild(rebuild_input_local);
108 container_div.appendChild(document.createElement("br"));
110 var rebuild_label_offsite = document.createElement("label");
111 var rebuild_text_offsite = document.createTextNode("Use Offsite Archives: ");
112 rebuild_label_offsite.appendChild(rebuild_text_offsite);
113 container_div.appendChild(rebuild_label_offsite);
114 var rebuild_input_offsite = document.createElement("input");
115 rebuild_input_offsite.setAttribute("type", "radio");
116 rebuild_input_offsite.setAttribute("name", "ArchiveSettings");
117 rebuild_input_offsite.setAttribute("id", "OffsiteArchive");
118 container_div.appendChild(rebuild_input_offsite);
119 container_div.appendChild(rebuild_input_offsite);
120 container_div.appendChild(document.createElement("br"));
122 var set_button = document.createElement("input");
123 set_button.setAttribute("type", "button");
124 set_button.setAttribute("id", "setTime");
125 set_button.setAttribute("value", "Set Archive");
126 set_button.addEventListener("click", function(){
127 if (storageAvailable('localStorage')) {
128 var radio_options = document.getElementsByName("ArchiveSettings");
129 for (var radio_input = 0 ; radio_input < radio_options.length; radio_input++)
130 if(radio_options[radio_input].checked){
131 localStorage.setItem("ArchiveType", radio_input);
132 if(radio_input == 0) use_offsite_archive = true;
137 container_div.appendChild(set_button);
139 document.body.appendChild(window_div);
143 function rebuildToggle(){
144 if(window_displayed){
145 document.getElementById("rebuildWindow").style.display = "none";
146 document.getElementById("rebuildBackground").style.display = "none";
147 window_displayed = false;
150 document.getElementById("rebuildWindow").style.display = "inline-block";
151 document.getElementById("rebuildBackground").style.display = "inline-block";
152 window_displayed = true;
156 function rebuildButton(){
157 var rebuild_button = document.createElement("input");
158 rebuild_button.setAttribute("Value", "Thread Rebuilder Settings");
159 rebuild_button.setAttribute("type", "button");
160 rebuild_button.setAttribute("style", "position:absolute;top:105px");
161 rebuild_button.addEventListener("click", rebuildWindow);
162 if(document.body === null){
163 setTimeout(rebuildButton, 30);
166 document.body.appendChild(rebuild_button);
167 rebuild_button.addEventListener("click", rebuildToggle);
171 var enhance4ChanX = function(){
172 var qr_window = document.getElementById("qr");
174 if(document.getElementById("qrRebuilder") !== null) qr_window.removeChild(document.getElementById("qrRebuilder"));
176 var thread_rebuilder_table = document.createElement("TABLE");
177 thread_rebuilder_table.setAttribute("id", "qrRebuilder");
178 thread_rebuilder_table.setAttribute("style", "text-align:center");
179 qr_window.appendChild(thread_rebuilder_table);
181 var instruction_row = document.createElement("TR");
182 var top_row_nodes = [document.createElement("BR"),
183 document.createTextNode("Insert the thread number of the post to rebuild"),
184 document.createElement("BR"),
185 document.createTextNode("Must be in either the 4chan archives or archived.moe"),
186 document.createElement("BR"),
188 top_row_nodes.forEach(
190 instruction_row.appendChild(node);
192 thread_rebuilder_table.appendChild(instruction_row);
194 var thread_row = document.createElement("TR");
195 var second_row_nodes = [
196 document.createTextNode("Thread: "),
197 document.createElement("INPUT"),
198 document.createElement("INPUT"),
200 second_row_nodes.forEach(
202 thread_row.appendChild(node);
204 thread_rebuilder_table.appendChild(thread_row);
206 second_row_nodes[1].setAttribute("ID", "threadInput");
207 second_row_nodes[1].setAttribute("style", "width:44.9%");
209 second_row_nodes[2].setAttribute("ID", "threadButton");
210 second_row_nodes[2].setAttribute("type", "button");
211 second_row_nodes[2].setAttribute("value", "Set Rebuild Queue");
213 second_row_nodes[2].addEventListener("click", function(){
216 getThread(second_row_nodes[1].value);
217 postID = setInterval(postRoutine, 1000);
218 if(timeListen === undefined) timeListen = setInterval(timeListenerFunction, 1000);
222 var thread_data_length = 0;
223 var posts_created = 0;
225 var postRoutine = function(){
228 thread_data_length = thread_data[0].length;
229 fillID = setInterval(fillRoutine, 10);
234 var stopRoutine = function(){
235 clearInterval(postID);
239 var fillRoutine = function(){
240 if(posts_created >= thread_data_length) {semaphore_posts = 0 ; stopFillRoutine();}
241 else if(semaphore_posts == 1){
243 createPost(thread_data[0][posts_created], thread_data[1][posts_created], thread_data[2][posts_created]);
248 var stopFillRoutine = function(){
249 clearInterval(fillID);
252 var setPropperLinking = function(text){
253 var search_regex = RegExp(">>\\d+", "g");
256 var link_arr = Array();
257 while((result = search_regex.exec(text)) != null){
258 var end_index = search_regex.lastIndex;
259 var post_no = result.toString().replace(/>/g, "");
260 link_arr.push([post_no, end_index]);
262 //hunt down the text of what it linked to
263 //Get the links inside of the origonal message to show text contents
264 var responding_text = Array();
265 if(use_offsite_archive)
266 URL = "https://www.archived.moe/_/api/chan/thread/?board=" + board + "&num=" + document.getElementById("threadInput").value;
268 URL = "https://a.4cdn.org/" + board + "/thread/" + document.getElementById("threadInput").value + ".json";
269 var xhr = new GM_xmlhttpRequest(({
272 responseType : "json",
273 onload: function(data){
274 if(use_offsite_archive)
275 data = data.response["" + document.getElementById("threadInput").value]["posts"];
277 data = data.response["posts"];
278 if(data == undefined){
279 alert("Invalid Thread ID: " + document.getElementById("threadInput").value + ". ");
282 link_arr.forEach(function(link_item){
283 for(var data_entry = 0 ; data_entry < data.length ; data_entry++){
284 if(parseInt(link_item[0]) == parseInt(data[data_entry]["no"])){
285 if(use_offsite_archive)
286 responding_text.push([ [post_no, end_index], data[data_entry]["comment_processed"].replace(/(>>|https:\/\/www\.archived\.moe\/.*\/thread\/.*\/#)\d+/g, ""), link_item["media"]["safe_media_hash"] ]);
288 responding_text.push([ [post_no, end_index], data[data_entry]["com"].replace(/(>>|#p)\d+/g, ""), data[data_entry]["md5"] ]);
294 var current_url = window.location.href;
295 var hash_index = current_url.lastIndexOf("#") != -1 ? current_url.lastIndexOf("#"): window.location.href.length;
296 var current_thread = window.location.href.substring(current_url.lastIndexOf("/")+1, hash_index);
297 var current_url = "https://a.4cdn.org/" + board + "/thread/" + current_thread + ".json";
298 //open current thread to hunt down the text found in links
299 var xhr = new GM_xmlhttpRequest(({
302 responseType : "json",
303 onload: function(data){
304 data = data.response["posts"];
305 if(data == undefined){
306 alert("Invalid Thread ID: " + document.getElementById("threadInput").value + ". ");
309 responding_text.forEach(function(response_item){
310 for(var data_entry = 0 ; data_entry < data.length ; data_entry++){
311 if((response_item[1] == data[data_entry]["com"].replace(/(>>|#p)\d+/g, "") || response_item[1] == null)
312 && (response_item[2] == data[data_entry]["md5"] || response_item[2] == null)){
313 var start_index = response_item[0][0].legth - response_item[0][1];
314 text = text.substring(0, start_index) + ">>" + data[data_entry]["no"] + text.substring(response_item[0][1]);
319 document.getElementById("qr").getElementsByTagName("TEXTAREA")[0].value = text;
320 document.getElementById("add-post").click();
331 //2) GET ARCHIVED THREAD
332 var getThread = function(threadNo){
333 thread_data = [[], [], [], []];
335 if(use_offsite_archive)
336 URL = "https://www.archived.moe/_/api/chan/thread/?board=" + board + "&num=" + document.getElementById("threadInput").value;
338 URL = "https://a.4cdn.org/" + board + "/thread/" + document.getElementById("threadInput").value + ".json";
339 var xhr = new GM_xmlhttpRequest(({
342 responseType : "json",
343 onload: function(data){
344 var starting_post = -1;
345 if(use_offsite_archive){
347 data = data.response["" + document.getElementById("threadInput").value];
351 data = data.response;
353 if(data == undefined){
354 alert("Invalid Thread ID: " + threadNo + ".\n4chan Archive ");
357 if(use_offsite_archive) data["posts"] = Object.values(data["posts"]);
358 var len = data["posts"].length;
360 for(var post_number = starting_post ; post_number < len ; post_number++){
361 var comment = undefined;
362 if(use_offsite_archive)
363 comment = data["posts"][post_number]["comment"];
365 comment = data["posts"][post_number]["com"];
366 if(comment !== undefined && comment !== null)
367 thread_data[0].push(comment);
369 thread_data[0].push(-1);
371 var filename = undefined;
372 if(use_offsite_archive)
373 if(data["posts"][post_number]["media"] !== null)
374 filename = "" + data["posts"][post_number]["media"]["media_filename"];
376 filename = "" + data["posts"][post_number]["tim"] + data["posts"][post_number]["ext"];
378 if(filename !== undefined && filename !== null && filename.indexOf("undefined") == -1)
379 if(use_offsite_archive)
380 if(data["posts"][post_number]["media"] !== null)
381 thread_data[1].push(data["posts"][post_number]["media"]["remote_media_link"]);
382 else thread_data[1].push(-1);
384 thread_data[1].push("https://i.4cdn.org/" + board + "/" + filename);
385 else thread_data[1].push(-1);
387 if(use_offsite_archive)
388 if(data["posts"][post_number]["media"] !== null)
389 thread_data[2].push(data["posts"][post_number]["media"]["media_id"]);
391 thread_data[2].push(data["posts"][post_number]["filename"]);
393 if(use_offsite_archive)
394 thread_data[3].push(data["posts"][post_number]["num"]);
396 thread_data[3].push(data["posts"][post_number]["no"]);
403 //3) RIP POSTS AND IMAGES
404 var createPost = function(text, imageURL, imageName){
406 var response_type = "arraybuffer";
407 if(use_offsite_archive) response_type = "text"
408 var xhr = new GM_xmlhttpRequest(({
411 responseType : response_type,
412 onload: function(response)
414 if(use_offsite_archive){
415 var parser = new DOMParser();
416 var content_attribute = parser.parseFromString(response.response, "text/html").getElementsByTagName("META")[0].getAttribute("content");
417 var redirect_url = content_attribute.substring(content_attribute.indexOf("http"));
418 var xhr = new GM_xmlhttpRequest(({method:"GET", url: redirect_url, responseType:"arraybuffer",
419 onload:function(response){
420 inputImage(response, text, imageURL, imageName);
425 inputImage(response, text, imageURL, imageName);
431 text = createPostComment(text);
432 setPropperLinking(text);
436 function inputImage(response, text, imageURL, imageName){
439 if(imageURL.indexOf(".jpg") > -1){
440 blob = new Blob([response.response], {type:"image/jpeg"});
443 else if(imageURL.indexOf(".png") > -1){
444 blob = new Blob([response.response], {type:"image/png"});
447 else if(imageURL.indexOf(".gif") > -1){
448 blob = new Blob([response.response], {type:"image/gif"});
451 else if(imageURL.indexOf(".webm") > -1){
452 blob = new Blob([response.response], {type:"video/webm"});
456 var name = imageName + ext;
458 //SEND RESULTING RESPONSE TO 4CHANX FILES === QRSetFile
459 var detail = {file:blob, name:name};
460 if (typeof cloneInto === 'function') {
461 detail = cloneInto(detail , document.defaultView);
464 document.dispatchEvent(new CustomEvent('QRSetFile', {bubbles:true, detail}));
466 if(text !== "" && text !== undefined && text !== -1) {
467 text = createPostComment(text);
468 setPropperLinking(text);
471 document.getElementById("add-post").click();
476 //4) CREATE POST QUEUE
477 var createPostComment = function(text){
478 text = text.replace(/<a href="\/[a-zA-Z]+\/" class="quotelink">/g, "");
479 text = text.replace(/<span class="deadlink">/g, "");
481 var quote_regex = /<a href="#p[0-9]+" class="quotelink">>>[0-9]+/g;
482 var find = text.match(quote_regex);
484 find.forEach(function(match){
485 var index_start = text.indexOf(match);
486 var match_len = match.length;
487 var index_len = index_start + match_len;
488 var first_quote = match.indexOf('"');
489 var second_quote = match.indexOf('"', first_quote + 1);
490 var post_no = match.substring(first_quote + 3, second_quote);
492 match = ">>" + post_no;
494 text = text.substr(0, index_start) + match + text.substr(index_len);
498 text = text.replace(/<span class="quote">/g, "");
499 text = text.replace(/<br>/g, "\n");
500 text = text.replace(/'/g, "'");
501 text = text.replace(/>/g, ">");
502 text = text.replace(/<\/a>/g, "");
503 text = text.replace(/<wbr>/g, "");
504 text = text.replace(/<\/span>/g, "");
510 var timeListenerFunction = function(){
511 var time = document.getElementById("qr-filename-container").nextSibling.value.replace(/[a-zA-Z]+/g, "");
520 document.addEventListener('QRPostSuccessful', function(e) {
522 document.getElementById("dump-list").childNodes[1].click();
523 setPropperLinking(document.getElementById("qr").getElementsByTagName("TEXTAREA")[0].value);
529 thread_data_length = 0;
537 thread_data = [['Comment'], ['Image URLs'], ['Image Names'] ,['Post No.']];
539 var qr_dumplist = document.getElementById("dump-list").childNodes;
540 var qr_dumplist_len = qr_dumplist.length;
541 var current_preview = 0;
542 while(qr_dumplist_len - current_preview > 1){
543 qr_dumplist[0].firstChild.click();