123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- import logging
- import time
- import re
- import praw
- import yaml
- from rapidfuzz import fuzz
- from string import Template
- from praw.exceptions import APIException
- from . import models
- SUBREDDIT_NAME = 'Piracy'
- SETTINGS_FILE = 'settings/WelcomeBot_settings.yaml'
- # hours * seconds per hour
- # max time for submission existance before the bot rejects approval
- MAX_TIME_ALLOWANCE = 8 * 3600
- logger = logging.getLogger('main')
- console_logger = logging.getLogger('console_logger')
- class WelcomeBot:
- def __init__(self, reddit):
- self.reddit = reddit
- self.settings = {}
- self._load_settings()
- def handle_comment(self, comment):
- if comment.subreddit.display_name != SUBREDDIT_NAME:
- return
- user = models.RedditUser.objects.get_or_create(name=comment.author.name)[0]
- if user.has_generally_interacted:
- return
- console_logger.info(f'Welcoming user: {user.name}')
- self._message_user(
- comment.author, self.settings['WELCOME_MESSAGE_SUBJECT_TITLE'], self.settings['WELCOME_MESSAGE'])
- self._update_user(user)
- def handle_submission(self, submission):
- if submission.subreddit.display_name != SUBREDDIT_NAME:
- return
- # refresh post in case automoderator is delayed in applying its own removal rules
- time.sleep(2)
- submission = self.reddit.submission(id=submission.id)
- if (submission.banned_by is not None
- or submission.approved
- or submission.author is None):
- return
- user = models.RedditUser.objects.get_or_create(name=submission.author.name)[0]
- # move forward with removal
- if not user.has_previously_submitted and self.is_link_flair_remove_worthy(submission.link_flair_text):
- logger.info(
- f'Processing submission by new user - {submission.author.name} - https://reddit.com{submission.permalink}')
- self._update_user(user)
- self._remove_submission(submission, user)
- # skip removal of submission and just welcome the user
- elif not user.has_generally_interacted:
- console_logger.info(f'Welcoming user: ${submission.author.name}')
- self._update_user(user, include_submissions=True)
- self._message_user(submission.author,
- self.settings['WELCOME_MESSAGE_SUBJECT_TITLE'], self.settings['WELCOME_MESSAGE'])
- def _remove_submission(self, submission, user):
- """Attempt to remove submission by new user
- Args:
- submission (praw.models.Submission): praw submission instance
- user (models.RedditUser): database record instance of user
- """
- removal_message = Template(self.settings['REMOVAL_MESSAGE']).safe_substitute(
- REMOVED_POST_PERMALINK=submission.permalink,
- REQUIRED_REPLY=self.settings['REQUIRED_REPLY']
- )
- success = self._message_user(submission.author, self.settings['REMOVAL_MESSAGE_SUBJECT_TITLE'], removal_message)
- if not success:
- self._update_user(user, include_submissions=True)
- return
- submission.mod.remove()
- def handle_message(self, message):
- valid_message, first_message = self._check_message_validity(message)
- if not valid_message:
- return
- logger.info(f' >> Processing agreement message by user: {message.author.name}')
- submission_url = re.search(self.settings['PERMALINK_FIRST_MESSAGE_RE'], first_message.body).group(1)
- submission = self.reddit.submission(url=submission_url)
- self._process_submission_approval(submission, submission_url, message)
- def _check_message_validity(self, message):
- """Checks whether message is not a comment reply and if message is in reply to the bot's original removal message
- and if the user's message contains the required keywords needed to approve their post
- Args:
- message (praw.models.Message): praw message instance
- Returns:
- tuple: tuple of (bool, str); The boolean indicates whether the message is a valid required response
- """
- if not isinstance(message, praw.models.Message):
- return False, ''
- is_agreement_message = False
- if (fuzz.partial_ratio(self.settings['REQUIRED_REPLY'], message.body, score_cutoff=90)
- or re.search(self.settings['REQUIRED_REPLY_RE'], message.body, re.IGNORECASE)):
- is_agreement_message = True
- # check for a previous message in conversation. If it fails, it means the message is a new conversation: ignore message
- try:
- first_message = self.reddit.inbox.message(message.first_message_name[3:])
- except:
- message.mark_read()
- if is_agreement_message:
- message.reply(
- 'There was an issue. You must reply to the original message instead of creating a new conversation')
- return False, ''
- is_reply_to_removal_message = False
- # check if identifier markdown comment is inside first message
- if 'identifier: removal message by WelcomeBot' in first_message.body:
- is_reply_to_removal_message = True
- if not is_reply_to_removal_message:
- return False, ''
- elif not is_agreement_message:
- if any(substr in message.body.lower() for substr in ['wtf', 'fuck']):
- logger.info(f' > Replying badly to {message.author.name}')
- message.reply('Villain, I have done thy mother')
- else:
- logger.info(f' > Replying nicely to {message.author.name}')
- message.reply('I am just a wee bot and you are using strange words. I do not understand')
- return False, ''
- return True, first_message
- def _process_submission_approval(self, submission, submission_url, message):
- """Upon the user replying to the bot for approval, this will help the bot decide
- if the submission if eligible for approval. eg. if the user replied within a set timeframe,
- or if another moderator has decided to remove the post (decline approval)
- Args:
- submission (praw.models.Submission): praw submission instance
- submission_url (str): full URL of submission
- message (praw.models.Message): praw message instance
- """
- # if message is deleted
- if submission.author is None:
- return
- link_flair_text = submission.link_flair_text
- if link_flair_text is None:
- link_flair_text = ''
- # if submission is approved already
- if submission.approved or submission.banned_by is None:
- message.reply(f'[Your submission]({submission_url}) is already visible to everyone.')
- # if reply is past the MAX_TIME_ALLOWANCE (seconds)
- elif time.time() - submission.created_utc > MAX_TIME_ALLOWANCE:
- SORRY_REPLY = Template(self.settings['SORRY_REPLY']).safe_substitute(
- MAX_TIME_ALLOWANCE=MAX_TIME_ALLOWANCE//3600)
- message.reply(SORRY_REPLY)
- # if someone else removed the post
- elif submission.banned_by is not None and submission.banned_by != self.reddit.user.me().name:
- message.reply(self.settings['OVERRIDE_UNAVAILABLE_REPLY'])
- # if post qualifies for bot approval
- elif link_flair_text.startswith(('custom removal', 'rule')):
- message.reply(
- 'Hello there. Your submission has been removed for spam/breaking the rules by another moderator. This bot cannot override their actions.')
- elif submission.banned_by == self.reddit.user.me().name:
- logger.info(
- f' >>> Approving submission by {submission.author.name}: https://reddit.com{submission.permalink}')
- submission.mod.approve()
- if submission.is_self:
- self._report_if_possible_rule3(submission)
- message.reply(
- Template(self.settings['ALLS_GOOD_REPLY']).safe_substitute(REMOVED_POST_PERMALINK=submission.permalink)
- )
- user = models.RedditUser.objects.get_or_create(name=submission.author.name)[0]
- self._update_user(user, include_submissions=True)
- def _report_if_possible_rule3(self, submission):
- """Sometimes the submission may be blatantly a rule-breaking post regardless of whether the user is told what the rules are
- this will help the bot to decide whether to report the submission after approval based on keywords for possible rule 3 breaking
- Args:
- submission (praw.models.Submission): praw submission instance
- """
- # reddit report reasons have a max char length of 95-100 i forgot
- max_length = 65
- for str_re in self.settings['POSSIBLE_RULE3_RE']:
- match = re.search(str_re, submission.title, re.IGNORECASE | re.DOTALL)
- if match:
- report_match = match.group()[:max_length]
- submission.report(f'Possible rule 3? - in_title: [{report_match}]')
- return
- match = re.search(str_re, submission.selftext, re.IGNORECASE | re.DOTALL)
- if match:
- report_match = match.group()[:max_length]
- submission.report(f'Possible rule 3? - in_body: [{report_match}]')
- return
- def is_link_flair_remove_worthy(self, link_flair_text) -> bool:
- if link_flair_text is None:
- link_flair_text = ''
- for flair_re in self.settings['FLAIR_TYPES_RE']:
- if re.search(flair_re, link_flair_text, re.IGNORECASE):
- return True
- return False
- @staticmethod
- def _message_user(user, subject, message) -> bool:
- """Sends a user a private message
- Args:
- user (praw.models.Redditor): praw redditor instance
- subject (str): subject line
- message (str): message body
- Returns:
- bool: A boolean indicating if the message was sent successfully
- """
- try:
- user.message(subject=subject, message=message)
- return True
- except APIException as e:
- if e.error_type == "NOT_WHITELISTED_BY_USER_MESSAGE":
- logger.info(f"User {user.name} has a whitelist, therefore there is no way to message them")
- else:
- logger.info(f"Error with attempt to send message: {e}")
- logger.exception(e)
- return False
- def _update_user(self, user: models.RedditUser, include_submissions=False):
- """update RedditUser record in database
- Args:
- user (models.RedditUser): database record instance of user
- include_submissions (bool, optional): Defaults to False.
- """
- user.has_generally_interacted = True
- if include_submissions:
- user.has_previously_submitted = True
- user.save()
- def _load_settings(self):
- with open(SETTINGS_FILE, 'r', encoding='utf8') as fd:
- self.settings = yaml.safe_load(fd)
|