YARCO.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. // ==UserScript==
  2. // @name Yet Another Reddit Comment Overwriter
  3. // @namespace https://github.com/adriantache/YARCO/
  4. // @description Local script to overwrite all your comments with random ASCII characters and delete them. This works because Reddit doesn't store editing history, so technically this is the only way to obfuscate the contents of the comments. Based on Reddit Overwrite script v.1.4.8.
  5. // @include https://*.reddit.com/user/*/comments/
  6. // @include http://*.reddit.com/user/*/comments/
  7. // @version 0.1
  8. // @run-at document-start
  9. // ==/UserScript==
  10. //EXTRA OPTIONS (disabled by default)
  11. let show_overwrite_button = false //show separate button to overwrite
  12. let show_delete_button = false //show separate button to delete
  13. let generate_individual_delete_buttons = false //generate per comment delete and overwrite links
  14. let only_delete_old_comments = false //ignore comments newer than the limit below
  15. let old_comments_limit = 2 //if above is active, number of days after which a comment is considered old
  16. let only_delete_by_subreddit = false //ignore comments from subreddits other than the one chosen in the dropdown
  17. let time_between_actions = 2000 //reddit API limit is 60 actions per minute so don't exceed that
  18. let only_delete_downvoted = false //only delete comments under a certain karma
  19. let downvote_limit = -1 //if above is active, only delete comments with karma <= to this
  20. let ignore_upvoted = false //ignore comments over a certain karma (useless if only_delete_downvoted is active)
  21. let upvote_limit = 50 //if above is active, ignore comments with karma >= to this
  22. let auto_delete = false //automatically delete comments when navigating to comments page (use with filters!)
  23. let reload_on_completion = false //reload page on completion
  24. // TODO add STOP button
  25. // TODO add optional confirmation dialog OR start up delay
  26. // TODO add logic to avoid posts and enable using script on other user pages (Overview and Submitted)
  27. // TODO check compatibility with new reddit
  28. // TODO implement dictionary instead of random characters to defeat overwrite detection
  29. // reddit username
  30. unsafeWindow.user = '';
  31. // array of comments (more precisely author tags)
  32. unsafeWindow.comments = [];
  33. // top section contents
  34. unsafeWindow.div = null;
  35. //status text
  36. unsafeWindow.status_message = null;
  37. // subreddit selected for deletion
  38. unsafeWindow.subreddit = "ALL";
  39. unsafeWindow.subreddit_array = [];
  40. // on page loaded, initialize the script
  41. window.addEventListener("DOMContentLoaded", init_script, false);
  42. function init_script(ev) {
  43. // get logged in username
  44. unsafeWindow.user = document.querySelector("span.user > a:not(.login-required)").innerHTML;
  45. // if not logged in exit
  46. if (!unsafeWindow.user) return;
  47. // retrieve all VISIBLE comments
  48. get_comments()
  49. // automatically start deletion process instead of generating buttons, if active
  50. if (auto_delete) {
  51. unsafeWindow.start_processing_comments(true, true);
  52. }
  53. else {
  54. // generate the top buttons
  55. generate_top_buttons();
  56. }
  57. }
  58. function get_comments() {
  59. // find all author tags to eventually get comments
  60. let comments = document.querySelectorAll("a.author");
  61. // filter out other authors
  62. unsafeWindow.comments = [].filter.call(comments, filter_author);
  63. // remove duplicates to fix double processing of comments to own posts
  64. unsafeWindow.comments = filter_duplicates(unsafeWindow.comments);
  65. // if active, filter out comments from the past 24 hours
  66. if (only_delete_old_comments) {
  67. unsafeWindow.comments = [].filter.call(unsafeWindow.comments, filter_time);
  68. }
  69. // if active, filter out comments from other subreddits than the chosen one
  70. if (only_delete_by_subreddit && unsafeWindow.subreddit !== "ALL") {
  71. unsafeWindow.comments = [].filter.call(unsafeWindow.comments, filter_subreddit);
  72. }
  73. // if active, filter out non downvoted comments
  74. if (only_delete_downvoted) {
  75. unsafeWindow.comments = [].filter.call(unsafeWindow.comments, filter_downvotes)
  76. }
  77. // if active, filter out upvoted comments
  78. if (ignore_upvoted) {
  79. unsafeWindow.comments = [].filter.call(unsafeWindow.comments, filter_upvotes)
  80. }
  81. if (unsafeWindow.status_message !== null) update_status_text();
  82. }
  83. // append buttons to page
  84. function generate_top_buttons() {
  85. if (unsafeWindow.comments.length) {
  86. unsafeWindow.div = document.createElement("div");
  87. unsafeWindow.div.setAttribute('class', 'nextprev secure_delete_all');
  88. unsafeWindow.div.innerHTML = "";
  89. unsafeWindow.div.style.marginBottom = "10px";
  90. unsafeWindow.div.style.display = "flex";
  91. unsafeWindow.div.style.justifyContent = "flex-start";
  92. unsafeWindow.div.style.alignItems = "center";
  93. // make Subreddit Filter
  94. if (only_delete_by_subreddit) {
  95. //Create array of subreddits from comments
  96. unsafeWindow.subreddit_array = get_subreddit_array();
  97. let selectList = document.createElement("select");
  98. selectList.id = "subredditSelect";
  99. selectList.setAttribute('onChange', 'javascript: subreddit_select(this.value)')
  100. let selectedTitle = document.createElement("option");
  101. selectedTitle.selected = true;
  102. selectedTitle.disabled = true;
  103. selectedTitle.label = "Subreddit";
  104. selectList.append(selectedTitle);
  105. //Create and append the options
  106. for (let i = 0; i < unsafeWindow.subreddit_array.length; i++) {
  107. let option = document.createElement("option");
  108. option.value = unsafeWindow.subreddit_array[i];
  109. option.text = unsafeWindow.subreddit_array[i];
  110. selectList.appendChild(option);
  111. }
  112. unsafeWindow.div.appendChild(selectList);
  113. }
  114. // make Status message
  115. let status_div = document.createElement("div");
  116. status_div.style.marginLeft = "10px";
  117. unsafeWindow.status_message = document.createElement("p");
  118. unsafeWindow.status_message.setAttribute('class', 'status_message');
  119. unsafeWindow.status_message.innerHTML = "ERROR";
  120. status_div.appendChild(unsafeWindow.status_message);
  121. unsafeWindow.div.appendChild(status_div);
  122. // make Overwrite and Delete All link
  123. let odlink = document.createElement("a");
  124. odlink.setAttribute('class', 'bylink');
  125. odlink.setAttribute('onClick', 'javascript: start_processing_comments(true, true)');
  126. odlink.setAttribute('href', 'javascript:void(0)');
  127. odlink.style.marginLeft = "10px";
  128. odlink.appendChild(document.createTextNode('OVERWRITE AND DELETE'));
  129. unsafeWindow.div.appendChild(odlink);
  130. let br = document.createElement("br");
  131. unsafeWindow.div.appendChild(br);
  132. if (show_overwrite_button) {
  133. // make Overwrite All link
  134. let olink = document.createElement("a");
  135. olink.setAttribute('class', 'bylink');
  136. olink.setAttribute('onClick', 'javascript: start_processing_comments(true, false)');
  137. olink.setAttribute('href', 'javascript:void(0)');
  138. olink.style.marginLeft = "10px";
  139. olink.appendChild(document.createTextNode('OVERWRITE'));
  140. unsafeWindow.div.appendChild(olink);
  141. let br2 = document.createElement("br");
  142. unsafeWindow.div.appendChild(br2);
  143. }
  144. if (show_delete_button) {
  145. // make Delete All link
  146. let dlink = document.createElement("a");
  147. dlink.setAttribute('class', 'bylink');
  148. dlink.setAttribute('onClick', 'javascript: start_processing_comments(false, true)');
  149. dlink.setAttribute('href', 'javascript:void(0)');
  150. dlink.style.marginLeft = "10px";
  151. dlink.appendChild(document.createTextNode('DELETE'));
  152. unsafeWindow.div.appendChild(dlink);
  153. }
  154. //add our div to the webpage
  155. document.querySelector("div.content").insertBefore(unsafeWindow.div, document.querySelector("div.content").firstChild);
  156. //update status text now that we have defined unsafeWindow.status_message
  157. update_status_text();
  158. //add individual comment buttons
  159. if (generate_individual_delete_buttons) unsafeWindow.generate_delete_buttons()
  160. } else if (unsafeWindow.div != null) {
  161. document.querySelector("div.content")
  162. .insertBefore(document.createTextNode("No comments found."),
  163. document.querySelector("div.content").firstChild);
  164. }
  165. }
  166. unsafeWindow.start_processing_comments = function (overwrite_all, delete_all) {
  167. //get comments again in case the user has scrolled and revealed more comments
  168. get_comments()
  169. let commentsArray = [];
  170. for (let i = 0; i < unsafeWindow.comments.length; i++) {
  171. //for each author, get ID of the input field of the comment
  172. let thing_id = unsafeWindow.comments[i].parentNode.parentNode.querySelector("form.usertext > input[name='thing_id']").value;
  173. if (commentsArray.indexOf(thing_id) == -1) {
  174. commentsArray.push(thing_id);
  175. }
  176. }
  177. if (overwrite_all && delete_all) {
  178. unsafeWindow.overwrite_all(commentsArray, true);
  179. } else if (overwrite_all) {
  180. unsafeWindow.overwrite_all(commentsArray, false);
  181. } else if (delete_all) {
  182. unsafeWindow.delete_all(commentsArray);
  183. }
  184. //set status message while working
  185. if (unsafeWindow.status_message) unsafeWindow.status_message.innerHTML = "Processing...";
  186. }
  187. unsafeWindow.overwrite_all = function (comments, also_delete) {
  188. //get next comment id
  189. let thing_id = comments.shift();
  190. //overwrite the next comment in the stack
  191. unsafeWindow.overwrite_comment(thing_id);
  192. //if also deleting, add a timeout and delete the comment
  193. if (also_delete) unsafeWindow.setTimeout(unsafeWindow.delete_comment(thing_id), time_between_actions);
  194. //if there are still comments left, get next comment
  195. //increase timeout if also deleting
  196. if (comments.length) unsafeWindow.setTimeout(unsafeWindow.overwrite_all, also_delete ? time_between_actions * 2 : time_between_actions, comments, also_delete);
  197. else if (reload_on_completion) unsafeWindow.setTimeout(reload_page, time_between_actions * 5);
  198. else get_comments();
  199. }
  200. unsafeWindow.delete_all = function (comments) {
  201. unsafeWindow.delete_comment(comments.shift());
  202. //if there are still comments left, get next comment
  203. if (comments.length) unsafeWindow.setTimeout(unsafeWindow.delete_all, time_between_actions, comments);
  204. else if (reload_on_completion) unsafeWindow.setTimeout(reload_page, time_between_actions * 5);
  205. else get_comments();
  206. }
  207. unsafeWindow.overwrite_comment = function (thing_id) {
  208. try {
  209. //find edit form (hidden on page but active)
  210. let edit_form = document.querySelector("input[name='thing_id'][value='" + thing_id + "']").parentNode;
  211. //if comment is currently being edited, cancel out of that
  212. let edit_cancel_btn = edit_form.querySelector("div.usertext-edit > div.bottom-area > div.usertext-buttons > button.cancel");
  213. edit_cancel_btn.click();
  214. //find edit button and click it
  215. let edit_btn = edit_form.parentNode.querySelector("ul > li > a.edit-usertext");
  216. if (edit_btn) edit_btn.click();
  217. //find edit textbox and replace the string with random chars
  218. let edit_textbox = edit_form.querySelector("div.usertext-edit > div > textarea");
  219. let repl_str = '';
  220. let chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz><.-,+!#$%^&*();:[]~";
  221. for (let i = 0; i < edit_textbox.value.length; i++) {
  222. if (edit_textbox.value.substr(i, 1) == '\n') {
  223. repl_str += '\n';
  224. } else {
  225. let random_char = Math.floor(Math.random() * chars.length);
  226. repl_str += chars.charAt(random_char, 1);
  227. }
  228. }
  229. //set edited value to the random string
  230. edit_textbox.value = repl_str;
  231. //find save comment button and click it
  232. let edit_save_btn = edit_form.querySelector("div.usertext-edit > div.bottom-area > div.usertext-buttons > button.save");
  233. edit_save_btn.click();
  234. } catch (e) {
  235. alert("Error interacting with overwrite form: " + e);
  236. }
  237. }
  238. unsafeWindow.delete_comment = function (thing_id) {
  239. try {
  240. // get current status of comment editing box to prevent deleting comment before overwrite is complete
  241. let thing = document.querySelector("input[name='thing_id'][value='" + thing_id + "']");
  242. let status = thing.parentNode.querySelector("div.usertext-edit > div.bottom-area > div.usertext-buttons > span.status").innerHTML;
  243. if (status.indexOf("error") != -1) {
  244. alert("Failed to overwrite comment " + thing_id + " due to an unknown reddit error, skipping.");
  245. return;
  246. }
  247. // if status is submitting, there may be an internet connectivity error, so we retry
  248. if (status.indexOf("submitting") != -1) {
  249. unsafeWindow.setTimeout(unsafeWindow.delete_comment, time_between_actions * 2.5, thing_id);
  250. return;
  251. }
  252. // find delete button and click it and then yes confirmation button
  253. let del_form = thing.parentNode.parentNode.querySelector("ul.buttons > li > form.del-button");
  254. unsafeWindow.toggle(del_form.querySelector("span.main > a"));
  255. del_form.querySelector("span.error > a.yes").click();
  256. } catch (e) {
  257. alert("Error deleting comment: " + e);
  258. }
  259. }
  260. //[UTILITY FUNCTIONS]
  261. function filter_author(comment) {
  262. return comment.innerHTML == unsafeWindow.user;
  263. }
  264. function filter_time(comment) {
  265. let time = comment.parentNode.parentNode.querySelector("time").innerHTML;
  266. //always exclude comments from the past day
  267. if (time.indexOf("days") === -1) return false;
  268. let num_days = time.split(" ");
  269. return parseInt(num_days[0]) >= old_comments_limit;
  270. }
  271. function filter_subreddit(comment) {
  272. return comment.parentNode.parentNode.parentNode.querySelector("a.subreddit").innerHTML == unsafeWindow.subreddit;
  273. }
  274. function filter_downvotes(comment) {
  275. return parseInt(comment.parentNode.parentNode.querySelector("span.score.likes").title) <= downvote_limit;
  276. }
  277. function filter_upvotes(comment) {
  278. return parseInt(comment.parentNode.parentNode.querySelector("span.score.likes").title) <= upvote_limit;
  279. }
  280. function filter_duplicates(comments) {
  281. let array = [];
  282. // For self-posts, the same author tag will show up twice, once for the post author and
  283. //then for the comment author. this gets the thing_id for that tag and if there are two
  284. //consecutive tags it only keeps the second one. Otherwise, the script would process some
  285. //comments twice, leading to some filters not working properly.
  286. for (let i = 0; i < comments.length - 1; i++) {
  287. let this_comment = comments[i].parentNode.parentNode.querySelector("form.usertext > input[name='thing_id']").value;
  288. let next_comment = comments[i + 1].parentNode.parentNode.querySelector("form.usertext > input[name='thing_id']").value;
  289. if (this_comment != next_comment) array.push(comments[i]);
  290. }
  291. //since the loop excludes the final item, add it here (will be a comment author)
  292. array.push(comments[comments.length - 1])
  293. return array;
  294. }
  295. function reload_page() {
  296. unsafeWindow.location.reload();
  297. }
  298. function get_subreddit_array() {
  299. let array = [];
  300. for (let i = 0; i < unsafeWindow.comments.length; i++) {
  301. let sub = unsafeWindow.comments[i].parentNode.parentNode.parentNode.querySelector("a.subreddit").innerHTML;
  302. if (array.indexOf(sub) === -1) array.push(sub);
  303. }
  304. // Sort the array case insensitive and add option to disable subreddit filtering
  305. array = array.sort(sort_ignore_caps);
  306. array.unshift("ALL");
  307. return array;
  308. }
  309. function sort_ignore_caps(a, b) {
  310. return a.toLowerCase().localeCompare(b.toLowerCase());
  311. }
  312. function update_status_text() {
  313. if (unsafeWindow.status_message === null) return;
  314. let message = "FOUND " + unsafeWindow.comments.length + " COMMENT";
  315. if (unsafeWindow.comments.length > 1) message += "S";
  316. if ((only_delete_by_subreddit && unsafeWindow.subreddit !== "ALL") ||
  317. only_delete_downvoted ||
  318. ignore_upvoted ||
  319. only_delete_old_comments) {
  320. message += "\n(filters active)";
  321. }
  322. unsafeWindow.status_message.innerHTML = message;
  323. }
  324. unsafeWindow.overwrite_delete = function (thing_id) {
  325. unsafeWindow.overwrite_comment(thing_id);
  326. unsafeWindow.setTimeout(unsafeWindow.delete_comment, time_between_actions, thing_id);
  327. }
  328. //function to regenerate secure delete buttons after only overwriting a comment
  329. unsafeWindow.overwrite_reload = function (thing_id) {
  330. unsafeWindow.overwrite_comment(thing_id);
  331. unsafeWindow.setTimeout(unsafeWindow.generate_delete_buttons, 500);
  332. }
  333. //[EXTRA FEATURES]
  334. //Add a "SECURE DELETE" button near each comment delete button
  335. unsafeWindow.generate_delete_buttons = function () {
  336. // find all author tags to bypass filters applied to main comments array
  337. let comments = document.querySelectorAll("a.author");
  338. for (let i = 0; i < comments.length; i++) {
  339. try {
  340. // get the parent
  341. let main_parent = comments[i].parentNode.parentNode;
  342. let thing_id = main_parent.querySelector("form > input[name='thing_id']").value;
  343. let list = main_parent.querySelector("ul.flat-list");
  344. // if it already contains the tags, skip
  345. if (list.querySelector("li.secure_delete") && list.querySelector("li.overwrite")) continue;
  346. // add SECURE DELETE link to comments
  347. let secure_delete_link = document.createElement("li");
  348. secure_delete_link.setAttribute('class', 'secure_delete');
  349. let dlink = document.createElement("a");
  350. dlink.setAttribute('class', 'bylink secure_delete');
  351. dlink.setAttribute('onClick', 'javascript: overwrite_delete("' + thing_id + '")');
  352. dlink.setAttribute('href', 'javascript:void(0)');
  353. dlink.appendChild(document.createTextNode('SECURE DELETE'));
  354. secure_delete_link.appendChild(dlink);
  355. main_parent.querySelector("ul.flat-list").appendChild(secure_delete_link);
  356. // add OVERWRITE link to comments
  357. let overwrite_link = document.createElement("li");
  358. overwrite_link.setAttribute('class', 'overwrite');
  359. let olink = document.createElement("a");
  360. olink.setAttribute('class', 'bylink secure_delete');
  361. olink.setAttribute('onClick', 'javascript: overwrite_reload("' + thing_id + '")');
  362. olink.setAttribute('href', 'javascript:void(0)');
  363. olink.appendChild(document.createTextNode('OVERWRITE'));
  364. overwrite_link.appendChild(olink);
  365. main_parent.querySelector("ul.flat-list").appendChild(overwrite_link);
  366. } catch (e) {
  367. alert("Error adding Secure Delete links to comments.\nError: " + e + " Stack:" + e.stack);
  368. }
  369. }
  370. }
  371. unsafeWindow.subreddit_select = function (option) {
  372. unsafeWindow.subreddit = option;
  373. get_comments();
  374. }