Example #1
0
    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,
        )
Example #2
0
    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
Example #3
0
    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
Example #5
0
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
Example #7
0
    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,
        )