您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Restores edits and deletes on Reddit
当前为
// ==UserScript== // @name RedditRestore // @namespace https://www.reddit.dynu.net // @version 0.9 // @description Restores edits and deletes on Reddit // @author /u/PortugalCache // @match https://*.reddit.com/r/*/comments/*/*/* // @exclude https://*.reddit.com/r/*/comments/*/*/*/ // @grant none // @require https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js // ==/UserScript== (function() { 'use strict'; var postId = location.pathname.match(/comments\/(.+?)\//i)[1]; var postCacheUrl = 'https://www.reddit.dynu.net'+location.pathname; // time ago var timeSince = function(date) { var format = function (intervalType) { return interval + ' ' + intervalType + (interval > 1 || interval === 0 ? 's' : '') + ' ago'; }; var seconds = Math.floor((new Date() - date) / 1000); var interval = Math.floor(seconds / 31536000); if (interval >= 1) return format('year'); if ((interval = Math.floor(seconds / 2592000)) >= 1) return format('month'); if ((interval = Math.floor(seconds / 86400)) >= 1) return format('day'); if ((interval = Math.floor(seconds / 3600)) >= 1) return format('hour'); if ((interval = Math.floor(seconds / 60)) >= 1) return format('minute'); interval = seconds; return format('second'); }; // time html var timeHtml = function (timestamp) { var date = new Date(timestamp*1000); return `<time title="${date.toString()}" datetime="${date.toISOString()}" class="">${timeSince(date)}</time>`; }; // compare html strings var compareHtml = function(html1, html2) { var el1 = document.createElement('span'); el1.innerHTML = html1; var el2 = document.createElement('span'); el2.innerHTML = html2; return el1.innerHTML == el2.innerHTML; }; // comment text html var editsHtml = function (plainTexts, original) { var html = '', text = '', lastHtml, edits = Object.keys(plainTexts), onlyIns = true, onlyDel = true, texts = {}; for (var i in plainTexts) texts[i] = marked(plainTexts[i]); if (original && !compareHtml(original.replace(/\n/g, ''), texts[edits[edits.length-1]].replace(/\n/g, ''))) { var i = Math.floor(Date.now()/1000); texts[i] = original; edits.push(i); } //if (edits.length == 1) return texts[edits[0]]; for (var i in texts) { if (edits.length != 1) html += '<p class="tagline" style="font-size: 11px; font-weight: bold;">'+(Number(i) ? 'Edited ' + timeHtml(i) : 'Original')+':</p>'; html += lastHtml = text ? htmlDiff(text, text = texts[i]) : text = texts[i]; if (/<span class="del">/.test(lastHtml)) onlyIns = false; if (/<span class="ins">/.test(lastHtml)) onlyDel = false; } if (onlyIns || onlyDel) return htmlDiff(texts[edits[0]], text); return text + `<div><a href="javascript:" onclick="this.parentNode.childNodes[1].style.display = (this.parentNode.childNodes[1].style.display ? '' : 'none');">Show ${edits.length-1} edits</a><div style="display: none;">${html}</div></div>`; }; // author html var authorHtml = function (author, del) { return `<a href="https://www.reddit.${del ? "dynu.net" : "com"}/user/${author}" class="author${del ? " del" : ""} may-blank id-t2_ydhqk">${author}</a>`; }; // comment html var commentHtml = function (id, comment) { return ` <div class="midcol unvoted" style="visibility: hidden;"> <div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0"></div> <div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0"></div> </div> <div class="entry unvoted"> <p class="tagline"> <a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a> ${authorHtml(comment.author)} <span class="userattrs"></span> <span class="score dislikes"></span> <span class="score unvoted"></span> <span class="score likes"></span> ${timeHtml(comment.created)} <span class="del"> DELETED </span> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(- childs)</a> </p> <form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_${id}6t3"><input type="hidden" name="thing_id" value="t1_${id}"> <div class="usertext-body may-blank-within md-container "> <div class="md">${editsHtml(comment.text)}</div> </div> </form> <ul class="flat-list buttons"> <li class="first"><a href="${postCacheUrl+id}/" target="_blank" data-event-action="permalink" class="bylink" rel="nofollow">cachelink</a></li> <li class="reply-button"><a class="access-required" href="javascript:void(0)">reply</a></li> </ul> <div class="reportform report-t1_${id}"></div> </div>`; }; // add cachelink button var addCacheLinkButton = function (id) { var buttons = document.querySelector(id ? '#thing_'+id+' .buttons' : '.sitetable .thing .buttons'); if (buttons) { var button = document.createElement('li'); button.innerHTML = '<a href="'+postCacheUrl+(/^t1_/.test(id) ? id.replace(/^t1_/, '') + '/' : '')+'" target="_blank">cachelink</a>'; buttons.insertBefore(button, buttons.childNodes[1]); } else console.log('Could not add cachelink button to ' + id); }; var addReplyButton = function (el, comment) { el.querySelector('.reply-button').addEventListener('click', function (e) { var parent = e.target.parentNode.parentNode.parentNode.parentNode, replyButton; do { parent = parent.parentNode; replyButton = parent.querySelector(':scope > .entry > .buttons .reply-button'); console.log(parent, replyButton); } while (parent.className != 'commentarea' && !replyButton); if (replyButton) replyButton.firstChild.click(); var text; for (var i in comment.text) text = comment.text[i]; text = text.split("\n"); var reply = []; for (var i = 0; i < text.length; i++) if (!/^ *>|^ *$/.test(text[i])) reply.push(text[i]); reply = '> /u/' + comment.author + ' ' + timeSince(new Date(comment.created * 1000)) + ':\n\n>' + reply.join("\n\n >"); var textarea = parent.querySelector('textarea'); textarea.value = reply + "\n\n"; textarea.focus(); }); }; var createThing = function (name, comment) { var div = document.createElement('div'); div.setAttribute('id', 'thing_t1_' + name); div.setAttribute('class', "thing comment noncollapsed"); div.innerHTML = commentHtml(name, comment) + `<div class="child"></div><div class="clearleft"></div>`; addReplyButton(div, comment); return div; }; // add cachelink button to post addCacheLinkButton(); // add styles for the green and red background var style = document.createElement('style'); style.textContent = ".ins, .ins * { background-color: #dfffd1!important; }\n.del, .del * { background-color: #faa!important; }"; document.head.appendChild(style); // retrieve ids of visible comments var visibleComments = []; var editedComments = []; var els = document.getElementsByClassName('parent'); //this way it doesn't match 'morechildren' comments for (var i = 0; i < els.length; i++) { var el = els[i].firstChild; if (el) { var name = el.getAttribute('name'); if (document.querySelector('#thing_t1_'+name+' > div > p > .edited-timestamp')) { editedComments.push(name); } else visibleComments.push(name); } } var moreCommentsLoadedTimer = {}; var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (!mutation.target.querySelector(':scope > .morechildren')) { var id = mutation.target.parentNode.parentNode.id; if (moreCommentsLoadedTimer[id]) clearTimeout(moreCommentsLoadedTimer[id]); moreCommentsLoadedTimer[id] = setTimeout(function () { console.log("more comments for:", id); }, 100); } }); }); // configuration of the observer: var config = {attributes: true, childList: true, characterData: true, attributeOldValue: true}; // retrieve ids of morechildren comments var hiddenComments = []; var els = document.querySelectorAll('.morechildren, .morerecursion'); for (var i = 0; i < els.length; i++) { observer.observe(els[i].parentNode, config); var id = els[i].parentNode.parentNode.parentNode.id; if (id) hiddenComments.push(id.replace(/^thing_t1_/, '')); } // fixme: will repeat ids already in visible comments // retrieve ids of orphan comments var orphanComments = []; var orphansParents = {}; var deletedComments = document.getElementsByClassName('deleted'); var getFirstOrphanComment = function (deletedComment) { return deletedComment.querySelector('.child > div > .comment:not(.deleted)'); }; for (var i = 0; i < deletedComments.length; i++) { var child = getFirstOrphanComment(deletedComments[i]); if (child) { var orphan = child.id.replace(/^thing_t1_/, ''); orphanComments.push(orphan); orphansParents[orphan] = deletedComments[i]; } } // retrieve ids of comments from deleted users //var it = function (xpath) { var r = [], n = null; while (n = xpath.iterateNext()) r.push(n); return r; }; var deletedUsersComments = [], deletedUserPost = false; var els = document.evaluate('//span[@class="author" and text()="[deleted]"]/../../..', document, null, XPathResult.ANY_TYPE, null), el; while ((el = els.iterateNext())) { if (/^thing_t1_/.test(el.id)) deletedUsersComments.push(el.id.replace(/^thing_t1_/, '')); else deletedUserPost = true; } var postThing = document.querySelector('.sitetable .thing'); var postMdContainer = document.querySelector('.sitetable .thing .md-container'); // is removed post var removedPost = false; var postTagline = document.querySelector('.sitetable .tagline'); if (!postMdContainer.querySelector('.md')) { console.log("The post was removed."); //postMdContainer.innerHTML = `<div class="md"><p>[removed]</p></div>`; removedPost = true; postTagline.innerHTML += ` <span class="del"> REMOVED </span>`; } // is deleted post var deletedPost = document.querySelector('.sitetable').querySelector('.thing.deleted') ? true : false; if (deletedPost) { console.log("The post was deleted."); document.querySelector('.sitetable .thing').setAttribute("id", 'thing_t3_' + postId); postTagline.style.display = 'initial'; postTagline.innerHTML += ` <span class="del"> DELETED </span>`; } // handle response var responseHandler = function (responseText) { var result = JSON.parse(responseText); console.log(result); for (var id in result.edits) { // add edits var md = document.querySelector('#thing_'+id+' .md'); if (md) { md.innerHTML = editsHtml(result.edits[id], md.innerHTML); addCacheLinkButton(id); } else console.log('could not add edits to ' + id); } // restore deleted authors if (result.authors) { for (var id in result.authors) { var span = document.querySelector('#thing_'+(/^t3_/.test(id) ? '' : 't1_')+id+' .author'); if (span) { var author = result.authors[id]; span.innerHTML = authorHtml(author, true); //console.log('restored author for ' + id); } else console.log('could not add author to ' + id); } } // restore parent of orphan comments if (result.parents) { for (let id in result.parents) { var comment = result.parents[id]; var parent = orphansParents[id]; if (parent) { console.log(parent); parent.classList.remove('deleted'); parent.removeChild(parent.firstChild); parent.removeChild(parent.firstChild); parent.removeChild(parent.firstChild); parent.innerHTML = commentHtml(comment.id, comment) + parent.innerHTML; addReplyButton(parent, comment); console.log('restored parent ' + comment.id + ' of orphan comment ' + id); } else console.log("Unknown orphan: " + id); } } // restore remaining deleted comments if (result.dels) { var siteTable = document.getElementById('siteTable_t3_'+postId, true); var keys = Object.keys(result.dels).sort(function(a,b) { return a > b ? 1 : -1;}); // we want to add comments by chronological order for (var i = 0; i < keys.length; i++) { var name = keys[i]; var comment = result.dels[name]; var parentEl; if (comment.parent_id) { parentEl = document.querySelector('#thing_t1_'+comment.parent_id+' .child'); var childSiteTable = parentEl.querySelector('.sitetable'); // if there are no other replies we need to create the sitetable if (!childSiteTable) { childSiteTable = document.createElement("div"); childSiteTable.setAttribute("id", "siteTable_t1_" + comment.parent_id); childSiteTable.setAttribute("class", "sitetable listing"); parentEl.appendChild(childSiteTable); } parentEl = childSiteTable; } else parentEl = document.getElementById("siteTable_t3_" + postId); if (parentEl) { parentEl.appendChild(createThing(name, comment)); } else console.log('add comment ' + name + ' to ' + comment.parent_id + ': ERROR'); } } // restore deleted post if (result.post) { if (result.post.text) postMdContainer.innerHTML = `<div class="md">${editsHtml(result.post.text)}</div>`; if (result.post.author) { postThing.querySelector('.author').innerHTML = authorHtml(result.post.author, deletedUserPost && !deletedPost); // set as post subbmiter in comments var authors = document.querySelectorAll('.commentarea a.author'); for (let i in authors) { if (authors[i].innerHTML == result.post.author) authors[i].classList.add("submitter"); } } } }; // the request to the cache site var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200 && this.responseText) { responseHandler(this.responseText); } }; xhttp.open("POST", "https://www.reddit.dynu.net/?gm=0.8&p="+postId, true); xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhttp.send( function (post) { var r = []; for (var i in post.arrays) if (post.arrays[i].length) r.push(i + '=' + post.arrays[i].join(',')); for (var i in post.booleans) if (post.booleans[i]) r.push(i + '='); return r.join('&'); }({ arrays: {c: visibleComments, o: orphanComments, uc: deletedUsersComments, e: editedComments, h: hiddenComments}, booleans: {up: deletedUserPost, rp: removedPost, dp: deletedPost} }) ); })(); // compute diff between two strings this.diff = function(oldStr, newStr) { var array_keys = function (a, s) { var r = []; if (!a) return r; for (var i = 0; i < a.length; i++) { if (a[i] == s) r.push(i); } return r; }; var maxlen = 0, omax = 0, nmax = 0, matrix = []; for (var oindex = 0; oindex < oldStr.length; oindex++) { var ovalue = oldStr[oindex]; var nkeys = array_keys(newStr, ovalue); for (var i = 0; i < nkeys.length; i++) { var nindex = nkeys[i]; if (!matrix[oindex]) matrix[oindex] = []; matrix[oindex][nindex] = (matrix[oindex - 1] && matrix[oindex - 1][nindex - 1]) ? matrix[oindex - 1][nindex - 1] + 1 : 1; if (matrix[oindex][nindex] > maxlen) { maxlen = matrix[oindex][nindex]; omax = oindex + 1 - maxlen; nmax = nindex + 1 - maxlen; } } } if (maxlen === 0) return [{d: oldStr, i: newStr}]; return diff(oldStr.slice(0, omax), newStr.slice(0, nmax)).concat( newStr.slice(nmax, nmax + maxlen)).concat( diff(oldStr.slice(omax + maxlen), newStr.slice(nmax + maxlen))); }; // pretty print the diff this.htmlDiff = function(oldStr, newStr) { var explode = function (str) { return str.trim().match(/<.+?>|[a-záéíóúâêîôûãõç0-9]+[ \n]*|[^a-záéíóúâêîôûãõç0-9]/ig); }; var diffResult = diff(explode(oldStr), explode(newStr)); var ret = ''; for (var i = 0; i < diffResult.length; i++) { var k = diffResult[i]; if (k instanceof Object) { ret += (k.d && k.d.length ? '<span class="del">'+k.d.join('')+"</span>" : '') + (k.i && k.i.length ? '<span class="ins">'+k.i.join('')+"</span>" : ''); } else ret += k; } return ret; };