// ==UserScript== // @name [s4s] interface // @namespace s4s4s4s4s4s4s4s4s4s // @version 3.2 // @author le fun css man AKA Doctor Worse Than Hitler, kekero // @email doctorworsethanhitler@gmail.com // @description Lets you view the greenposts. // @match https://boards.4chan.org/s4s/* // @match http://boards.4chan.org/s4s/* // @connect funposting.online // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant unsafeWindow // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTAgMEgxNlYxNkgwIiBmaWxsPSIjZGZkIi8+PHBhdGggZD0iTTMgNCA2IDFoNGwzIDN2OGwtMyAzSDZMMyAxMiIgZmlsbD0iZ3JlZW4iLz48cGF0aCBkPSJtNS41IDExLjVoLTJ2LTdoMnYtM2MtMyAwLTUgMi41LTUgNi41IDAgNCAyIDYuNSA1IDYuNXptNSAzYzMgMCA1LTIuNSA1LTYuNSAwLTQtMi02LjUtNS02LjV2M2gydjdoLTJ6bS00LTRoM0wxMCAyLjVINlptMCAzaDN2LTNoLTN6IiBmaWxsPSIjZmZmIiBzdHJva2U9ImdyZWVuIi8+PC9zdmc+ // @downloadURL none // ==/UserScript== "use strict"; if(query("#s4sinterface-css")){ throw "Multiple instances of [s4s] interface detected" } var interfaceLinkRegex = new RegExp(']*>>>\\d*( \\(.*\\))?<\\/a>()?-\\d*'); var weekdays=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"] var postForm={} var lastCommentForm var updateLinks=new Set() var cacheCatalogPosts={} var mode="" var threadId var pathName=location.pathname var threadMatch=pathName.match(/\/thread\/(\d+)/) if(threadMatch){ // /board/thread/1 mode="thread" threadId=threadMatch[1] }else if(/\/catalog$/.test(pathName)){ // /board/catalog mode="catalog" }else if(/^\/[^\/]+\/\d*$/.test(pathName)){ // /board/ mode="index" } if(typeof GM=="undefined"){ window.GM={ xmlHttpRequest:window.GM_xmlhttpRequest } } // Request green posts var serverurl="https://funposting.online/interface/" if(mode=="thread"){ getGreenPosts(threadId) }else if(mode=="catalog"){ onPageLoad(_=>{ getGreenPostsCatalog() }) }else if(mode=="index") { onPageLoad(_=>{ addGreenPostsToIndex() }) } function addGreenPostsToIndex() { var threads = document.getElementsByClassName("thread") for (var i = 0; i < threads.length; i++) { var responses = threads[i].getElementsByClassName("replyContainer") var since = 0 if(responses != null && responses.length > 0) { var since = threads[i].getElementsByClassName("replyContainer")[0].id.substr(2) } getGreenPosts(threads[i].id.substr(1), since) } } onPageLoad(_=>{ // Classic post form if(mode=="thread"){ var nameField=query("#postForm input[name=name]") if(nameField){ var commentField=query("#postForm textarea") addCommentForm(commentField,1) var greenToggle=element( ["button#toggle",{ class:"greenToggle", title:"[s4s] Interface", onclick:event=>{ event.preventDefault() event.stopPropagation() showPostFormClassic() } },"!"] ).toggle var nameParent=nameField.parentNode nameParent.classList.add("nameFieldParent") insertBefore(greenToggle,nameField) }else{ // Thread is archived showPostFormClassic() } getUpdateLinks() } }) // Native extension QR document.addEventListener("QRNativeDialogCreation",onQRCreated) if(unsafeWindow.Main){ onNativeextInit() }else{ document.addEventListener("4chanMainInit",onNativeextInit) } // 4chan-X QR integration if(document.documentElement.classList.contains("fourchan-x")){ on4chanXInit() }else{ document.addEventListener("4chanXInitFinished",on4chanXInit) } document.addEventListener("QRDialogCreation",onQRXCreated) function onPageLoad(func){ if(document.readyState=="loading"){ addEventListener("DOMContentLoaded",func) }else{ func() } } // replaces links like >>1234567-123 in native 4chan posts with an appropriate link back to the interface post. function replaceInterfaceLinks(post) { while(interfaceLinkRegex.test(post.innerHTML)) { var link = interfaceLinkRegex.exec(post.innerHTML)[0] //something like >>6696342 (You)-6754
test pls ignorlol var link_afterno = />\d+/.exec(link)[0].substr(4) var link_interfaceno = /-\d+/.exec(link)[0].substr(1) var has_span = //.test(link) // sometimes the end of the link starts with a so lets not forget it later var replace = '>>'+link_afterno+'-'+link_interfaceno+'' if(has_span) replace += '' post.innerHTML = post.innerHTML.replace(interfaceLinkRegex, replace) } } // Request green posts & add them function getGreenPosts(thread, since = 0){ GM.xmlHttpRequest({ method:"get", url:serverurl+"get.php?thread="+thread+((since != 0) ? "since="+since : ""), onload:response=>{ if(response.status==200){ onPageLoad(_=>{ var postsObj=JSON.parse(response.responseText) var postsCount=Object.keys(postsObj).length if(postsCount){ if(mode == "thread") { var oldPosts=queryAll(".greenPostContainer") for(var i=0;i{ } }) } // takes the JSON from the server and converts to an HTML element function postJsonToElement(aPost){ var numberless=aPost.options=="numberless" var afterNo=numberless?"XXXXXX":aPost.after_no var postId=afterNo+"-"+aPost.id var date=new Date(aPost.timestamp*1000) var dateString= padding(date.getMonth()+1,2)+"/"+ padding(date.getDate(),2)+"/"+ (""+date.getFullYear()).slice(-2)+ "("+weekdays[date.getDay()]+")"+ padding(date.getHours(),2)+":"+ padding(date.getMinutes(),2)+":"+ padding(date.getSeconds(),2) var linkReply if(!numberless){ linkReply=[0, " ", ["a",{ href:"#p"+postId, title:"Link to this post" },"No."], ["a",{ href:"javascript:quote('"+postId+"');", onclick:insertQuote, title:"Reply to this post" },postId] ] } var replyHideX=document.documentElement.classList.contains("reply-hide") var post=element( ["div#post",{ class:"postContainer replyContainer greenPostContainer", id:"pc"+aPost.after_no }, (replyHideX? ["div",{ id:"sa"+postId }, ["a",{ class:"hide-reply-button" }, ["span",{ class:"fa fa-minus-square-o" }] ] ] : ["div",{ class:"sideArrows", id:"sa"+postId },">>"] ), ["div",{ class:"post reply", id:"p"+postId }, ["div",{ class:"postInfoM mobile", id:"pim"+postId }, ["span",{ class:"nameBlock" }, ["span",{ class:"name" },aPost.username], ["br"] ], ["span",{ class:"dateTime postNum", "data-utc":aPost.timestamp }, dateString, linkReply ] ], ["div",{ class:"postInfo desktop", id:"pi"+postId }, ["input",{ type:"checkbox", name:"ignore", value:"delete" }], ["span",{ class:"nameBlock" }, ["span",{ class:"name" },aPost.username] ], " ", ["span",{ class:"dateTime", "data-utc":aPost.timestamp },dateString], (!numberless&& ["span",{ class:"postNum desktop", onclick:insertQuote, title:"Reply to this post" },linkReply] ) ], ["blockquote",{ class:"postMessage", id:"m"+postId, innerHTML:aPost.text.replace(/\r/g,"") }] ] ] ).post return post } // Add a post to the proper position in the thread function addPost(aPost,currentPost){ if(!currentPost){ currentPost=query(".thread>.postContainer") } var post=postJsonToElement(aPost) if(mode == "thread") { // Add the post while(currentPost){ var lastPost=currentPost if(!/^pc\d+$/.test(currentPost.id)||currentPost.id.slice(2)<=aPost.after_no){ currentPost=currentPost.nextSibling }else{ return insertBefore(post,currentPost) } } return insertAfter(post,lastPost) } else if(mode == "index") { return insertAfter(post,document.getElementById("pc"+aPost.after_no)) } } // Get green post count on catalog function getGreenPostsCatalog(){ var threadContainer=query(".is_catalog #threads,.catalog-mode .board") if(!threadContainer||!threadContainer.children.length){ if(mode=="catalog"){ return setTimeout(getGreenPostsCatalog,500) }else{ var insertListener=event=>{ document.removeEventListener("PostsInserted",insertListener) getGreenPostsCatalog() } return document.addEventListener("PostsInserted",insertListener) } } var threads=[] var catalogThreads=threadContainer.children for(var i=0;i{ if(response.status==200){ cacheCatalogPosts=JSON.parse(response.responseText) showGreenPostsCatalog() if(mode=="catalog"){ new MutationObserver(mutations=>{ showGreenPostsCatalog() }).observe(threadContainer,{childList:1}) }else{ document.addEventListener("PostsInserted",showGreenPostsCatalog) } } }, onerror:response=>{ } }) } function showGreenPostsCatalog(){ var countObj=cacheCatalogPosts var oldPosts=queryAll(".greenPostCount") for(var i=0;i.catalog-stats>span") } if(threadMeta){ addCatalogPosts(countObj[thread],threadMeta) } } } function addCatalogPosts(count,threadMeta){ if(count){ var nativeCatalog=0 if(mode=="catalog"){ nativeCatalog=1 } var text=document.createTextNode(" / ") var postCount=element( ["span#span",{ class:"greenPostCount" }, (nativeCatalog&& "G: " ), ["b",count] ] ).span var afterNode=threadMeta.childNodes[nativeCatalog] insertAfter(text,afterNode) insertAfter(postCount,text) } } // Classic post form function showPostFormClassic(hide){ var formSelector="body>form:not(.greenPostForm)" var nameField=query(formSelector+" input[name=name]") var optionsField=query(formSelector+" input[name=email]") var commentField=query(formSelector+" textarea") if(hide){ if(postForm.classic){ if(nameField){ nameField.value=postForm.classic.name.value optionsField.value=postForm.classic.options.value commentField.value=postForm.classic.comment.value lastCommentForm=commentField } removeChild(postForm.classic.form) postForm.classic=0 } return } if(postForm.classic){ return } var username="" if(nameField){ username=nameField.value }else{ var nameMatch=document.cookie.match(/4chan_name=(.*?)(?:;|$)/) if(nameMatch){ username=nameMatch[1] } } postForm.classic=element( ["form#form",{ name:"post", action:serverurl+"post.php", method:"post", enctype:"multipart/form-data", class:"greenPostForm", onsubmit:submitGreenPost }, ["input",{ name:"thread", value:threadId, type:"hidden" }], ["table",{ class:"postForm" }, ["tbody", ["tr", ["td","Name"], ["td",{ class:"nameFieldParent" }, (nameField&& ["button#toggle",{ class:"greenToggle pressed", title:"[s4s] Interface", onclick:event=>{ event.preventDefault() event.stopPropagation() showPostFormClassic(1) } },"!"] ), ["input#name",{ type:"text", name:"username", tabIndex:1, placeholder:"Anonymous", value:username }] ] ], ["tr", ["td","Options"], ["td", ["input#options",{ type:"text", name:"options", tabIndex:2, value:optionsField?optionsField.value:"" }], ["input",{ type:"submit", tabIndex:6, value:"Post" }] ] ], ["tr", ["td","Comment"], ["td", ["textarea#comment",{ name:"text", tabindex:4, cols:48, rows:4, wrap:"soft", value:commentField?commentField.value:"" }] ] ] ] ] ] ) addCommentForm(postForm.classic.comment) var originalForm=query("#postForm") if(originalForm){ originalForm=originalForm.parentNode }else{ originalForm=query("body>.closed+*") if(!originalForm){ originalForm=query("#op") } } insertBefore(postForm.classic.form,originalForm) } // Native extension initialised function onNativeextInit(){ if(mode=="thread"||mode=="index"){ getUpdateLinks() // Native extension quick reply unsafeWindow.QR.showInterface=unsafeWindow.QR.show var newQRshow=thread=>{ var event=new CustomEvent("QRNativeDialogCreation",{ bubbles:true, detail:{thread:thread} }) document.dispatchEvent(event) } if(typeof exportFunction=="function"){ newQRshow=exportFunction(newQRshow,document.defaultView) } unsafeWindow.QR.show=newQRshow } } function onQRCreated(event){ threadId=event.detail.thread try{ unsafeWindow.QR.showInterface(threadId) }catch(e){} // Clean up post form if it was initialised before var oldToggle=query("#quickReply form:not(.greenPostForm) .greenToggle") if(oldToggle){ removeChild(oldToggle) } showPostFormQR(1) var formSelector="#qrForm" var nameField=query(formSelector+" input[name=name]") nameField.value=query("#postForm input[name=name]").value nameField.tabIndex=0 var commentField=query(formSelector+" textarea") addCommentForm(commentField) var toggle=element( ["button#toggle",{ type:"button", class:"greenToggle", title:"[s4s] Interface", onclick:event=>{ event.preventDefault() event.stopPropagation() showPostFormQR() } },"!"] ).toggle var nameParent=nameField.parentNode nameParent.classList.add("nameFieldParent") insertBefore(toggle,nameField) } function showPostFormQR(hide){ var formSelector="#qrForm" var nameField=query(formSelector+" input[name=name]") var optionsField=query(formSelector+" input[name=email]") var commentField=query(formSelector+" textarea") if(hide){ if(postForm.QR){ nameField.value=postForm.QR.name.value optionsField.value=postForm.QR.options.value commentField.value=postForm.QR.comment.value lastCommentForm=commentField removeChild(postForm.QR.form) postForm.QR=0 } return } var qr=query("#quickReply form:not(.greenPostForm)") if(postForm.QR||!qr){ return } postForm.QR=element( ["form#form",{ name:"post", action:serverurl+"post.php", method:"post", enctype:"multipart/form-data", class:"greenPostForm", onsubmit:submitGreenPost }, ["input",{ name:"thread", value:threadId, type:"hidden" }], ["div",{ class:"nameFieldParent" }, ["button",{ type:"button", class:"greenToggle pressed", title:"[s4s] Interface", onclick:event=>{ showPostFormQR(1) } },"!"], ["input#name",{ type:"text", name:"username", class:"field", placeholder:"Anonymous", value:nameField.value }] ], ["div", ["input#options",{ type:"text", name:"options", class:"field", placeholder:"Options", value:optionsField.value }] ], ["div", ["textarea#comment",{ name:"text", class:"field", cols:48, rows:4, wrap:"soft", placeholder:"Comment", value:commentField.value }], ], ["div", ["span",{ class:"greenSubmit", onclick:event=>{ submitGreenPost(event,postForm.QR.form) } },"Post"] ] ] ) addCommentForm(postForm.QR.comment) insertBefore(postForm.QR.form,qr) } // 4chan-X initialised function on4chanXInit(){ if(mode=="index"&&document.documentElement.classList.contains("catalog-mode")){ getGreenPostsCatalog() } } // 4chan-X QR function onQRXCreated(){ getUpdateLinks() var formSelector="#qr form:not(.greenPostForm)" var commentField=query(formSelector+" textarea") addCommentForm(commentField) var toggle=element( ["button#toggle",{ type:"button", class:"greenToggle", title:"[s4s] Interface", onclick:event=>{ event.preventDefault() event.stopPropagation() showPostFormQRX() } },"!"] ).toggle var qrPersona=query("#qr .persona") insertBefore(toggle,qrPersona.firstChild) } function showPostFormQRX(hide){ var formSelector="#qr form:not(.greenPostForm)" var nameField=query(formSelector+" input[name=name]") var optionsField=query(formSelector+" input[name=email]") var commentField=query(formSelector+" textarea") if(hide){ if(postForm.QRX){ nameField.value=postForm.QRX.name.value optionsField.value=postForm.QRX.options.value commentField.value=postForm.QRX.comment.value lastCommentForm=commentField removeChild(postForm.QRX.form) postForm.QRX=0 } return } var qrx=query(formSelector) if(postForm.QRX||!qrx){ return } threadId=query("#qr select[data-name=thread]").value postForm.QRX=element( ["form#form",{ name:"post", action:serverurl+"post.php", method:"post", enctype:"multipart/form-data", class:"greenPostForm", onsubmit:submitGreenPost }, ["input",{ name:"thread", value:threadId, type:"hidden" }], ["div",{ class:"persona" }, ["button",{ type:"button", class:"greenToggle pressed", title:"[s4s] Interface", onclick:event=>{ showPostFormQRX(1) } },"!"], ["input#name",{ name:"username", class:"field", placeholder:"Name", size:1, value:nameField.value }], ["input#options",{ name:"options", class:"field", placeholder:"Options", size:1, value:optionsField.value }] ], ["textarea#comment",{ name:"text", class:"field", placeholder:"Comment", value:commentField.value }], ["div",{ class:"file-n-submit" }, ["input",{ type:"submit", value:"Submit" }] ] ] ) addCommentForm(postForm.QRX.comment) insertBefore(postForm.QRX.form,qrx) } // Track last used comment field for inserting quotes function addCommentForm(commentField,notLast){ if(!notLast){ lastCommentForm=commentField } commentField.addEventListener("focus",event=>{ lastCommentForm=event.currentTarget }) } function insertQuote(event){ var commentField=lastCommentForm if(commentField&&document.contains(commentField)){ event.preventDefault() event.stopPropagation() var isQRX=commentField.closest("#qr") if(isQRX){ isQRX.hidden=0 } var text=">>"+event.currentTarget.firstChild.data+"\n" var caretPos=commentField.selectionStart commentField.value= commentField.value.slice(0,caretPos) +text +commentField.value.slice(commentField.selectionEnd) var range=caretPos+text.length commentField.setSelectionRange(range,range) commentField.focus() } } // Manually update thread with green posts function getUpdateLinks(){ var update=queryAll("[data-cmd=update],.updatelink>a") for(var i=0;i{ getGreenPosts(threadId) }) updateLinks.add(update[i]) } } } // Submit a green post function submitGreenPost(event,form){ event.preventDefault() event.stopPropagation() if(!form){ form=event.currentTarget } var submit={} submit.button=form.querySelector(":scope input[type=submit],:scope .greenSubmit") submit.fakeButton=submit.button.classList.contains("greenSubmit") if(submit.fakeButton){ submit.text=submit.button.firstChild.data submit.button.firstChild.data="..." submit.button.classList.add("greenSubmitDisabled") }else{ submit.text=submit.button.value submit.button.value="..." submit.button.disabled=1 } var data=[] var formData=new FormData(form) for(var nameValue of formData){ data.push( nameValue[0]+"=" +encodeURIComponent(nameValue[1].replace(/\r?\n/g,"\r")) ) } data=data.join("&") GM.xmlHttpRequest({ method:"post", headers:{ "Content-type":"application/x-www-form-urlencoded" }, url:serverurl+"post.php", data:data, onload:response=>{ if(response.status==200){ if(/Post Successful/.test(response.responseText)){ form.getElementsByTagName("textarea")[0].value="" if(mode=="thread"){ getGreenPosts(threadId) }else{ alert("Post successful") } }else{ return postSubmitted(submit,response.status,response.responseText) } } postSubmitted(submit,response.status) }, onerror:response=>{ postSubmitted(submit) } }) } function postSubmitted(submit,errorCode,responseText){ if(submit.fakeButton){ submit.button.firstChild.data=submit.text submit.button.classList.remove("greenSubmitDisabled") }else{ submit.button.value=submit.text submit.button.disabled=0 } if(errorCode==200){ if(responseText){ alert("Could not submit post ("+responseText+")") } }else{ var alertText="Could not connect to the [s4s] interface" if(errorCode){ alertText+=" ("+errorCode+")" } alert(alertText) } } //updates native 4chan posts with whatever, atm it's only fixing links function updatePosts() { var posts=document.querySelectorAll('.postMessage:not(.interfaced)'); for(var i=0;itbody>tr:not(.rules), #quickReply .greenPostForm+form, #qr .greenPostForm+form, #qr:not(.reply-to-thread) .greenToggle:not(.pressed){ display:none!important; } .greenPostForm .file-n-submit{ display:flex; align-items:stretch; justify-content:flex-end; height:25px; margin-top:1px; } .greenPostForm .file-n-submit input{ width:25%; background:linear-gradient(to bottom,#f8f8f8,#dcdcdc) no-repeat; border:1px solid #bbb; border-radius:2px; height:100%; } .greenPostContainer .post.reply{ background-color:#dfd!important; border:2px solid #008000!important; } .greenPostContainer .postMessage{ color:#000!important; } .greenToggle{ font-family:monospace; font-size:16px; line-height:17px; background:#ceb!important; width:24px; padding:0; border:1px solid #bbb; } .greenPostForm input:not([type=submit]), .greenPostForm textarea{ background-color:#dfd; color:#000; } .greenToggle.pressed{ background:#6d6!important; font-weight:bold; color:#fff; } .postForm .greenToggle+input{ width:220px!important; } .postForm .nameFieldParent, #quickReply .nameFieldParent{ display:flex; flex-direction:row; } .postForm textarea{ width:292px; } #quickReply .greenToggle{ width:23px; height:23px; } #quickReply .greenToggle+input{ width:273px!important; } .greenSubmit{ display:inline-block; width:75px; float:right; padding:1px 6px; text-align:center; border:1px solid #adadad; background-color:#e1e1e1; box-sizing:border-box; user-select:none; font:400 13.3333px Arial,sans-serif; font:-moz-button; color:#000; cursor:default; } .greenSubmit:hover{ border-color:#0078d7; background-color:#e5f1fb; } .greenSubmit:active{ border-color:#005499; background-color:#cce4f7; } .greenSubmitDisabled{ color:#808080; pointer-events:none; } .greenPostCount{ color:#060; } .greenPostContainer .hide-reply-button{ opacity:0!important; pointer-events:none; } @media only screen and (max-width:480px){ .postForm .greenToggle+input{ width:196px!important; } .postForm input[type="submit"]{ width:60px; padding:2px 4px 3px; margin:0; } .postForm:not(.hideMobile){ margin-top:20px; } } `.replace(/\n\s*/g,"") element( document.head||document.documentElement, ["style",{ id:"s4sinterface-css" },stylesheet] ) function padding(string,num){ return (""+string).padStart(num,0) } function query(selector){ return document.querySelector(selector) } function queryAll(selector){ return document.querySelectorAll(selector) } function insertBefore(newElement,targetElement){ return targetElement.parentNode.insertBefore(newElement,targetElement) } function insertAfter(newElement,targetElement){ var nextSibling=targetElement.nextSibling if(nextSibling){ return insertBefore(newElement,nextSibling) }else{ return targetElement.parentNode.appendChild(newElement) } } function removeChild(targetElement){ return targetElement.parentNode.removeChild(targetElement) } function element(){ var parent var lasttag var createdtag var toreturn={} for(var i=0;i