scratch-comment-anonymizer.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. // ==UserScript==
  2. // @name Scratch Comment Redactor
  3. // @namespace https://towerofnix.github.io
  4. // @match *://scratch.mit.edu/*
  5. // @grant none
  6. // ==/UserScript==
  7. const usersSymbol = Symbol()
  8. const replaceText = function(el, newText) {
  9. while (el.firstChild) {
  10. el.firstChild.remove()
  11. }
  12. el.appendChild(document.createTextNode(newText))
  13. }
  14. const redactCommentThread = function(comment, usernameToRedact) {
  15. const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  16. let topThread = comment
  17. while (topThread && !topThread.classList.contains('top-level-reply')) {
  18. topThread = topThread.parentElement
  19. }
  20. if (!topThread) {
  21. console.error('Could not get the thread element, sorry :(')
  22. console.info(comment)
  23. return
  24. }
  25. if (!(usersSymbol in topThread)) {
  26. topThread[usersSymbol] = {} // Mapping of username -> descriptor
  27. }
  28. const users = topThread[usersSymbol]
  29. if (!users.hasOwnProperty(usernameToRedact)) {
  30. const index = Object.keys(users).length
  31. users[usernameToRedact] = {
  32. realUsername: usernameToRedact,
  33. fakeUsername: `[User ${alphabet[index]}]`,
  34. hue: Math.round(360 / 7 * index)
  35. }
  36. }
  37. const descriptor = users[usernameToRedact]
  38. const allComments = [comment, ...topThread.querySelectorAll('.reply .comment')]
  39. for (const comment of allComments) {
  40. // Redact bold "author" username at top of comment
  41. const authorUsernameEl = comment.querySelector('.info .name a')
  42. const authorUsername = authorUsernameEl.innerText.trim()
  43. if (authorUsername === usernameToRedact) {
  44. replaceText(authorUsernameEl, descriptor.fakeUsername)
  45. // Also redact profile picture:
  46. const authorAvatarEl = comment.querySelector('#comment-user')
  47. const img = authorAvatarEl.querySelector('img')
  48. if (img) {
  49. authorAvatarEl.querySelector('img').remove()
  50. const fakeProfilePicture = document.createElement('div')
  51. Object.assign(fakeProfilePicture.style, {
  52. float: 'left', display: 'block', marginRight: '10px',
  53. width: '45px', height: '45px',
  54. backgroundColor: `hsl(${descriptor.hue}deg, 100%, 50%)`
  55. })
  56. authorAvatarEl.appendChild(fakeProfilePicture)
  57. }
  58. }
  59. // Redact "@user" replies
  60. const a = comment.querySelector('.content a')
  61. if (a && a.innerText.trim() === '@' + usernameToRedact) {
  62. replaceText(a, '@' + descriptor.fakeUsername)
  63. }
  64. }
  65. }
  66. // Button-adder
  67. const commentsContainer = document.querySelector('#comments ul.comments')
  68. if (commentsContainer) {
  69. const observer = new MutationObserver(mutations => {
  70. for (const mutation of mutations) {
  71. for (const addedNode of mutation.addedNodes) {
  72. if (typeof addedNode.classList === 'undefined') continue
  73. if (addedNode.classList.contains('top-level-reply') === false) continue
  74. const topComment = addedNode
  75. const comments = [topComment, ...topComment.querySelectorAll('.reply .comment')]
  76. for (const comment of comments) {
  77. const usernameEl = comment.querySelector('.info .name a')
  78. if (usernameEl) {
  79. const actionContainer = comment.querySelector('.actions-wrap')
  80. const span = document.createElement('span')
  81. const username = usernameEl.innerText.trim()
  82. span.classList.add('actions', 'report')
  83. span.appendChild(document.createTextNode('Anonymize'))
  84. const handler = () => {
  85. redactCommentThread(topComment, username)
  86. replaceText(span, 'Redacted')
  87. span.style.cursor = 'default'
  88. span.removeEventListener('click', handler)
  89. }
  90. span.addEventListener('click', handler)
  91. actionContainer.appendChild(span)
  92. }
  93. }
  94. }
  95. }
  96. })
  97. observer.observe(commentsContainer, {childList: true})
  98. }