Advertisement
Guest User

Untitled

a guest
Apr 1st, 2023
98
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.00 KB | None | 0 0
  1. #!/usr/bin/python3
  2. import sys
  3. import praw
  4. import prawcore
  5. import time
  6. import logging
  7. import config
  8. import dataset
  9. import pymongo
  10. from datetime import datetime
  11. from collections import deque
  12.  
  13. logger = logging.getLogger()
  14. handler = logging.StreamHandler(sys.stdout)
  15. formatter = logging.Formatter(
  16. '%(asctime)s %(levelname)-8s %(message)s')
  17. handler.setFormatter(formatter)
  18. logger.addHandler(handler)
  19. logger.setLevel(logging.INFO)
  20.  
  21. # Reddit app API login creds
  22. username = config.username
  23. r = praw.Reddit(client_id=config.client_id,
  24. client_secret=config.client_secret,
  25. user_agent=config.user_agent,
  26. username=config.username,
  27. password=config.password,
  28. timeout=60)
  29.  
  30. subname = config.subname
  31. subreddit = r.subreddit(subname)
  32.  
  33. REMOVAL_PERIOD_MINUTES = 0.2
  34. REINSTATE_PERIOD_MINUTES = 30
  35. ANSWER_MINIMUM = 20
  36. ANSWER_PHRASE_MINIMUM = 60
  37. REMOVAL_PHRASES = []
  38. POST_FETCH_LIMIT = 500
  39. POST_DB_PRUNE_MINUTES = 1440
  40. message_title = config.message_title
  41. message_body = config.message_body
  42. retry_message_title = config.retry_message_title
  43. retry_message_body = config.retry_message_body
  44. sticky_comment = config.sticky_comment
  45. timeout_message_title = config.timeout_message_title
  46. timeout_message_body = config.timeout_message_body
  47. # Keep track of the last 1,000 posts and users to avoid reprocessing
  48. post_history = deque([])
  49. user_history = deque([])
  50.  
  51. # Create list of sub moderators to ignore
  52. moderators = []
  53. for mod in subreddit.moderator():
  54. moderators.append(mod.name)
  55.  
  56. # Database structure
  57. # Table posts
  58. # -----------
  59. # --> id
  60. # --> created
  61. # --> user
  62. # --> prompted
  63. # --> removed
  64. # --> replied
  65. db = dataset.connect('sqlite:///questionbot.db')
  66. post_table = db['posts']
  67.  
  68.  
  69. # Load config values from AITP wiki, fall back to defaults
  70. def load_config():
  71. global REMOVAL_PERIOD_MINUTES, REINSTATE_PERIOD_MINUTES, REMOVAL_PHRASES, ANSWER_MINIMUM, ANSWER_PHRASE_MINIMUM, POST_FETCH_LIMIT, POST_DB_PRUNE_MINUTES, CONTEST_MODE_HOURS
  72. wikipage = r.subreddit("YOUR SUBREDDIT NAMe GOES HERE").wiki["botconfig"]
  73. for line in wikipage.content_md.split("\n"):
  74. if "REMOVAL_PERIOD_MINUTES" in line.upper():
  75. REMOVAL_PERIOD_MINUTES = float(line.partition(":")[2].lstrip())
  76. elif "REINSTATE_PERIOD_MINUTES" in line.upper():
  77. REINSTATE_PERIOD_MINUTES = float(line.partition(":")[2].lstrip())
  78. elif "REMOVAL_PHRASES" in line.upper():
  79. phrase_string = line.partition(":")[2].lstrip().partition("[")[2].partition("]")[0]
  80. REMOVAL_PHRASES = list(phrase_string.replace(", ", ",").split(","))
  81. elif "ANSWER_MINIMUM" in line.upper():
  82. ANSWER_MINIMUM = int(line.partition(":")[2].lstrip())
  83. elif "ANSWER_PHRASE_MINIMUM" in line.upper():
  84. ANSWER_PHRASE_MINIMUM = int(line.partition(":")[2].lstrip())
  85. elif "POST_FETCH_LIMIT" in line.upper():
  86. POST_FETCH_LIMIT = int(line.partition(":")[2].lstrip())
  87. elif "POST_DB_PRUNE_MINUTES" in line.upper():
  88. POST_DB_PRUNE_MINUTES = int(line.partition(":")[2].lstrip())
  89. elif "CONTEST_MODE_HOURS" in line.upper():
  90. CONTEST_MODE_HOURS = float(line.partition(":")[2].lstrip())
  91.  
  92.  
  93. # Calculates the age in hours of a reddit submission
  94. def get_age(created_utc):
  95. t = datetime.now()
  96. utc_seconds = time.mktime(t.timetuple())
  97. minutes = round((utc_seconds - created_utc) / 60, 2)
  98. return minutes
  99.  
  100.  
  101. # Determines whether bot user has already replied to a reddit submission or comment
  102. def replied(item):
  103. replies = ""
  104. if "_replies" in vars(item).keys():
  105. replies = item.replies
  106. else:
  107. replies = item.comments
  108. for reply in replies:
  109. if reply.author and reply.author.name.lower() == username.lower():
  110. return True
  111. return False
  112.  
  113.  
  114. # Return whether user has a post to the subreddit in the post DB
  115. def has_approved_post(post_user):
  116. for i, user in enumerate(user_history):
  117. if user == post_user:
  118. post = r.submission(post_history[i])
  119. if post.approved:
  120. return True
  121. return False
  122.  
  123.  
  124. # Check user history for a post created within the reinstatement period
  125. # Necessary if DB is missing a post
  126. def user_has_recent_post(user):
  127. try:
  128. for post in user.new(limit=20):
  129. if post.subreddit == subreddit and get_age(post.created_utc) <= REINSTATE_PERIOD_MINUTES:
  130. return True
  131. except prawcore.exceptions.Forbidden as e:
  132. logging.error("Failed to get posts for %s: %s" % (user.name, str(e)))
  133. return False
  134. return False
  135.  
  136.  
  137. # Look up a reddit post in DB by ID
  138. def get_post(submission):
  139. entry = post_table.find_one(post_id=submission.id)
  140. return entry
  141.  
  142.  
  143. # Send user message requesting their answer
  144. def prompt_user(submission):
  145. post = post_table.find_one(post_id=submission.id)
  146. # Only send to users we have not already messaged
  147. if not post['prompted']:
  148. user = submission.author
  149. prompt_message_body = message_body.replace("{{post}}", submission.shortlink)
  150. try:
  151. user.message(subject=message_title, message=prompt_message_body)
  152. except Exception as e:
  153. logging.error("Failed to message %s: %s" % (user.name, str(e)))
  154.  
  155. post_table.update(dict(id=post['id'], prompted=True), ['id'])
  156.  
  157.  
  158. # Send follow-up message
  159. def retry_prompt_user(user):
  160. r.redditor(user).message(subject=retry_message_title, message=retry_message_body)
  161.  
  162.  
  163. # Write user xp to database manually
  164. def add_post(submission):
  165. insertion = None
  166. try:
  167. insertion = post_table.insert(dict(
  168. post_id=submission.id,
  169. user=submission.author.name,
  170. created=submission.created_utc,
  171. prompted=False,
  172. removed=False,
  173. replied=False,
  174. reports=submission.num_reports
  175. ))
  176. except Exception as e:
  177. logging.error("Failed to add to DB: %s" % str(e))
  178.  
  179. if not insertion:
  180. logging.error("Post failed to insert %s " % submission.shortlink)
  181.  
  182.  
  183. # Delete post from DB
  184. def db_delete_post(post):
  185. post_to_delete = post_table.find_one(post_id=post["post_id"])
  186. if not post_to_delete:
  187. return
  188. deleted = post_table.delete(id=post_to_delete['id'])
  189. if not deleted:
  190. logging.warning("Failed to delete %s" % post["post_id"])
  191.  
  192.  
  193. # Remove user's old posts from DB when new post is posted
  194. def db_clear_user_posts(user):
  195. user_post = post_table.find_one(user=user)
  196. if user_post:
  197. logging.info("Clearing DB for %s" % submission.author.name)
  198. post_table.delete(user=user)
  199.  
  200.  
  201. # Remove posts over removal threshold
  202. # Remove posts from DB over reinstatement threshold
  203. def process_post_queue():
  204. for post in post_table:
  205. elapsed_time = get_age(post['created'])
  206. # Remove posts older than removal threshold
  207. if elapsed_time >= REMOVAL_PERIOD_MINUTES and not post['removed']:
  208. submission = r.submission(post['post_id'])
  209. logging.info("Removing %s (%s)" % (post['user'], submission.shortlink))
  210. submission.mod.remove()
  211. post_table.update(dict(id=post['id'], removed=True), ['id'])
  212. # Remove posts from DB older than prune threshold
  213. if elapsed_time >= POST_DB_PRUNE_MINUTES:
  214. logging.info("Deleting from DB %s (%s) (time limit reached: %s mins)" % (post['user'], post['post_id'], elapsed_time))
  215. db_delete_post(post)
  216.  
  217.  
  218. # Leave the user answer as a stickied comment on the post
  219. # The contest mode code is for variable contest mode and can be removed
  220. def post_user_answer(submission, answer):
  221. bot_comment = sticky_comment.replace("{{answer}}", answer).replace("[CONTEST_MODE_HOURS]", str(CONTEST_MODE_HOURS))
  222. if not submission["replied"]:
  223. submission = r.submission(submission['post_id'])
  224. comment = submission.reply(bot_comment)
  225. comment.mod.distinguish(sticky=True)
  226. comment.mod.lock()
  227.  
  228.  
  229. # Reinstate removed post from DB
  230. def approve_post(post):
  231. submission = r.submission(post['post_id'])
  232. submission.mod.approve()
  233. # Add automod reports to the approved post
  234. if post.get('reports') and post['reports'] > 0:
  235. logging.info("Reporting %s" % post['post_id'])
  236. submission.report("Warning: Check automod queue for auto report")
  237.  
  238.  
  239. # Check PMs to process answers
  240. def process_inbox():
  241. inbox = r.inbox.unread()
  242. for message in inbox:
  243. # Do not process if user deleted account
  244. if not message.author or "[deleted]" in message.author.name:
  245. continue
  246.  
  247. user = message.author.name
  248. answer = message.body
  249. user_post = None
  250. user_post = post_table.find_one(user=user)
  251. message_age_minutes = get_age(message.created_utc)
  252. # Time delta is period between when post was created and when user answered the message
  253. message_time_delta = 999
  254. if user_post:
  255. message_time_delta = round(get_age(user_post['created']) - message_age_minutes, 2)
  256.  
  257. # If user responds outside of window and post was not approved, send timeout message
  258. # First conditional set is for posts no longer in DB and checks the user history
  259. # Second conditional is for users with posts still in DB
  260. if (not user_post \
  261. and (message_title in message.subject or retry_message_title in message.subject) \
  262. and not has_approved_post(message.author) \
  263. and not user_has_recent_post(message.author)) \
  264. or (user_post \
  265. and message_time_delta >= REINSTATE_PERIOD_MINUTES \
  266. and not user_post['replied']):
  267. logstring = "Got timed out response from %s" % user
  268. if user_post:
  269. logstring += (" (%s mins)" % message_time_delta)
  270. logging.info(logstring)
  271. try:
  272. message.author.message(subject=timeout_message_title, message=timeout_message_body)
  273. except Exception as e:
  274. logging.error("Failed to message %s: %s" % (user, str(e)))
  275. r.inbox.mark_read([message])
  276. continue
  277.  
  278. # Ignore messages not in reply to bot
  279. elif not user_post and not user_has_recent_post(message.author):
  280. logging.info("Got random message from %s" % user)
  281. r.inbox.mark_read([message])
  282. continue
  283.  
  284. # User has no existing post, do nothing
  285. elif not user_post:
  286. logging.info("Cound not find post for %s" % user)
  287. r.inbox.mark_read([message])
  288. continue
  289.  
  290. # Check for lazy answers
  291. if len(answer) < ANSWER_MINIMUM:
  292. logging.info("Got short message from %s, reprompting" % user)
  293. retry_prompt_user(user)
  294.  
  295. # Check for lazy answers with unacceptable phrases
  296. elif len(answer) < ANSWER_PHRASE_MINIMUM:
  297. for phrase in REMOVAL_PHRASES:
  298. if phrase.lower() in answer.lower():
  299. logging.info("Got short message with phrase from %s, reprompting" % user)
  300. retry_prompt_user(user)
  301. break
  302.  
  303. # Standard answer scenario
  304. else:
  305. logging.info("Answer from %s (%s mins)" % (user, message_time_delta))
  306. if user_post['removed'] and message_time_delta <= REINSTATE_PERIOD_MINUTES:
  307. post_user_answer(user_post, answer)
  308. post_table.update(dict(id=user_post['id'],
  309. post_id=user_post["post_id"],
  310. replied=True),
  311. ['id'])
  312. logging.info("Approving %s (%s)" % (user, user_post["post_id"]))
  313. approve_post(user_post)
  314. logging.info("Deleting %s (%s) from DB (answer completed)" % (user, user_post["post_id"]))
  315. db_delete_post(user_post)
  316. logging.info("Messaging user approval")
  317. message.reply("\
  318. 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\
  319. \
  320. 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\
  321. \
  322. 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\
  323. \
  324. **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 \
  325. \
  326. **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\
  327. \
  328. **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\
  329. \
  330. [Full Rules](https://www.reddit.com/r/AmItheAsshole/about/rules)")
  331.  
  332. # Add post to active post DB for ultra contest mode
  333. #add_active_post(user_post)
  334.  
  335. r.inbox.mark_read([message])
  336.  
  337. # Print all rows in post database
  338. def print_post_db():
  339. for post in post_table:
  340. print("%s %s" % (post['user'], post['post_id']))
  341.  
  342.  
  343. # Delete all entries from post database
  344. def clear_post_db():
  345. for post in post_table:
  346. r.submission(post['post_id']).mod.approve()
  347. db_delete_post(post)
  348.  
  349.  
  350. # Main entry point
  351. if __name__ == "__main__":
  352. try:
  353. load_config()
  354. logging.info("Loaded config successfully")
  355. except Exception as e:
  356. logging.error("Failed to load config, using default values: %s" % str(e))
  357.  
  358. try:
  359.  
  360. while True:
  361.  
  362. # Fetch new submissions to subreddit
  363. for submission in subreddit.new(limit=POST_FETCH_LIMIT):
  364.  
  365. # Ignore mod posts
  366. if submission.distinguished or (submission.author and submission.author.name in moderators):
  367. continue
  368.  
  369. # Ignore fully processed posts
  370. if submission.id in post_history:
  371. continue
  372.  
  373. # Ignore manually approved posts
  374. if submission.approved:
  375. continue
  376.  
  377. # Ignore old posts
  378. if get_age(submission.created_utc) > REINSTATE_PERIOD_MINUTES:
  379. continue
  380.  
  381. # Ignored replied to posts
  382. if replied(submission):
  383. continue
  384.  
  385. # Ignore update posts
  386. if "update" in submission.title.lower():
  387. continue
  388.  
  389. # Check for post in database
  390. post_data = get_post(submission)
  391. # Process brand new post
  392. if not post_data and submission.author.name:
  393. # Remove user's outstanding posts from DB
  394. db_clear_user_posts(submission.author.name)
  395. logging.info("Adding to DB %s (%s)" % (submission.author.name, submission.shortlink))
  396. # Add new post to DB
  397. add_post(submission)
  398. logging.info("Prompting %s (%s)" % (submission.author.name, submission.shortlink))
  399. # Send user question prompt
  400. prompt_user(submission)
  401. post_history.append(submission.id)
  402. user_history.append(submission.author)
  403.  
  404.  
  405. # Check existing posts for removals or reinstatements
  406. process_post_queue()
  407. # Check messages for user answers
  408. process_inbox()
  409.  
  410. # Prune post queues
  411. if len(post_history) > 1000:
  412. post_history.popleft()
  413. user_history.popleft()
  414.  
  415. except prawcore.exceptions.ServerError as e:
  416. logging.error("PRAW Error: %s" % str(e))
  417. time.sleep(10)
  418.  
  419. except prawcore.exceptions.RequestException as e:
  420. logging.error("PRAW ERROR: %s" % str(e))
  421. time.sleep(10)
  422.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement