コード例 #1
0
    def __init__(self, reddit, test_mode):
        self.reddit = reddit
        self.test_mode = test_mode
        self.data_access = DataAccess(test_mode)
        self.my_id = self.reddit.user.me().id

        self.subreddit_name = "InsiderMemeBot_Test" if test_mode else "InsiderMemeTrading"
        self.subreddit = self.reddit.subreddit(self.subreddit_name)
コード例 #2
0
    def __init__(self, reddit, test_mode):
        self.reddit = reddit
        self.test_mode = test_mode
        self.data_access = DataAccess(test_mode)
        self.my_id = self.reddit.user.me().id

        self.imt_subreddit_name = "InsiderMemeBot_Test" if test_mode else "InsiderMemeTrading"
        self.imt_subreddit = self.reddit.subreddit(self.imt_subreddit_name)

        # Store the IDs of the last 1000 comments that the RequestListener has processed.

        # For efficiency the IDs are stored twice, in two different orders.
        # -  One set is a simple list that keeps the comments in the order they were processed, so that we can
        #    easily know which comment to pop off the collection once 1000 comments are exceeded.
        #
        # -  One set is sorted by the hash of the comment. This makes it easy to determine if a
        #    given comment has already been processed, without having to iterate over the entire list.
        #
        # These collections shouldn't be used directly by implementing classes.
        self.__processed_ids_by_time = []
        self.__processed_ids_by_hash = SortedSet()
コード例 #3
0
    def __init__(self, reddit, test_mode):
        """
        Creates a new instance of InsiderMemeBot.

        @param reddit: The authenticated praw.Reddit instance.
        @param test_mode: A boolean indicating whether InsiderMemeBot will be running in testing mode or for real.
        """

        # Initialize fields
        self.reddit = reddit
        self.test_mode = test_mode
        self.features = []
        self.test_mode = test_mode
        self.subreddit_name = "InsiderMemeBot_Test" if test_mode else "InsiderMemeTrading"
        self.subreddit = self.reddit.subreddit(self.subreddit_name)
        self.my_id = self.reddit.user.me().id

        print("User: "******"Subreddit: " + self.subreddit_name)

        self.data_access = DataAccess(test_mode)

        # Store the IDs of the last 1000 comments that the feature has processed.
        # For efficiency the IDs are stored twice, in two different orders.
        # -  One set is a simple list that keeps the comments in the order they were processed, so that we can
        #    easily know which comment to pop off the collection once 1000 comments are exceeded.
        #
        # -  One set is sorted by the hash of the comment. This makes it easy to determine if a
        #    given comment has already been processed, without having to iterate over the entire list.
        #
        # These collections shouldn't be used directly by implementing classes.
        self.__processed_ids_by_time = []
        self.__processed_ids_by_hash = SortedSet()
        # Initialize the features
        self.init_features()

        print("Initialization Complete")
コード例 #4
0
class InboxListener:
    """
    This class continuously monitors the bot's inbox and processes messages.
    """
    ID_STORE_LIMIT = 1000 # The number of recent comment/submission IDs stored by the listener

    def __init__(self, reddit, test_mode):
        self.reddit = reddit
        self.test_mode = test_mode
        self.data_access = DataAccess(test_mode)
        self.my_id = self.reddit.user.me().id

        self.subreddit_name = "InsiderMemeBot_Test" if test_mode else "InsiderMemeTrading"
        self.subreddit = self.reddit.subreddit(self.subreddit_name)

    def run(self):
        """
        Runs the inbox listener
        """
        while True:
            try:
                for item in self.reddit.inbox.unread(limit=None):
                    if isinstance(item, praw.models.Message):
                        try:
                            self.process_message(item)
                            item.mark_read()
                        except Exception as e:
                            print("Error processing message: " + str(item))
                            traceback.print_exc()
                time.sleep(1)
            except Exception as e:
                print("Error reading inbox: " + str(e))
                traceback.print_exc()

    def process_message(self, message):
        """
        Processes an unread Message
        """
        print("Received message: " + message.id)
        print("  Subject: " + message.subject)
        print("  Author: " + message.author.name)
        print("  Body: " + message.body)

        # Regular expression for moderator response to a fulfilled template request
        fulfilled_response_subject_regex = r"re: Fulfilled template request <(\S+),(\S+)>"
        match = re.match(fulfilled_response_subject_regex, message.subject)
        if match is not None:            
            # Get the comment ID from the subject line that the command is referring to
            comment_id = match.groups()[0]
            imt_submission_id = match.groups()[1]
            self.__process_fulfilled_template_request_mod_reply(comment_id, imt_submission_id, message)

    def __process_fulfilled_template_request_mod_reply(self, comment_id, imt_submission_id, message):
        """
        Helper method for process_message. Processes a message that is a reply
        from a moderator about a submitted template for a template request

        comment_id: The ID of the comment for the submitted feature on the bot's feature request post
        imt_submission_id: The Submission ID for the Template Request post on IMT
        """

        # Validation Step 1: Make sure the author is actually a moderator
        if not RedditUtils.is_moderator(self.subreddit, message.author):
            return # Don't respond at all if the user isn't a moderator

        # Validation Step 2: Get the comment, and make sure it hasn't been deleted by the author
        comment = self.reddit.comment(id=comment_id)
        if comment is None or comment.author is None:
            # The comment was deleted
            msg = "The comment for this template request has been deleted, and can no longer be processed."
            message.reply(msg)
            return

        # Validation Step 3: Make sure that the comment ID exists as an
        # active request in the database. (If another mod had already approved the submission,
        # then it would have been removed)
        active_request_dict = self.data_access.get_variable("templaterequest_active_requests")

        # Note: The dictionary is keyed by the Submission ID of the posts in which there has been a request, not on the
        # submission ID of the corresponding bot post on IMT for the request.
        request_info_key = None
        for submission_id in active_request_dict: 
            info_dict = active_request_dict[submission_id]
            if info_dict['imt_request_submission_id'] == imt_submission_id:
                request_info_key  = submission_id
        
        if request_info_key == None:
            # Inform the moderator that the template request is no longer active.
            msg = "This template request is no longer active. This could mean that the submitted template has already been approved or rejected " + \
            "by another moderator, or that the template request has been fulfilled by a different user. " + \
            "Please contact the bot team if you believe this is in error."
            message.reply(msg)
            return

        # Validation Step 4: Make sure that the command in the body is valid
        command_text = message.body.strip()

        if command_text =="!approve":
            self.__process_template_approval(request_info_key, comment, message, False)
        elif command_text == "!approve -all":
            self.__process_template_approval(request_info_key, comment, message, True)
        elif command_text.startswith("!reject"):
            # See if there is a message provided
            custom_reply = ""
            custom_reply_regex = r"!reject\s+-message\s+(.+)"
            match = re.match(custom_reply_regex, command_text)
            if match != None:
                custom_reply = match.groups()[0]
            self.__process_template_rejection(comment, message, custom_reply)
        else:
            # Inform the moderator that the command was invalid.
            msg = "I could not understand your command. Accepted commands are:\n\n" + \
            "`!approve`: To approve this template\n\n" + \
            "`!approve -all`: To approve this template, and all future templates submitted by this user\n\n" + \
            "`!reject`: To reject this template\n\n" + \
            "`!reject -message <Message Text>`: To reject the template and add a message with an explanation"
            message.reply(msg)
            return


    def __process_template_approval(self, request_key, comment, message, approve_all_future=False):
        """
        Helper method for approving a template

        request_key: The dictionary key for the request in the templaterequest_active_requests map
        comment: The Comment where the user provided the requested template
        message: The approval message from the moderator that the bot will reply to
        approve_all_future: Whether or not to automatically approve all future requests by this user
        """

        ####  Update Database ####

        # Add tuple of the comment ID and the key in the active_requests map to the approved_requests list
        item_pair = [comment.id, request_key]
        item_key = {'key' : 'templaterequest_approved_requests'}
        item_update_expr = "SET val = list_append(val, :i)"
        item_expr_attrs = {':i' : [item_pair]}
        self.data_access.update_item(DataAccess.Tables.VARS, item_key, item_update_expr, item_expr_attrs)

        if approve_all_future:
            # Add the user to the approved users list
            item_key = {'key' : 'templaterequest_approved_users'}
            item_update_expr = "SET val = list_append(val, :i)"
            item_expr_attrs = {':i' : [comment.author.id]}
            self.data_access.update_item(DataAccess.Tables.VARS, item_key, item_update_expr, item_expr_attrs)

        #### Reply to moderator message ####
        message_reply = "Thank you, the template has been approved and will be processed shortly."
        if approve_all_future:
            message_reply += " u/" + comment.author.name + " has been added to the list of approved template providers."

        message.reply(message_reply)



    def __process_template_rejection(self, comment, message, custom_reply=""):
        """
        Helper method for rejecting a template

        comment: The Comment where the user provided the requested template
        message: The approval message from the moderator that the bot will reply to
        custom_reply: A custom reply from the moderator explaining why an example was rejected
        """

        # Add tuple of the comment ID, the key in the active_requests map, and the custom message
        # to the rejected_requests list
        item_list = [comment.id, custom_reply]
        item_key = {'key' : 'templaterequest_rejected_requests'}
        item_update_expr = "SET val = list_append(val, :i)"
        item_expr_attrs = {':i' : [item_list]}
        self.data_access.update_item(DataAccess.Tables.VARS, item_key, item_update_expr, item_expr_attrs)

        ### Reply to the moderator message ###
        message.reply("Thank you, the template has been rejected and will be processed shortly.")
コード例 #5
0
ファイル: Tracker.py プロジェクト: poopgoose/InsiderMemeBot
    def __init__(self, reddit, test_mode):
        self.reddit = reddit
        self.test_mode = test_mode
        self.data_access = DataAccess(test_mode)

        self.tracked_items = []
コード例 #6
0
ファイル: Tracker.py プロジェクト: poopgoose/InsiderMemeBot
class Tracker:
    """
    This class continuously tracks items in the Tracking Database, and
    updates them with scores retrieved from PRAW
    """
    def __init__(self, reddit, test_mode):
        self.reddit = reddit
        self.test_mode = test_mode
        self.data_access = DataAccess(test_mode)

        self.tracked_items = []

    def run(self):
        """
        Runs the tracker
        """
        self.__start__()

        while (True):
            self.__update__()
            time.sleep(1)

    ######### Private helper functions ###########

    def __start__(self):
        """
        Begins the tracker
        """
        print("*" * 40)
        print("RUNNING TRACKER")
        print("Tracking Database: " +
              str(self.data_access.tracking_table.name))
        print("*" * 40)

    def __update__(self):
        """
        Updates the items being tracked
        """
        cur_time = int(time.time())

        # Get the items in the database being tracked
        tracked_items = []
        try:
            response = self.data_access.scan(DataAccess.Tables.TRACKING)
            for item in response['Items']:
                tracked_items.append(item)
        except Exception as e:
            print("Could not load tracking data!")
            print(e)
            return

        for item in tracked_items:
            self.__update_item__(item)

        end_time = int(time.time())
        time_elapsed = end_time - cur_time

        print("=" * 40)
        print("Update cycle time: " + str(time_elapsed) + " seconds")
        print("=" * 40)

    def __update_item__(self, item):
        """
        Updates a single tracked item
        """
        begin_time = time.time()
        try:

            if not 'expire_time' in item:
                """
                In the case that the tracker adds an item at the same time as one is removed by InsiderMemeBot, it is
                possible that a remnant will remain, with only the "submission_id", "last_update", and "score" fields defined.

                This check catches any such instance, as all non-deleted items will have the "expire_time" field defined.
                If an entry without this field is found, it should simply be removed from the database since InsiderMemeBot is 
                finished with it.
                """
                self.data_access.delete_item(
                    DataAccess.Tables.TRACKING,
                    {'submission_id': item['submission_id']})
                print("Removing invalid item: " + str(item))
                return

            submission_id = item['submission_id']
            expire_time = decimal.Decimal(item['expire_time'])
            last_update = decimal.Decimal(
                0) if not 'last_update' in item else decimal.Decimal(
                    item['last_update'])

            submission = self.reddit.submission(id=submission_id)
            new_score = decimal.Decimal(submission.score)
            update_time = decimal.Decimal(int(time.time()))
            author = '[None]' if submission.author == None else submission.author.name

            key = {'submission_id': submission_id}
            update_expr = 'set last_update = :update, score = :score'
            expr_vals = {':update': update_time, ':score': new_score}

            # Make sure the item still exists since we added it to the list of items to update.
            # If it's been removed from the tracking database, then we don't need to update it anymore
            key_condition_expr = Key('submission_id').eq(submission_id)
            response = self.data_access.query(DataAccess.Tables.TRACKING,
                                              key_condition_expr)
            item_exists = len(response['Items']) == 1

            if item_exists:
                self.data_access.update_item(DataAccess.Tables.TRACKING, key,
                                             update_expr, expr_vals)
                end_time = time.time()
                update_duration = round(end_time - begin_time, 2)

            else:
                print("Submission has been removed from tracking: " +
                      str(submission_id))
        except Exception as e:
            print("Failed to update submission: " + submission_id)
            print(e)
コード例 #7
0
class TemplateRequestListener:
    """
    This class continuously monitors subreddits for "!IMTRequest" commands
    """
    ID_STORE_LIMIT = 1000  # The number of recent comment/submission IDs stored by the listener

    def __init__(self, reddit, test_mode):
        self.reddit = reddit
        self.test_mode = test_mode
        self.data_access = DataAccess(test_mode)
        self.my_id = self.reddit.user.me().id

        self.imt_subreddit_name = "InsiderMemeBot_Test" if test_mode else "InsiderMemeTrading"
        self.imt_subreddit = self.reddit.subreddit(self.imt_subreddit_name)

        # Store the IDs of the last 1000 comments that the RequestListener has processed.

        # For efficiency the IDs are stored twice, in two different orders.
        # -  One set is a simple list that keeps the comments in the order they were processed, so that we can
        #    easily know which comment to pop off the collection once 1000 comments are exceeded.
        #
        # -  One set is sorted by the hash of the comment. This makes it easy to determine if a
        #    given comment has already been processed, without having to iterate over the entire list.
        #
        # These collections shouldn't be used directly by implementing classes.
        self.__processed_ids_by_time = []
        self.__processed_ids_by_hash = SortedSet()

    def run(self):
        """
        Runs the template request listener
        """
        self.__start__()

        while True:

            for sub in self.subreddits:

                ### Get new comments and process them ###
                for comment in sub.comments(limit=10):
                    try:
                        if self.is_processed_recently(comment):
                            continue
                        # Ignore own comments, old comments, and comments already replied to
                        elif comment.author.id == self.my_id or self.is_old(
                                comment) or self.did_reply(comment):
                            self.mark_item_processed(comment)
                            continue

                        if comment.body.strip().lower().startswith(
                                "!imtrequest"):
                            self.process_request(comment)

                        self.mark_item_processed(comment)

                    except Exception as e:
                        print("Unable to process comment: " + str(comment))
                        print(e)
                        traceback.print_exc()
                        self.mark_item_processed(comment)

            time.sleep(1)

    def process_request(self, request_comment):

        active_requests = self.data_access.get_variable(
            "templaterequest_active_requests")
        pending_requests = self.data_access.get_variable(
            "templaterequest_pending_requests")
        #fulfilled_requests = self.data_access.get_variable("templaterequest_filled_requests") TODO
        fulfilled_requests = []

        comment_id = request_comment.id
        submission_id = request_comment.submission.id

        # Case 1: There is already a pending request for the requested template
        if submission_id in pending_requests:
            request_dict = pending_requests[submission_id]
            request_dict["requestor_ids"].append(request_comment.author.id)
            request_dict["requestor_names"].append(request_comment.author.name)
            request_dict["requestor_comments"].append(request_comment.id)
            pending_requests[submission_id] = request_dict

            self.data_access.set_variable("templaterequest_pending_requests",
                                          pending_requests)

        # Case 2: There is already an active request for the template
        elif submission_id in active_requests:
            imt_permalink = active_requests[submission_id][
                "imt_request_permalink"]
            # Respond to the comment
            request_comment.reply(
                "There is already an open request for this template. I will notify you when it is fullfiled!" + \
                "You can track the request for this template [here](" + imt_permalink + ")")

        # Case 3: There is a completed request for the requested template
        elif submission_id in fulfilled_requests:
            pass  # TODO
        # Case 4: This is a new request
        else:
            request_dict = {
                "requestor_ids": [request_comment.author.id],
                "requestor_names": [request_comment.author.name],
                "requestor_comments": [request_comment.id],
                "created_utc": decimal.Decimal(request_comment.created_utc),
                "permalink": request_comment.permalink,
                "submission_title": request_comment.submission.title,
                "subreddit_name": request_comment.subreddit.display_name
            }

            pending_requests[
                submission_id] = request_dict  # Key by the submission ID

            self.data_access.set_variable("templaterequest_pending_requests",
                                          pending_requests)

            print("=" * 40)
            print("TemplateRequestListener: Received request\n")
            print("Permalink: " + str(request_comment.permalink))
            print("Author: " + str(request_comment.author))
            print("=" * 40)

    def __start__(self):
        """
        Performs initialization
        """

        # Get the subreddits to listen on
        self.subreddit_names = self.data_access.get_variable(
            "templaterequest_monitored_subreddits")
        print("=" * 40)
        print("Starting Template Request Listener\n")
        print("Monitored subreddits:")
        for sub in self.subreddit_names:
            print("    r/" + sub)

        self.subreddits = []
        for sub in self.subreddit_names:
            self.subreddits.append(self.reddit.subreddit(sub))

    def did_reply(self, comment):
        """
        Returns whether or not the given comment contains a reply from InsiderMemeBot
        """
        comment.refresh()
        replies = comment.replies

        replies.replace_more(limit=None)
        for reply in replies:
            if reply.author.id == self.reddit.user.me().id:
                return True
        return False

    def is_processed_recently(self, obj):
        """
        Returns whether or not the given submission or comment ID was processed by the Feature (Not the entire bot)
        within the previous 1000 comments
        
        obj: The Comment or Submission that we are testing whether or not was processed
        """
        return obj.id in self.__processed_ids_by_hash

    def mark_item_processed(self, item):
        """ Marks that the item has been processed by the feature
        
        item: The Comment or Submission that has been processed
        """

        if item.id in self.__processed_ids_by_hash:
            # The edge case is if we're replying to a comment that's already been replied to.
            # Having duplicate comment IDs in the collections can make things messy, so we don't want to add it again.
            # In this case, we will have to traverse the entire comments_by_time collection ( O(n) ) to find and remove the comment
            self.__processed_ids_by_time.remove(item.id)
            self.__processed_ids_by_time.append(
                item.id)  # Add it back on the end
        else:
            # This is the usual case, where a comment hasn't already been replied to.
            self.__processed_ids_by_time.append(item.id)
            self.__processed_ids_by_hash.add(item.id)

        if len(self.__processed_ids_by_time
               ) > TemplateRequestListener.ID_STORE_LIMIT:
            oldest_id = self.__processed_ids_by_time.pop(
                0)  # Pop oldest comment at 0
            self.__processed_ids_by_hash.remove(oldest_id)

    def is_old(self, obj):
        """
        Returns true if the given comment or submission is more than 10 minutes old.
        """

        time_diff = timedelta(
            seconds=(datetime.now() -
                     datetime.fromtimestamp(obj.created_utc)).total_seconds())

        return time_diff.seconds // 60 > 10