def perform_post_game_actions(self): """ After the game ends, performs a series of operations. These operations currently include: - Generating the winners list - Saving the winners list to a file - Updating the main submission with the winners list content - Sending Private Messages to staff members & winners """ # Save winners list in memory potential_winners_list = self.db.get_potential_winners_list() winners_list = self.db.get_random_winners_from_list( num_winners=NUM_WINNERS, potential_winners_list=potential_winners_list) # Save winners submission content to file winners_submission_content = output_winners_list_to_file( potential_winners_list=potential_winners_list, winners_list=winners_list, output_file_path=WINNERS_LIST_FILE_PATH, ) tprint( f"Winners list successfully output to: {WINNERS_LIST_FILE_PATH}") # Update main submission with winner submission content at the top self.submission.edit(winners_submission_content + self.submission.selftext) tprint( "Reddit submission successfully updated with the winners list info!" ) # Private messages version_string = self.patch_notes_file.get_version_string() send_message_to_staff( reddit=self.reddit, winners_list_path=WINNERS_LIST_FILE_PATH, staff_recipients=STAFF_RECIPIENTS_LIST, version_string=version_string, gold_coin_reward=GOLD_COIN_REWARD, ) send_message_to_winners( reddit=self.reddit, winners_list=winners_list, reward_codes_list=get_reward_codes_list( self.reward_codes_filepath), version_string=version_string, gold_coin_reward=GOLD_COIN_REWARD, )
def has_exceeded_revealed_line_count(self) -> bool: """ Checks if the number of revealed lines exceeds the max allowed revealed line count Returns True if the line count has been exceeded Returns False otherwise """ b_line_count_exceeded = self.db.get_entry_count_in_patch_notes_line_tracker( ) >= ((MAX_PERCENT_OF_LINES_REVEALED / 100) * self.patch_notes_file.get_total_line_count()) if b_line_count_exceeded: tprint( "Number of revealed lines exceeds the max allowed revealed line count." ) return b_line_count_exceeded
def safe_comment_reply(self, comment: Comment, text_body: str): """ Attempts to reply to a comment & safely handles a RedditAPIException (e.g. if that comment has been deleted & cannot be responded to) Attributes: comment: a praw Comment model instance text_body: the markdown text to include in the reply made """ try: comment.reply(body=text_body) except RedditAPIException as redditErr: tprint(f"Unable to reply (RedditAPIException): {redditErr}") return None except Exception as err: tprint(f"Unable to reply (general Exception): {err}") return None
def test_tprint(): message = "Test message" output_message = util.tprint(message) # Test if message is within the output message assert message in output_message # Test whether timestamp is in correct format in the output message regex_capture = re.search(r"\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}]", output_message) assert regex_capture is not None
def main(): """ Main method for the Reddit bot/script """ # Initialize bot by creating reddit & subreddit instances reddit = praw.Reddit(BOT_USERNAME, user_agent=USER_AGENT) reddit.validate_on_submit = True subreddit = reddit.subreddit(SUBREDDIT_NAME) # Initialize other variables database = Database() patch_notes_file = PatchNotesFile(PATCH_NOTES_PATH) # Initialize submissions (i.e. Reddit threads) submission, community_submission = init_submissions( reddit, subreddit, database, patch_notes_file, SUBMISSION_CONTENT_PATH, COMMUNITY_SUBMISSION_CONTENT_PATH, ) # Create core object core = Core( reddit=reddit, db=database, submission=submission, community_submission=community_submission, patch_notes_file=patch_notes_file, ) # =============================================================== # Core loop to listen to unread comment messages on Reddit # =============================================================== tprint("Reddit Bot's core loop started") while 1: if not core.loop(): tprint("Reddit Bot script ended via core loop end conditions") break # Time to wait before calling the Reddit API again (in seconds) time.sleep(SLEEP_INTERVAL_SECONDS) # ======================== # Bot end script actions # ======================== tprint("Performing actions after the game has ended...") core.perform_post_game_actions() tprint("Reddit bot script ended gracefully")
def send_message_to_staff( reddit: Reddit, winners_list_path: str, staff_recipients: List[str], version_string: str, gold_coin_reward: int, ): """ Sends the winners list results to a list of recipients via Private Message (PM) This function must be called only after the winners_list_path file exists! Attributes: reddit: the PRAW Reddit instance winners_list_path: the file path to read from for the winners list + potential winners list staff_recipients: a list of staff member recipients for the PM version_string: the version of the patch notes gold_coin_reward: the number of Gold Coins intended for the reward """ with open(winners_list_path, "r") as winners_list_file: winners_list_text = ( f"The following Reddit users have won {str(gold_coin_reward)} Gold Coins from the Reddit Patch Notes game:\n\n" + winners_list_file.read()) subject_line = ( f"{version_string} - Winners for the HoN Patch Notes Guessing Game" ) for recipient in staff_recipients: try: reddit.redditor(recipient).message(subject=subject_line, message=winners_list_text) except RedditAPIException as redditError: tprint(f"RedditAPIException encountered: {redditError}") tprint( f"{recipient} was not sent a message, continuing to next recipient" ) continue except Exception as error: tprint(f"General Exception encountered: {error}") tprint( f"{recipient} was not sent a message, continuing to next recipient" ) continue
def loop(self): # noqa: C901 """ Core loop of the bot Returns: - True, if the loop should continue running - False, if the loop should stop running """ # Check unread replies try: # Stop indefinite loop if current time is greater than the closing time. if is_game_expired(self.game_end_time): return False for unread_item in self.reddit.inbox.unread(limit=None): unread_item.mark_read() # Only proceed with processing the unread item if it belongs to the current thread if (isinstance(unread_item, Comment) and unread_item.submission.id == self.submission.id): author = unread_item.author # Exit loop early if the user does not meet the posting conditions if self.is_disallowed_to_post(author, unread_item): continue # Get patch notes line number from the user's post patch_notes_line_number = get_patch_notes_line_number( unread_item.body) if patch_notes_line_number is None: continue # Get author user id & search for it in the Database (add it if it doesn't exist) user = self.get_user_from_database(author) # Run the game rules, and exit early if game-ending conditions are met if not self.process_game_rules_for_user( user, author, unread_item, patch_notes_line_number): return False # Stop indefinite loop if current time is greater than the closing time. if is_game_expired(self.game_end_time): return False # After going through the bot's inbox, return True if inner loop stop functions are not met return True # Occasionally, Reddit may throw a 503 server error while under heavy load. # In that case, log the error & just wait and try again in the next loop cycle except ServerError as serverError: tprint(f"Server error encountered in core loop: {serverError}") sleep_time = 60 tprint(f"Sleeping for {sleep_time} seconds...") time.sleep(sleep_time) return True # main.py loop should continue after the sleep period # Handle remaining unforeseen exceptions and log the error except Exception as error: tprint(f"General exception encountered in core loop: {error}") sleep_time = 60 tprint(f"Sleeping for {sleep_time} seconds...") time.sleep(sleep_time) return True # main.py loop should continue after the sleep period
def send_message_to_winners( # noqa: C901 reddit: Reddit, winners_list: List[str], reward_codes_list: List[str], version_string: str, gold_coin_reward: int, ): """ Sends the winners list results to a list of recipients via Private Message (PM). This function uses recursion to send messages to failed recipients. This function also frequently encounters Reddit API Exceptions due to rate limits. To sleep for the appropriate duration without wasting time, the rate limit error is parsed: Test strings for regex capture: RATELIMIT: "Looks like you've been doing that a lot. Take a break for 4 minutes before trying again." on field 'ratelimit' RATELIMIT: "Looks like you've been doing that a lot. Take a break for 47 seconds before trying again." on field 'ratelimit' RATELIMIT: "Looks like you've been doing that a lot. Take a break for 4 minutes 47 seconds before trying again." on field 'ratelimit' RATELIMIT: "Looks like you've been doing that a lot. Take a break for 1 minute before trying again." on field 'ratelimit' Attributes: reddit: the PRAW Reddit instance winners_list: a list of winning recipients for the PM reward_codes_list: a list of reward codes for each winner version_string: the version of the patch notes gold_coin_reward: the number of Gold Coins intended for the reward """ subject_line = f"Winner for the {version_string} Patch Notes Guessing Game" failed_recipients_list = [] for recipient in winners_list: reward_code = ( "N/A - all possible reward codes have been used up.\n\n" f"Please contact {STAFF_MEMBER_THAT_HANDS_OUT_REWARDS} for a code to be issued manually." ) if len(reward_codes_list) > 0: reward_code = reward_codes_list[0] # TODO: Add this back if reward codes generator works again # message = ( # f"Congratulations {recipient}!\n\n" # f"You have been chosen by the bot as a winner for the {version_string} Patch Notes Guessing Game!\n\n" # f"Your reward code for {str(gold_coin_reward)} Gold Coins is: **{reward_code}**\n\n" # "You can redeem your reward code here: https://www.heroesofnewerth.com/redeem/\n\n" # f"Please contact {STAFF_MEMBER_THAT_HANDS_OUT_REWARDS} if any issues arise.\n\n" # "Thank you for participating in the game! =)" # ) message = ( f"Congratulations {recipient}!\n\n" f"You have been chosen by the bot as a winner for the {version_string} Patch Notes Guessing Game!\n\n" f"Please contact /u/{STAFF_MEMBER_THAT_HANDS_OUT_REWARDS} via the Reddit Messaging system to obtain your code.\n\n" "Please include your In-Game Username in your message.\n\n" "Thank you for participating in the game! =)") try: reddit.redditor(recipient).message(subject=subject_line, message=message) tprint( f"Winner message sent to {recipient}, with code: {reward_code}" ) # Pop reward code from list only if the message was sent successfully if len(reward_codes_list) > 0: reward_codes_list.pop(0) # Reddit API Exception except RedditAPIException as redditException: tprint(f"Full Reddit Exception: {redditException}\n\n") for subException in redditException.items: # Rate limit error handling if subException.error_type == "RATELIMIT": failed_recipients_list.append(recipient) tprint( f"{redditException}\n{recipient} was not sent a message (added to retry list), " "continuing to next recipient") tprint(f"Subexception: {subException}\n\n") # Sleep for the rate limit duration by parsing the minute and seconds count from # the message into named groups regex_capture = re.search( r"\s+((?P<minutes>\d+) minutes?)?\s?((?P<seconds>\d+) seconds)?\s+", subException.message, ) if regex_capture is None: print( "Invalid regex detected. Sleeping for 60 seconds..." ) time.sleep(60) break else: # Use named groups from regex capture and assign them to a dictionary sleep_time_regex_groups = regex_capture.groupdict( default=0) # Add 1 extra second to account for millisecond-precision checking secondsToSleep = ( 60 * int( sleep_time_regex_groups.get( "minutes") # type: ignore ) + int( sleep_time_regex_groups.get( "seconds") # type: ignore ) + 1) # type: ignore print(f"Sleeping for {str(secondsToSleep)} seconds") time.sleep(secondsToSleep) break continue except Exception as error: tprint( f"{error}\n{recipient} was not sent a message (will not retry), continuing to next recipient" ) continue # At the end of the function, recurse this function to re-send messages to failed recipients # Recurse only if failed_recipients_list has content in it # Prevents infinite loops by ensuring that the failed recipients count # gradually progresses towards the end condition. failed_recipients = len(failed_recipients_list) if failed_recipients > 0 and failed_recipients < len(winners_list): send_message_to_winners( reddit, failed_recipients_list, reward_codes_list, version_string, gold_coin_reward, )