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 __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 __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")
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.")
def __init__(self, reddit, test_mode): self.reddit = reddit self.test_mode = test_mode self.data_access = DataAccess(test_mode) self.tracked_items = []
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)
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