RedditRestore

Restores edits and deletes on Reddit

目前為 2017-06-05 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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">&nbsp;DELETED&nbsp;</span>
&nbsp;
<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">&nbsp;REMOVED&nbsp;</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">&nbsp;DELETED&nbsp;</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;
};