ModerationHelper.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import json
  2. import logging
  3. import time
  4. import re
  5. from praw.exceptions import ClientException
  6. from urllib.parse import unquote
  7. SUBREDDIT_NAME = 'Piracy'
  8. logger = logging.getLogger('main')
  9. class SubmissionRemover:
  10. """Moderation helper. Comment with `!rule x` keyword to have a submission removed due to breaking that specific rule.
  11. Follow up with a `ban x` keyword to help the bot decide whether to ban the submission OP for the specified amount of days, 0 days is permaban.
  12. The removal reasons for `!rule x` keywords are read from the moderation toolbox extension settings
  13. saved in the subreddit's wiki in `reddit.com/r/subreddit/wiki/toolbox/`
  14. """
  15. # Moderator Toolbox extension's settings: ie. toolbox settings path is sitting on the base path `reddit.com/r/subreddit/wiki/toolbox/`
  16. toolbox_settings_wiki_path = 'toolbox'
  17. cache_frequency_secs = 15 * 60
  18. rule_re = r'(?si)!(?:(rule) *(\d))'
  19. custom_re = r'(?si)!(custom) *(.+)?'
  20. ban_re = r'!rule *\d ban (\d+)'
  21. def __init__(self, reddit):
  22. self.subreddit = reddit.subreddit(SUBREDDIT_NAME)
  23. self.last_cached = 0
  24. self.toolbox_settings = None
  25. self._load_settings()
  26. def handle_comment(self, comment):
  27. if comment.subreddit.display_name != SUBREDDIT_NAME:
  28. return
  29. self._load_settings()
  30. keyword = self._get_keyword(comment)
  31. ban_length = self._get_ban_length(comment)
  32. if keyword and comment.author.name in self.subreddit.moderator():
  33. removal_reason = self._get_removal_reason(keyword, comment)
  34. self._remove_submission(comment, keyword, removal_reason)
  35. # 'if ban_length' should not be used because ban_length could be 0
  36. if ban_length is not None:
  37. self._ban_user(comment, keyword, ban_length)
  38. def _load_settings(self):
  39. if time.time() - self.last_cached > self.cache_frequency_secs:
  40. toolbox_settings = self.subreddit.wiki[self.toolbox_settings_wiki_path].content_md
  41. toolbox_settings = json.loads(toolbox_settings)
  42. self.last_cached = time.time()
  43. self.toolbox_settings = toolbox_settings
  44. def _get_keyword(self, comment):
  45. match = re.search(self.rule_re, comment.body)
  46. if match:
  47. return match.group(1) + ' ' + match.group(2)
  48. match = re.search(self.custom_re, comment.body)
  49. if match:
  50. return match.group(1)
  51. return None
  52. def _get_removal_reason(self, keyword, comment):
  53. if keyword.lower() == 'custom':
  54. return '> ' + re.search(self.custom_re, comment.body).group(2).replace('\n', '\n> ')
  55. for reason in self.toolbox_settings['removalReasons']['reasons']:
  56. if reason['flairText'].lower() == keyword.lower():
  57. return unquote(reason['text'])
  58. return None
  59. def _get_ban_length(self, comment):
  60. ban_length = re.search(self.ban_re, comment.body, re.IGNORECASE)
  61. if ban_length:
  62. return int(ban_length.group(1))
  63. return None
  64. def _ban_user(self, comment, keyword, ban_length):
  65. submission = comment.submission
  66. if keyword.lower() == 'custom' or submission.author is None:
  67. return
  68. if ban_length == 0:
  69. ban_length = None
  70. ban_reason = keyword
  71. self.subreddit.banned.add(
  72. comment.submission.author.name,
  73. duration=ban_length,
  74. ban_reason=ban_reason,
  75. ban_message=f'Banned due to breaking {keyword}. https://reddit.com{submission.permalink}',
  76. note=f'Banned by {comment.author.name}. Offending post: https://reddit.com{submission.permalink}'
  77. )
  78. logger.info(f'Banning {comment.submission.author.name} for {ban_length} days')
  79. def _remove_submission(self, comment, keyword, removal_reason):
  80. submission = comment.submission
  81. if submission.selftext == '[deleted]':
  82. return
  83. logger.info(f'Remover (triggered by /u/{comment.author.name}): Processing submission {submission.id} ({submission.title})')
  84. if not submission.removed:
  85. submission.mod.remove()
  86. author_name = 'there'
  87. # check if author is discoverable: if user account is deleted, submission.author (praw.models.Redditor) instance will yield None
  88. if submission.author is not None:
  89. author_name = '/u/' + submission.author.name
  90. flair_text = 'custom removal' if keyword.lower() == 'custom' else keyword
  91. submission.mod.flair(text=flair_text)
  92. removal_reason = f'Hello {author_name}, your submission has been removed due to:\n\n' + removal_reason
  93. myreply = submission.reply(removal_reason)
  94. myreply.mod.distinguish(how='yes', sticky=True)
  95. myreply.disable_inbox_replies()
  96. if keyword.lower().endswith( ('rule 2', 'rule 3') ):
  97. submission.mod.lock()
  98. class Nuker:
  99. """Auto remove a comment tree or submission. The root of the comment tree to remove is decided
  100. based on which comment the keyword `!nuke` was commented as a reply. If `!nuke` is commented directly
  101. as a reply to the submission itself, all comments will be nuked
  102. """
  103. nuke_kw_re = r'(?i)!nuke\b'
  104. unnuke_kw_re = r'(?i)!unnuke\b'
  105. def __init__(self, reddit):
  106. self.subreddit = reddit.subreddit(SUBREDDIT_NAME)
  107. def handle_comment(self, comment):
  108. if comment.subreddit.display_name != SUBREDDIT_NAME:
  109. return
  110. if re.search(self.nuke_kw_re, comment.body):
  111. self._process_nuke(comment)
  112. if re.search(self.unnuke_kw_re, comment.body):
  113. self._process_unnuke(comment)
  114. def _process_nuke(self, comment):
  115. if not comment.author in self.subreddit.moderator():
  116. return
  117. parent = comment.parent()
  118. if comment.parent_id.startswith('t3'):
  119. logger.info(f'Nuking submission (exec by /u/{comment.author.name}): {parent.permalink}')
  120. self._nuke_submission(parent)
  121. else:
  122. logger.info(f'Nuking comment tree (exec by /u/{comment.author.name}): {parent.permalink}')
  123. self._nuke_comment_tree(parent)
  124. try:
  125. comment.refresh()
  126. except ClientException as e:
  127. self.logger.info(f'ClientException: {e}. Comment permalink: https://reddit.com{comment.permalink}')
  128. return
  129. comment.replies.replace_more(limit=None)
  130. child_comments = comment.replies.list()
  131. for child_comment in child_comments:
  132. if child_comment.removed:
  133. child_comment.mod.approve()
  134. def _nuke_submission(self, submission):
  135. submission.comments.replace_more(limit=None)
  136. if not submission.removed:
  137. submission.mod.remove()
  138. comments = submission.comments.list()
  139. for comment in comments:
  140. if comment.author is not None and comment.author.name == 'PiracyBot' and comment.distinguished == 'moderator':
  141. continue
  142. if not comment.removed:
  143. comment.mod.remove()
  144. def _nuke_comment_tree(self, comment):
  145. if not comment.removed:
  146. comment.mod.remove()
  147. comment.refresh()
  148. comment.replies.replace_more(limit=None)
  149. child_comments = comment.replies.list()
  150. for child_comment in child_comments:
  151. if comment.author is not None and comment.author.name == 'PiracyBot' and comment.distinguished == 'moderator':
  152. continue
  153. if not child_comment.removed:
  154. child_comment.mod.remove()
  155. def _process_unnuke(self, comment):
  156. if not comment.author in self.subreddit.moderator():
  157. return
  158. parent = comment.parent()
  159. if comment.parent_id.startswith('t3'):
  160. logger.info(f'Un-nuking submission (exec by /u/{comment.author.name}): {parent.permalink}')
  161. self._unnuke_submission(parent)
  162. else:
  163. logger.info(f'Un-nuking comment tree (exec by /u/{comment.author.name}): {parent.permalink}')
  164. self._unnuke_comment_tree(parent)
  165. def _unnuke_submission(self, submission):
  166. submission.comments.replace_more(limit=None)
  167. if submission.removed:
  168. submission.mod.approve()
  169. comments = submission.comments.list()
  170. for comment in comments:
  171. if comment.removed:
  172. comment.mod.approve()
  173. def _unnuke_comment_tree(self, comment):
  174. if comment.removed:
  175. comment.mod.approve()