RedditRestore

Restores edits and deletes on Reddit

当前为 2017-06-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name RedditRestore
  3. // @namespace https://www.reddit.dynu.net
  4. // @version 0.9
  5. // @description Restores edits and deletes on Reddit
  6. // @author /u/PortugalCache
  7. // @match https://*.reddit.com/r/*/comments/*/*/*
  8. // @exclude https://*.reddit.com/r/*/comments/*/*/*/
  9. // @grant none
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.6/marked.min.js
  11. // ==/UserScript==
  12.  
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. var postId = location.pathname.match(/comments\/(.+?)\//i)[1];
  18. var postCacheUrl = 'https://www.reddit.dynu.net'+location.pathname;
  19.  
  20. // time ago
  21. var timeSince = function(date) {
  22. var format = function (intervalType) { return interval + ' ' + intervalType + (interval > 1 || interval === 0 ? 's' : '') + ' ago'; };
  23. var seconds = Math.floor((new Date() - date) / 1000);
  24. var interval = Math.floor(seconds / 31536000);
  25. if (interval >= 1) return format('year');
  26. if ((interval = Math.floor(seconds / 2592000)) >= 1) return format('month');
  27. if ((interval = Math.floor(seconds / 86400)) >= 1) return format('day');
  28. if ((interval = Math.floor(seconds / 3600)) >= 1) return format('hour');
  29. if ((interval = Math.floor(seconds / 60)) >= 1) return format('minute');
  30. interval = seconds;
  31. return format('second');
  32. };
  33.  
  34. // time html
  35. var timeHtml = function (timestamp) {
  36. var date = new Date(timestamp*1000);
  37. return `<time title="${date.toString()}" datetime="${date.toISOString()}" class="">${timeSince(date)}</time>`;
  38. };
  39.  
  40. // compare html strings
  41. var compareHtml = function(html1, html2) {
  42. var el1 = document.createElement('span'); el1.innerHTML = html1;
  43. var el2 = document.createElement('span'); el2.innerHTML = html2;
  44. return el1.innerHTML == el2.innerHTML;
  45. };
  46.  
  47. // comment text html
  48. var editsHtml = function (plainTexts, original) {
  49. var html = '', text = '', lastHtml, edits = Object.keys(plainTexts), onlyIns = true, onlyDel = true, texts = {};
  50. for (var i in plainTexts) texts[i] = marked(plainTexts[i]);
  51.  
  52. if (original && !compareHtml(original.replace(/\n/g, ''), texts[edits[edits.length-1]].replace(/\n/g, ''))) {
  53. var i = Math.floor(Date.now()/1000);
  54. texts[i] = original;
  55. edits.push(i);
  56. }
  57. //if (edits.length == 1) return texts[edits[0]];
  58. for (var i in texts) {
  59. if (edits.length != 1) html += '<p class="tagline" style="font-size: 11px; font-weight: bold;">'+(Number(i) ? 'Edited ' + timeHtml(i) : 'Original')+':</p>';
  60. html += lastHtml = text ? htmlDiff(text, text = texts[i]) : text = texts[i];
  61. if (/<span class="del">/.test(lastHtml)) onlyIns = false;
  62. if (/<span class="ins">/.test(lastHtml)) onlyDel = false;
  63. }
  64. if (onlyIns || onlyDel) return htmlDiff(texts[edits[0]], text);
  65. 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>`;
  66. };
  67.  
  68. // author html
  69. var authorHtml = function (author, del) {
  70. return `<a href="https://www.reddit.${del ? "dynu.net" : "com"}/user/${author}" class="author${del ? " del" : ""} may-blank id-t2_ydhqk">${author}</a>`;
  71. };
  72.  
  73. // comment html
  74. var commentHtml = function (id, comment) {
  75. return `
  76. <div class="midcol unvoted" style="visibility: hidden;">
  77. <div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0"></div>
  78. <div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0"></div>
  79. </div>
  80. <div class="entry unvoted">
  81. <p class="tagline">
  82. <a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a>
  83. ${authorHtml(comment.author)}
  84. <span class="userattrs"></span>
  85. <span class="score dislikes"></span>
  86. <span class="score unvoted"></span>
  87. <span class="score likes"></span>
  88. ${timeHtml(comment.created)}
  89. <span class="del">&nbsp;DELETED&nbsp;</span>
  90. &nbsp;
  91. <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(- childs)</a>
  92. </p>
  93. <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}">
  94. <div class="usertext-body may-blank-within md-container ">
  95. <div class="md">${editsHtml(comment.text)}</div>
  96. </div>
  97. </form>
  98. <ul class="flat-list buttons">
  99. <li class="first"><a href="${postCacheUrl+id}/" target="_blank" data-event-action="permalink" class="bylink" rel="nofollow">cachelink</a></li>
  100. <li class="reply-button"><a class="access-required" href="javascript:void(0)">reply</a></li>
  101. </ul>
  102. <div class="reportform report-t1_${id}"></div>
  103. </div>`;
  104. };
  105.  
  106. // add cachelink button
  107. var addCacheLinkButton = function (id) {
  108. var buttons = document.querySelector(id ? '#thing_'+id+' .buttons' : '.sitetable .thing .buttons');
  109. if (buttons) {
  110. var button = document.createElement('li');
  111. button.innerHTML = '<a href="'+postCacheUrl+(/^t1_/.test(id) ? id.replace(/^t1_/, '') + '/' : '')+'" target="_blank">cachelink</a>';
  112. buttons.insertBefore(button, buttons.childNodes[1]);
  113. }
  114. else console.log('Could not add cachelink button to ' + id);
  115. };
  116.  
  117. var addReplyButton = function (el, comment) {
  118. el.querySelector('.reply-button').addEventListener('click', function (e) {
  119. var parent = e.target.parentNode.parentNode.parentNode.parentNode, replyButton;
  120. do {
  121. parent = parent.parentNode;
  122. replyButton = parent.querySelector(':scope > .entry > .buttons .reply-button');
  123. console.log(parent, replyButton);
  124. } while (parent.className != 'commentarea' && !replyButton);
  125. if (replyButton) replyButton.firstChild.click();
  126.  
  127. var text; for (var i in comment.text) text = comment.text[i];
  128. text = text.split("\n");
  129. var reply = [];
  130. for (var i = 0; i < text.length; i++) if (!/^ *>|^ *$/.test(text[i])) reply.push(text[i]);
  131. reply = '> /u/' + comment.author + ' ' + timeSince(new Date(comment.created * 1000)) + ':\n\n>' + reply.join("\n\n >");
  132.  
  133. var textarea = parent.querySelector('textarea');
  134. textarea.value = reply + "\n\n";
  135. textarea.focus();
  136. });
  137. };
  138.  
  139. var createThing = function (name, comment) {
  140. var div = document.createElement('div');
  141. div.setAttribute('id', 'thing_t1_' + name);
  142. div.setAttribute('class', "thing comment noncollapsed");
  143. div.innerHTML = commentHtml(name, comment) + `<div class="child"></div><div class="clearleft"></div>`;
  144. addReplyButton(div, comment);
  145. return div;
  146. };
  147.  
  148. // add cachelink button to post
  149. addCacheLinkButton();
  150.  
  151. // add styles for the green and red background
  152. var style = document.createElement('style');
  153. style.textContent = ".ins, .ins * { background-color: #dfffd1!important; }\n.del, .del * { background-color: #faa!important; }";
  154. document.head.appendChild(style);
  155.  
  156. // retrieve ids of visible comments
  157. var visibleComments = [];
  158. var editedComments = [];
  159. var els = document.getElementsByClassName('parent'); //this way it doesn't match 'morechildren' comments
  160. for (var i = 0; i < els.length; i++) {
  161. var el = els[i].firstChild;
  162. if (el) {
  163. var name = el.getAttribute('name');
  164. if (document.querySelector('#thing_t1_'+name+' > div > p > .edited-timestamp')) {
  165. editedComments.push(name);
  166. }
  167. else visibleComments.push(name);
  168. }
  169. }
  170.  
  171. var moreCommentsLoadedTimer = {};
  172. var observer = new MutationObserver(function(mutations) {
  173. mutations.forEach(function(mutation) {
  174. if (!mutation.target.querySelector(':scope > .morechildren')) {
  175. var id = mutation.target.parentNode.parentNode.id;
  176. if (moreCommentsLoadedTimer[id]) clearTimeout(moreCommentsLoadedTimer[id]);
  177. moreCommentsLoadedTimer[id] = setTimeout(function () {
  178. console.log("more comments for:", id);
  179. }, 100);
  180. }
  181. });
  182. });
  183. // configuration of the observer:
  184. var config = {attributes: true, childList: true, characterData: true, attributeOldValue: true};
  185.  
  186. // retrieve ids of morechildren comments
  187. var hiddenComments = [];
  188. var els = document.querySelectorAll('.morechildren, .morerecursion');
  189. for (var i = 0; i < els.length; i++) {
  190. observer.observe(els[i].parentNode, config);
  191. var id = els[i].parentNode.parentNode.parentNode.id;
  192. if (id) hiddenComments.push(id.replace(/^thing_t1_/, ''));
  193. }
  194.  
  195. // fixme: will repeat ids already in visible comments
  196. // retrieve ids of orphan comments
  197. var orphanComments = [];
  198. var orphansParents = {};
  199. var deletedComments = document.getElementsByClassName('deleted');
  200. var getFirstOrphanComment = function (deletedComment) {
  201. return deletedComment.querySelector('.child > div > .comment:not(.deleted)');
  202. };
  203. for (var i = 0; i < deletedComments.length; i++) {
  204. var child = getFirstOrphanComment(deletedComments[i]);
  205. if (child) {
  206. var orphan = child.id.replace(/^thing_t1_/, '');
  207. orphanComments.push(orphan);
  208. orphansParents[orphan] = deletedComments[i];
  209. }
  210. }
  211.  
  212. // retrieve ids of comments from deleted users
  213. //var it = function (xpath) { var r = [], n = null; while (n = xpath.iterateNext()) r.push(n); return r; };
  214. var deletedUsersComments = [], deletedUserPost = false;
  215. var els = document.evaluate('//span[@class="author" and text()="[deleted]"]/../../..', document, null, XPathResult.ANY_TYPE, null), el;
  216. while ((el = els.iterateNext())) {
  217. if (/^thing_t1_/.test(el.id)) deletedUsersComments.push(el.id.replace(/^thing_t1_/, ''));
  218. else deletedUserPost = true;
  219. }
  220.  
  221. var postThing = document.querySelector('.sitetable .thing');
  222. var postMdContainer = document.querySelector('.sitetable .thing .md-container');
  223.  
  224. // is removed post
  225. var removedPost = false;
  226. var postTagline = document.querySelector('.sitetable .tagline');
  227. if (!postMdContainer.querySelector('.md')) {
  228. console.log("The post was removed.");
  229. //postMdContainer.innerHTML = `<div class="md"><p>[removed]</p></div>`;
  230. removedPost = true;
  231. postTagline.innerHTML += ` <span class="del">&nbsp;REMOVED&nbsp;</span>`; }
  232.  
  233. // is deleted post
  234. var deletedPost = document.querySelector('.sitetable').querySelector('.thing.deleted') ? true : false;
  235. if (deletedPost) {
  236. console.log("The post was deleted.");
  237. document.querySelector('.sitetable .thing').setAttribute("id", 'thing_t3_' + postId);
  238. postTagline.style.display = 'initial';
  239. postTagline.innerHTML += ` <span class="del">&nbsp;DELETED&nbsp;</span>`;
  240. }
  241.  
  242. // handle response
  243. var responseHandler = function (responseText) {
  244. var result = JSON.parse(responseText);
  245. console.log(result);
  246.  
  247. for (var id in result.edits) {
  248. // add edits
  249. var md = document.querySelector('#thing_'+id+' .md');
  250. if (md) {
  251. md.innerHTML = editsHtml(result.edits[id], md.innerHTML);
  252. addCacheLinkButton(id);
  253. }
  254. else console.log('could not add edits to ' + id);
  255. }
  256.  
  257. // restore deleted authors
  258. if (result.authors) {
  259. for (var id in result.authors) {
  260. var span = document.querySelector('#thing_'+(/^t3_/.test(id) ? '' : 't1_')+id+' .author');
  261. if (span) {
  262. var author = result.authors[id];
  263. span.innerHTML = authorHtml(author, true);
  264. //console.log('restored author for ' + id);
  265. }
  266. else console.log('could not add author to ' + id);
  267. }
  268. }
  269.  
  270. // restore parent of orphan comments
  271. if (result.parents) {
  272. for (let id in result.parents) {
  273. var comment = result.parents[id];
  274. var parent = orphansParents[id];
  275. if (parent) {
  276. console.log(parent);
  277. parent.classList.remove('deleted');
  278. parent.removeChild(parent.firstChild);
  279. parent.removeChild(parent.firstChild);
  280. parent.removeChild(parent.firstChild);
  281. parent.innerHTML = commentHtml(comment.id, comment) + parent.innerHTML;
  282. addReplyButton(parent, comment);
  283. console.log('restored parent ' + comment.id + ' of orphan comment ' + id);
  284. }
  285. else console.log("Unknown orphan: " + id);
  286. }
  287. }
  288.  
  289. // restore remaining deleted comments
  290. if (result.dels) {
  291. var siteTable = document.getElementById('siteTable_t3_'+postId, true);
  292. var keys = Object.keys(result.dels).sort(function(a,b) { return a > b ? 1 : -1;}); // we want to add comments by chronological order
  293. for (var i = 0; i < keys.length; i++) {
  294. var name = keys[i];
  295. var comment = result.dels[name];
  296. var parentEl;
  297. if (comment.parent_id) {
  298. parentEl = document.querySelector('#thing_t1_'+comment.parent_id+' .child');
  299. var childSiteTable = parentEl.querySelector('.sitetable');
  300. // if there are no other replies we need to create the sitetable
  301. if (!childSiteTable) {
  302. childSiteTable = document.createElement("div");
  303. childSiteTable.setAttribute("id", "siteTable_t1_" + comment.parent_id);
  304. childSiteTable.setAttribute("class", "sitetable listing");
  305. parentEl.appendChild(childSiteTable);
  306. }
  307. parentEl = childSiteTable;
  308. }
  309. else parentEl = document.getElementById("siteTable_t3_" + postId);
  310. if (parentEl) {
  311. parentEl.appendChild(createThing(name, comment));
  312. }
  313. else console.log('add comment ' + name + ' to ' + comment.parent_id + ': ERROR');
  314. }
  315. }
  316.  
  317. // restore deleted post
  318. if (result.post) {
  319. if (result.post.text) postMdContainer.innerHTML = `<div class="md">${editsHtml(result.post.text)}</div>`;
  320. if (result.post.author) {
  321. postThing.querySelector('.author').innerHTML = authorHtml(result.post.author, deletedUserPost && !deletedPost);
  322. // set as post subbmiter in comments
  323. var authors = document.querySelectorAll('.commentarea a.author');
  324. for (let i in authors) {
  325. if (authors[i].innerHTML == result.post.author) authors[i].classList.add("submitter");
  326. }
  327. }
  328. }
  329.  
  330. };
  331.  
  332. // the request to the cache site
  333. var xhttp = new XMLHttpRequest();
  334. xhttp.onreadystatechange = function() {
  335. if (this.readyState == 4 && this.status == 200 && this.responseText) {
  336. responseHandler(this.responseText);
  337. }
  338. };
  339. xhttp.open("POST", "https://www.reddit.dynu.net/?gm=0.8&p="+postId, true);
  340. xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  341. xhttp.send(
  342. function (post) {
  343. var r = [];
  344. for (var i in post.arrays) if (post.arrays[i].length) r.push(i + '=' + post.arrays[i].join(','));
  345. for (var i in post.booleans) if (post.booleans[i]) r.push(i + '=');
  346. return r.join('&');
  347. }({
  348. arrays: {c: visibleComments, o: orphanComments, uc: deletedUsersComments, e: editedComments, h: hiddenComments},
  349. booleans: {up: deletedUserPost, rp: removedPost, dp: deletedPost}
  350. })
  351. );
  352. })();
  353.  
  354. // compute diff between two strings
  355. this.diff = function(oldStr, newStr) {
  356. var array_keys = function (a, s) {
  357. var r = [];
  358. if (!a) return r;
  359. for (var i = 0; i < a.length; i++) {
  360. if (a[i] == s) r.push(i);
  361. }
  362. return r;
  363. };
  364. var maxlen = 0, omax = 0, nmax = 0, matrix = [];
  365. for (var oindex = 0; oindex < oldStr.length; oindex++) {
  366. var ovalue = oldStr[oindex];
  367. var nkeys = array_keys(newStr, ovalue);
  368. for (var i = 0; i < nkeys.length; i++) {
  369. var nindex = nkeys[i];
  370. if (!matrix[oindex]) matrix[oindex] = [];
  371. matrix[oindex][nindex] = (matrix[oindex - 1] && matrix[oindex - 1][nindex - 1]) ?
  372. matrix[oindex - 1][nindex - 1] + 1 : 1;
  373. if (matrix[oindex][nindex] > maxlen) {
  374. maxlen = matrix[oindex][nindex];
  375. omax = oindex + 1 - maxlen;
  376. nmax = nindex + 1 - maxlen;
  377. }
  378. }
  379. }
  380. if (maxlen === 0) return [{d: oldStr, i: newStr}];
  381. return diff(oldStr.slice(0, omax), newStr.slice(0, nmax)).concat(
  382. newStr.slice(nmax, nmax + maxlen)).concat(
  383. diff(oldStr.slice(omax + maxlen), newStr.slice(nmax + maxlen)));
  384. };
  385.  
  386. // pretty print the diff
  387. this.htmlDiff = function(oldStr, newStr) {
  388. var explode = function (str) { return str.trim().match(/<.+?>|[a-záéíóúâêîôûãõç0-9]+[ \n]*|[^a-záéíóúâêîôûãõç0-9]/ig); };
  389. var diffResult = diff(explode(oldStr), explode(newStr));
  390. var ret = '';
  391. for (var i = 0; i < diffResult.length; i++) {
  392. var k = diffResult[i];
  393. if (k instanceof Object) {
  394. ret += (k.d && k.d.length ? '<span class="del">'+k.d.join('')+"</span>" : '') +
  395. (k.i && k.i.length ? '<span class="ins">'+k.i.join('')+"</span>" : '');
  396. }
  397. else ret += k;
  398. }
  399. return ret;
  400. };