Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- import sys
- import praw
- import prawcore
- import time
- import logging
- import config
- import dataset
- import pymongo
- from datetime import datetime
- from collections import deque
- logger = logging.getLogger()
- handler = logging.StreamHandler(sys.stdout)
- formatter = logging.Formatter(
- '%(asctime)s %(levelname)-8s %(message)s')
- handler.setFormatter(formatter)
- logger.addHandler(handler)
- logger.setLevel(logging.INFO)
- # Reddit app API login creds
- username = config.username
- r = praw.Reddit(client_id=config.client_id,
- client_secret=config.client_secret,
- user_agent=config.user_agent,
- username=config.username,
- password=config.password,
- timeout=60)
- subname = config.subname
- subreddit = r.subreddit(subname)
- REMOVAL_PERIOD_MINUTES = 0.2
- REINSTATE_PERIOD_MINUTES = 30
- ANSWER_MINIMUM = 20
- ANSWER_PHRASE_MINIMUM = 60
- REMOVAL_PHRASES = []
- POST_FETCH_LIMIT = 500
- POST_DB_PRUNE_MINUTES = 1440
- message_title = config.message_title
- message_body = config.message_body
- retry_message_title = config.retry_message_title
- retry_message_body = config.retry_message_body
- sticky_comment = config.sticky_comment
- timeout_message_title = config.timeout_message_title
- timeout_message_body = config.timeout_message_body
- # Keep track of the last 1,000 posts and users to avoid reprocessing
- post_history = deque([])
- user_history = deque([])
- # Create list of sub moderators to ignore
- moderators = []
- for mod in subreddit.moderator():
- moderators.append(mod.name)
- # Database structure
- # Table posts
- # -----------
- # --> id
- # --> created
- # --> user
- # --> prompted
- # --> removed
- # --> replied
- db = dataset.connect('sqlite:///questionbot.db')
- post_table = db['posts']
- # Load config values from AITP wiki, fall back to defaults
- def load_config():
- global REMOVAL_PERIOD_MINUTES, REINSTATE_PERIOD_MINUTES, REMOVAL_PHRASES, ANSWER_MINIMUM, ANSWER_PHRASE_MINIMUM, POST_FETCH_LIMIT, POST_DB_PRUNE_MINUTES, CONTEST_MODE_HOURS
- wikipage = r.subreddit("YOUR SUBREDDIT NAMe GOES HERE").wiki["botconfig"]
- for line in wikipage.content_md.split("\n"):
- if "REMOVAL_PERIOD_MINUTES" in line.upper():
- REMOVAL_PERIOD_MINUTES = float(line.partition(":")[2].lstrip())
- elif "REINSTATE_PERIOD_MINUTES" in line.upper():
- REINSTATE_PERIOD_MINUTES = float(line.partition(":")[2].lstrip())
- elif "REMOVAL_PHRASES" in line.upper():
- phrase_string = line.partition(":")[2].lstrip().partition("[")[2].partition("]")[0]
- REMOVAL_PHRASES = list(phrase_string.replace(", ", ",").split(","))
- elif "ANSWER_MINIMUM" in line.upper():
- ANSWER_MINIMUM = int(line.partition(":")[2].lstrip())
- elif "ANSWER_PHRASE_MINIMUM" in line.upper():
- ANSWER_PHRASE_MINIMUM = int(line.partition(":")[2].lstrip())
- elif "POST_FETCH_LIMIT" in line.upper():
- POST_FETCH_LIMIT = int(line.partition(":")[2].lstrip())
- elif "POST_DB_PRUNE_MINUTES" in line.upper():
- POST_DB_PRUNE_MINUTES = int(line.partition(":")[2].lstrip())
- elif "CONTEST_MODE_HOURS" in line.upper():
- CONTEST_MODE_HOURS = float(line.partition(":")[2].lstrip())
- # Calculates the age in hours of a reddit submission
- def get_age(created_utc):
- t = datetime.now()
- utc_seconds = time.mktime(t.timetuple())
- minutes = round((utc_seconds - created_utc) / 60, 2)
- return minutes
- # Determines whether bot user has already replied to a reddit submission or comment
- def replied(item):
- replies = ""
- if "_replies" in vars(item).keys():
- replies = item.replies
- else:
- replies = item.comments
- for reply in replies:
- if reply.author and reply.author.name.lower() == username.lower():
- return True
- return False
- # Return whether user has a post to the subreddit in the post DB
- def has_approved_post(post_user):
- for i, user in enumerate(user_history):
- if user == post_user:
- post = r.submission(post_history[i])
- if post.approved:
- return True
- return False
- # Check user history for a post created within the reinstatement period
- # Necessary if DB is missing a post
- def user_has_recent_post(user):
- try:
- for post in user.new(limit=20):
- if post.subreddit == subreddit and get_age(post.created_utc) <= REINSTATE_PERIOD_MINUTES:
- return True
- except prawcore.exceptions.Forbidden as e:
- logging.error("Failed to get posts for %s: %s" % (user.name, str(e)))
- return False
- return False
- # Look up a reddit post in DB by ID
- def get_post(submission):
- entry = post_table.find_one(post_id=submission.id)
- return entry
- # Send user message requesting their answer
- def prompt_user(submission):
- post = post_table.find_one(post_id=submission.id)
- # Only send to users we have not already messaged
- if not post['prompted']:
- user = submission.author
- prompt_message_body = message_body.replace("{{post}}", submission.shortlink)
- try:
- user.message(subject=message_title, message=prompt_message_body)
- except Exception as e:
- logging.error("Failed to message %s: %s" % (user.name, str(e)))
- post_table.update(dict(id=post['id'], prompted=True), ['id'])
- # Send follow-up message
- def retry_prompt_user(user):
- r.redditor(user).message(subject=retry_message_title, message=retry_message_body)
- # Write user xp to database manually
- def add_post(submission):
- insertion = None
- try:
- insertion = post_table.insert(dict(
- post_id=submission.id,
- user=submission.author.name,
- created=submission.created_utc,
- prompted=False,
- removed=False,
- replied=False,
- reports=submission.num_reports
- ))
- except Exception as e:
- logging.error("Failed to add to DB: %s" % str(e))
- if not insertion:
- logging.error("Post failed to insert %s " % submission.shortlink)
- # Delete post from DB
- def db_delete_post(post):
- post_to_delete = post_table.find_one(post_id=post["post_id"])
- if not post_to_delete:
- return
- deleted = post_table.delete(id=post_to_delete['id'])
- if not deleted:
- logging.warning("Failed to delete %s" % post["post_id"])
- # Remove user's old posts from DB when new post is posted
- def db_clear_user_posts(user):
- user_post = post_table.find_one(user=user)
- if user_post:
- logging.info("Clearing DB for %s" % submission.author.name)
- post_table.delete(user=user)
- # Remove posts over removal threshold
- # Remove posts from DB over reinstatement threshold
- def process_post_queue():
- for post in post_table:
- elapsed_time = get_age(post['created'])
- # Remove posts older than removal threshold
- if elapsed_time >= REMOVAL_PERIOD_MINUTES and not post['removed']:
- submission = r.submission(post['post_id'])
- logging.info("Removing %s (%s)" % (post['user'], submission.shortlink))
- submission.mod.remove()
- post_table.update(dict(id=post['id'], removed=True), ['id'])
- # Remove posts from DB older than prune threshold
- if elapsed_time >= POST_DB_PRUNE_MINUTES:
- logging.info("Deleting from DB %s (%s) (time limit reached: %s mins)" % (post['user'], post['post_id'], elapsed_time))
- db_delete_post(post)
- # Leave the user answer as a stickied comment on the post
- # The contest mode code is for variable contest mode and can be removed
- def post_user_answer(submission, answer):
- bot_comment = sticky_comment.replace("{{answer}}", answer).replace("[CONTEST_MODE_HOURS]", str(CONTEST_MODE_HOURS))
- if not submission["replied"]:
- submission = r.submission(submission['post_id'])
- comment = submission.reply(bot_comment)
- comment.mod.distinguish(sticky=True)
- comment.mod.lock()
- # Reinstate removed post from DB
- def approve_post(post):
- submission = r.submission(post['post_id'])
- submission.mod.approve()
- # Add automod reports to the approved post
- if post.get('reports') and post['reports'] > 0:
- logging.info("Reporting %s" % post['post_id'])
- submission.report("Warning: Check automod queue for auto report")
- # Check PMs to process answers
- def process_inbox():
- inbox = r.inbox.unread()
- for message in inbox:
- # Do not process if user deleted account
- if not message.author or "[deleted]" in message.author.name:
- continue
- user = message.author.name
- answer = message.body
- user_post = None
- user_post = post_table.find_one(user=user)
- message_age_minutes = get_age(message.created_utc)
- # Time delta is period between when post was created and when user answered the message
- message_time_delta = 999
- if user_post:
- message_time_delta = round(get_age(user_post['created']) - message_age_minutes, 2)
- # If user responds outside of window and post was not approved, send timeout message
- # First conditional set is for posts no longer in DB and checks the user history
- # Second conditional is for users with posts still in DB
- if (not user_post \
- and (message_title in message.subject or retry_message_title in message.subject) \
- and not has_approved_post(message.author) \
- and not user_has_recent_post(message.author)) \
- or (user_post \
- and message_time_delta >= REINSTATE_PERIOD_MINUTES \
- and not user_post['replied']):
- logstring = "Got timed out response from %s" % user
- if user_post:
- logstring += (" (%s mins)" % message_time_delta)
- logging.info(logstring)
- try:
- message.author.message(subject=timeout_message_title, message=timeout_message_body)
- except Exception as e:
- logging.error("Failed to message %s: %s" % (user, str(e)))
- r.inbox.mark_read([message])
- continue
- # Ignore messages not in reply to bot
- elif not user_post and not user_has_recent_post(message.author):
- logging.info("Got random message from %s" % user)
- r.inbox.mark_read([message])
- continue
- # User has no existing post, do nothing
- elif not user_post:
- logging.info("Cound not find post for %s" % user)
- r.inbox.mark_read([message])
- continue
- # Check for lazy answers
- if len(answer) < ANSWER_MINIMUM:
- logging.info("Got short message from %s, reprompting" % user)
- retry_prompt_user(user)
- # Check for lazy answers with unacceptable phrases
- elif len(answer) < ANSWER_PHRASE_MINIMUM:
- for phrase in REMOVAL_PHRASES:
- if phrase.lower() in answer.lower():
- logging.info("Got short message with phrase from %s, reprompting" % user)
- retry_prompt_user(user)
- break
- # Standard answer scenario
- else:
- logging.info("Answer from %s (%s mins)" % (user, message_time_delta))
- if user_post['removed'] and message_time_delta <= REINSTATE_PERIOD_MINUTES:
- post_user_answer(user_post, answer)
- post_table.update(dict(id=user_post['id'],
- post_id=user_post["post_id"],
- replied=True),
- ['id'])
- logging.info("Approving %s (%s)" % (user, user_post["post_id"]))
- approve_post(user_post)
- logging.info("Deleting %s (%s) from DB (answer completed)" % (user, user_post["post_id"]))
- db_delete_post(user_post)
- logging.info("Messaging user approval")
- message.reply("\
- Your response to judgment bot has been accepted and your post is live! Below is some important information to review before interacting with your post. \n\n\
- \
- Don't panic because you aren't seeing any comments right away! You will see all of the comments users have left in 30 minutes. We've implemented a feature we call Ultimate Contest Mode to ensure you get the best, most thoughtful, and most diverse comments. This involves hiding all comments for the first hour a post is live and then revealing them all when that hour is expired. After this period everything functions as usual. [See here for a further explanation] of this test and to offer your feedback.\n\n\
- \
- You might see our bot message you during this time with any clarifying questions users ask or requests for important information they feel they need to base their judgment on. This is a good opportunity for you to ensure you're including any details you think would be important in your post or that would help users to provide judgments you would find the most valuable.\n\n\
- \
- **Rule 4:** /r/AmITheAsshole is an extremely public forum. Millions of people read it and multiple media sources steal stories for their own content. If you aren't prepared for this possibility delete your post before the conversation starts. Once people start engaging [rule 4: never delete an active discussion](https://www.reddit.com/r/AmItheAsshole/about/rules/) applies.\n\n \
- \
- **Rule 3:** Keep in mind that this is a place to gather feedback from impartial third parties, not a place to defend your actions. [You can read more about rule 3 here](https://www.reddit.com/r/AmItheAsshole/wiki/faq#wiki_rule_3.3A_accept_your_judgement) \n\n\
- \
- **Rule 1:** While we strive to keep the conversations here [civil](https://www.reddit.com/r/AmItheAsshole/wiki/faq#wiki_civility) some people are the asshole and aren't civil. Occasionally it can be extreme. Use the report button and block as you see fit. We review every reported comment we get, and the admins review reported PMs. You can also disable inbox notifications using that link under your post. \n\n\
- \
- [Full Rules](https://www.reddit.com/r/AmItheAsshole/about/rules)")
- # Add post to active post DB for ultra contest mode
- #add_active_post(user_post)
- r.inbox.mark_read([message])
- # Print all rows in post database
- def print_post_db():
- for post in post_table:
- print("%s %s" % (post['user'], post['post_id']))
- # Delete all entries from post database
- def clear_post_db():
- for post in post_table:
- r.submission(post['post_id']).mod.approve()
- db_delete_post(post)
- # Main entry point
- if __name__ == "__main__":
- try:
- load_config()
- logging.info("Loaded config successfully")
- except Exception as e:
- logging.error("Failed to load config, using default values: %s" % str(e))
- try:
- while True:
- # Fetch new submissions to subreddit
- for submission in subreddit.new(limit=POST_FETCH_LIMIT):
- # Ignore mod posts
- if submission.distinguished or (submission.author and submission.author.name in moderators):
- continue
- # Ignore fully processed posts
- if submission.id in post_history:
- continue
- # Ignore manually approved posts
- if submission.approved:
- continue
- # Ignore old posts
- if get_age(submission.created_utc) > REINSTATE_PERIOD_MINUTES:
- continue
- # Ignored replied to posts
- if replied(submission):
- continue
- # Ignore update posts
- if "update" in submission.title.lower():
- continue
- # Check for post in database
- post_data = get_post(submission)
- # Process brand new post
- if not post_data and submission.author.name:
- # Remove user's outstanding posts from DB
- db_clear_user_posts(submission.author.name)
- logging.info("Adding to DB %s (%s)" % (submission.author.name, submission.shortlink))
- # Add new post to DB
- add_post(submission)
- logging.info("Prompting %s (%s)" % (submission.author.name, submission.shortlink))
- # Send user question prompt
- prompt_user(submission)
- post_history.append(submission.id)
- user_history.append(submission.author)
- # Check existing posts for removals or reinstatements
- process_post_queue()
- # Check messages for user answers
- process_inbox()
- # Prune post queues
- if len(post_history) > 1000:
- post_history.popleft()
- user_history.popleft()
- except prawcore.exceptions.ServerError as e:
- logging.error("PRAW Error: %s" % str(e))
- time.sleep(10)
- except prawcore.exceptions.RequestException as e:
- logging.error("PRAW ERROR: %s" % str(e))
- time.sleep(10)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement