Advertisement
CrashARuntimeToday

VirtueTron9000 v0.3.8b

Jul 9th, 2018
260
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 20.24 KB | None | 0 0
  1. #!/usr/bin/python
  2. LICENCE = "WTFPL", "http://www.wtfpl.net/about/"
  3. VERSION = "v0.3.8b"
  4.  
  5. import logging
  6. import pickle
  7. import praw
  8. import prawcore
  9. import shelve
  10. import sys
  11. from datetime import datetime, timedelta
  12. from math import ceil
  13. from time import sleep
  14. from random import randint
  15. from statistics import mean
  16.  
  17. log = logging.getLogger("VirtueTron")
  18. log.setLevel(logging.DEBUG)
  19. formatter = logging.Formatter(fmt="%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y/%m/%d %I:%M:%S%p")
  20. file_log = logging.FileHandler(filename="VirtueTron.log", mode="a")
  21. file_log.setLevel(logging.INFO)
  22. file_log.setFormatter(formatter)
  23. log.addHandler(file_log)
  24. console_log = logging.StreamHandler(stream=sys.stdout)
  25. console_log.setLevel(logging.DEBUG)
  26. console_log.setFormatter(formatter)
  27. log.addHandler(console_log)
  28.  
  29. log.info("Hello, Reddit!")
  30. log.info("VirtueTron® 9000™ {0} © CrashARuntimeToday@outlook.com".format(VERSION))
  31.  
  32. credentials = pickle.load(open("credentials.pickle", "rb")) # { client_id: "VirtueTron9000", client_secret: "🤖🤡🍆💯™", username: "SignalAVirtueToday", password: "https://youtu.be/RCVJ7bujnSc"}
  33. credentials["user_agent"] = "VirtueTron 9000 {0}".format(VERSION)
  34. reddit = praw.Reddit(**credentials)
  35. tbp = reddit.subreddit("TheBluePill")
  36.  
  37. NICE_LIST = []
  38. NAUGHTY_LIST = ["theredpill", "marriedredpill", "mgtow", "braincels", "asktrp", "askmrp", "redpillwomen", "redpillwives", "cringeanarchy", "the_donald", "rpchristians", "pussypassdenied", "mensrights", "milliondollarextreme", "4chan", "whereareallthegoodmen"]
  39. IMMUTABLE_FLAIRS = ["vanguard", "vexatious", "endorsedflair", "alpha", "betaasfuck", "feeemale", "purged"]
  40. BAD_FLAIRS = ["purged", "vexatious"]
  41. GOOD_FLAIRS = ["endorsedflair", "vanguard", "alpha", "betaasfuck", "feeemale"]
  42. TODAYS_THREAT_LEVEL = {"tlsevere":"Severe", "tlhigh":"High", "tlelevated":"Elevated", "tlguarded":"Guarded", "tllow":"Low"}
  43. THREAT_MATRIX = {"tllow": BAD_FLAIRS + ["hb{0}".format(x) for x in range(1,7)],
  44.                  "tlguarded": BAD_FLAIRS + ["hb{0}".format(x) for x in range(1,5)],
  45.                  "tlelevated": BAD_FLAIRS + ["hb{0}".format(x) for x in range(1,3)],
  46.                  "tlhigh": BAD_FLAIRS,
  47.                  "tlsevere": []}
  48. FLAIR_EXEMPT = []
  49. SHIT_TESTS = {"no_e": {"message": "Your comments may no longer contain the letter 'e'.", "eval_func": lambda comment: "e" not in comment.body.lower()}}
  50.  
  51. delay = 2
  52. last_fuckup = None
  53. def praw_fucked_up():
  54.     global delay
  55.     global last_fuckup
  56.     log.warning("Reddit API error: waiting {0} seconds".format(delay))
  57.     try:
  58.         if delay > 128 or datetime.now() > last_fuckup + timedelta(minutes=2): delay = 2
  59.     except TypeError:
  60.         delay = 2
  61.     sleep(delay)
  62.     delay *= 2
  63.     last_fuckup = datetime.now()
  64.  
  65. def get_flair(name):
  66.     while True:
  67.         try:
  68.             for flair in tbp.flair(redditor=name):
  69.                 return flair["flair_css_class"]
  70.         except prawcore.PrawcoreException:
  71.             praw_fucked_up()
  72.  
  73. def set_flair(name, flair_text, flair_css):
  74.     while True:
  75.         try:
  76.             tbp.flair.set(name, flair_text, flair_css)
  77.             return True
  78.         except prawcore.PrawcoreException:
  79.             praw_fucked_up()
  80.  
  81. def set_link_flair(submission, flair_text, flair_css):
  82.     while True:
  83.         try:
  84.             submission.mod.flair(css_class=flair_css, text=flair_text)
  85.             return True
  86.         except prawcore.PrawcoreException:
  87.             praw_fucked_up()
  88.  
  89. def add_approved(name):
  90.     while True:
  91.         try:
  92.             tbp.contributor.add(name)
  93.             return True
  94.         except praw.exceptions.APIException as e:
  95.             if e.error_type == "USER_DOESNT_EXIST":
  96.                 log.info("User: {0} is deleted or suspended, dropping from userlist".format(name))
  97.                 del users[name]
  98.                 del user_data[name]
  99.                 return True
  100.         except prawcore.PrawcoreException:
  101.             praw_fucked_up()
  102.  
  103. def del_approved(name):
  104.     while True:
  105.         try:
  106.             tbp.contributor.remove(name)
  107.         except praw.exceptions.APIException as e:
  108.             if e.error_type == "USER_DOESNT_EXIST":
  109.                 log.info("User: {0} is deleted or suspended, dropping from userlist".format(name))
  110.                 del users[name]
  111.                 del user_data[name]
  112.                 return True
  113.         except prawcore.PrawcoreException:
  114.             praw_fucked_up()
  115.  
  116. class Tracker:
  117.     SCAN_INTERVAL = timedelta(minutes=5)
  118.     # For some reason VSCode eats the indents on blank lines and this gives the python interpreter fits when I copy/paste, so empty comments at the correct indent level it is!
  119.     def __init__(self, name, new_user=True):
  120.         self.name = name
  121.         if new_user:
  122.             self.data = {   "name": name,
  123.                             "scan_count": 0,
  124.                             "last_seen": None,
  125.                             "next_scan": None,
  126.                             "smv": 0,
  127.                             "flair": None,
  128.                             "good_score": [],
  129.                             "bad_score": [],
  130.                             "manual_score_bias": 0,
  131.                             "best_post": { "score": 0, "id": None },
  132.                             "worst_post": { "score": 0, "id": None },
  133.                             "comment_eater": {}
  134.                         }  
  135.             self.update()
  136.             log.info("Adding new user: {0}".format(name))
  137.         else:
  138.             self.data = user_data[name]
  139.     #
  140.     @property
  141.     def good_score(self):
  142.         if len(self.data["good_score"]) > 0:
  143.             return mean(self.data["good_score"]) + self.data["manual_score_bias"]
  144.         else:
  145.             #log.warn("User: {0} has no tracked good points?".format(self.name))
  146.             return 0
  147.     @property
  148.     def bad_score(self):
  149.         if len(self.data["bad_score"]) > 0:
  150.             return mean(self.data["bad_score"])
  151.         else:
  152.             #log.warn("User: {0} has no tracked bad points?".format(self.name))
  153.             return 0
  154.     @property
  155.     def score(self):
  156.         return self.good_score + self.bad_score
  157.     def update(self):
  158.         self.data["last_seen"] = datetime.now()
  159.         try:
  160.             if self.data["last_seen"] < self.data["next_scan"]:
  161.                 log.debug("Skipping user: {0} (next scan on {1:%Y/%m/%d %I:%M:%S%p})".format(self.name, self.next_refresh))
  162.                 return True
  163.         except TypeError:
  164.             pass
  165.         good, bad, good_count, bad_count = 0, 0, 0, 0
  166.         try:
  167.             for comment in reddit.redditor(self.name).comments.new(limit=100):
  168.                 sub = comment.subreddit.display_name.lower()
  169.                 karma = comment.score - 1
  170.                 if sub == "thebluepill":
  171.                     good += karma
  172.                     good_count += 1
  173.                     try:
  174.                         if karma > self.data["best_post"]["score"]:
  175.                             self.data["best_post"] = {"score": karma, "id": comment.id}
  176.                     except TypeError:
  177.                         self.data["best_post"] = {"score": karma, "id": comment.id}
  178.                     try:
  179.                         if karma < self.data["worst_post"]["score"]:
  180.                             self.data["worst_post"] = {"score": karma, "id": comment.id}
  181.                     except TypeError:
  182.                         self.data["worst_post"] = {"score": karma, "id": comment.id}
  183.                 elif sub in NICE_LIST:
  184.                     good += karma / 2
  185.                 elif sub in NAUGHTY_LIST and karma > 1:
  186.                     bad -= karma
  187.                     bad_count += 1
  188.             if good_count > 0:
  189.                 good /= good_count
  190.                 good *= good_count / 10 # Every 20 posts in r/TheBluePill doubles your good karma
  191.             if bad_count > 0:
  192.                 bad /= bad_count
  193.                 bad *= bad_count / 5 # Every 10 posts in NAUGHTY_LIST doubles your bad karma
  194.                 # Are they vexatious? (Of course they are!)
  195.                 if self.data["flair"] != "vexatious" and bad_count > 10 and abs(bad) > good:
  196.                     log.info("User: {0} is vexatious ({1} posts in NAUGHTY_LIST)".format(self.name, bad_count))
  197.                     set_flair(self.name, "VEXATIOUS LITIGANT", "vexatious")
  198.                     self.data["flair"] = "vexatious"
  199.             self.data["good_score"].insert(0, good)
  200.             if len(self.data["good_score"]) > 8:
  201.                 self.data["good_score"].pop()
  202.                 log.debug("Dropping oldest good karma sample for user: {0}".format(self.name))
  203.             self.data["bad_score"].insert(0, bad)
  204.             if len(self.data["bad_score"]) > 8:
  205.                 self.data["bad_score"].pop()
  206.                 log.debug("Dropping oldest bad karma sample for user: {0}".format(self.name))
  207.             self.data["scan_count"] += 1
  208.             self.data["next_refresh"] = self.data["last_seen"] + Tracker.SCAN_INTERVAL
  209.             log.info("Scanned user: {0} (scanned {1} times), good_karma: {2:.2f} ({3} comments), bad_karma: {4:.2f} ({5} comments)".format(self.name, self.data["scan_count"], good, good_count, bad, bad_count))
  210.             user_data[self.name] = self.data
  211.             return True
  212.         except (prawcore.exceptions.NotFound, prawcore.exceptions.Forbidden):
  213.             log.debug("User: {0} is suspended or deleted".format(self.name))
  214.             return False
  215.  
  216. def update_flairs():
  217.     log.info("Recalculating SMV")
  218.     i = 0
  219.     total = len(users)
  220.     # I'm so fuckin' dumb, we should've been doing it this way from the beginning
  221.     purged_users = list(tbp.banned(limit=None))
  222.     approved_users = list(tbp.contributor(limit=None))
  223.     #
  224.     for user in sorted(users.values(), key=lambda user: user.score):
  225.         dirty = False
  226.         flair = user.data["flair"]
  227.         # Check if user should be marked/unmarked as purged
  228.         if flair != "purged" and user.name in purged_users:
  229.             log.info("Marking user: {0} purged".format(user.name))
  230.             set_flair(user.name, "PURGED", "purged")
  231.             flair = "purged"
  232.             user.data["flair"] = "purged"
  233.             dirty = True
  234.         elif flair == "purged" and user.name not in purged_users:
  235.             log.info("User: {0} was unbanned, correcting flair".format(user.name))
  236.             set_flair(user.name, "", "")
  237.             flair = None
  238.             user.data["flair"] = None
  239.             dirty = True
  240.         # Calculate SMV
  241.         i += 1
  242.         smv = ceil((i / total) * 10)
  243.         if not flair:
  244.             log.debug("No cached flair for user: {0}, checking Reddit".format(user.name))
  245.             flair = get_flair(user.name)
  246.             user.data["flair"] = flair
  247.             log.debug("Reddit says user: {0}'s flair is {1}".format(user.name, flair))
  248.             dirty = True
  249.         log.debug("User: {0}, SMV: {1}, score: {2:.2f} (current flair {3})".format(user.name, smv, user.score, flair))
  250.         # Update flairs
  251.         if flair in IMMUTABLE_FLAIRS:
  252.             if "flair" == "vexatious" and smv > 2 and user.good_score > abs(user.bad_score):
  253.                 log.warn("Should user: {0} be vexatious? SMV: {1}, good_score: {2:.2f}, bad_score: {3:.2f}".format(user.name, smv, user.good_karma, user.bad_karma))
  254.             else:
  255.                 log.debug("Not changing user: {0} (immutable flair {1})".format(user.name, flair))
  256.         elif flair != "hb{0}".format(smv):
  257.             flair = "hb{0}".format(smv)
  258.             user.data["flair"] = flair
  259.             user.data["smv"] = smv
  260.             set_flair(user.name, "Hβ{0}".format(smv), "hb{0}".format(smv))
  261.             log.info("Updating user: {0} flair to hb{1}".format(user.name, smv))
  262.             dirty = True
  263.         else:
  264.             log.debug("User: {0} still an Hβ{1}".format(user.name, smv))
  265.         # Add/remove approved contributors
  266.         if ((smv > 7 or flair in GOOD_FLAIRS) and flair not in BAD_FLAIRS) and user.name not in approved_users:
  267.             add_approved(user.name)
  268.             log.debug("Adding approved contributor: {0}".format(user.name))
  269.         elif ((smv < 4 or flair in BAD_FLAIRS) and flair not in GOOD_FLAIRS) and user.name in approved_users:
  270.             del_approved(user.name)
  271.             log.info("Removing approved contributor: {0}".format(user.name))
  272.         if dirty:
  273.             user_data[user.name] = user.data
  274.  
  275. def drop_dead_users():
  276.     for user in users.values():
  277.         try:
  278.             for comment in reddit.redditor(user.name).new(limit=1):
  279.                 log.debug("User: {0} still exists (most recent comment id is {1})".format(user.name, comment.id))
  280.         except (prawcore.exceptions.NotFound, prawcore.exceptions.Forbidden):
  281.             log.info("User: {0} is deleted or suspended, dropping from userlist".format(user.name))
  282.             del users[user.name]
  283.             del user_data[user.name]
  284.             continue
  285.  
  286. def botloop(first_update=datetime.now()):
  287.     DEAD_USER_INTERVAL = timedelta(days=1)
  288.     FLAIR_UPDATE_INTERVAL = timedelta(minutes=15)
  289.     next_flair_update = first_update
  290.     next_dead_user_update = first_update + DEAD_USER_INTERVAL
  291.     # Loop over comment stream indefinitely
  292.     for comment in tbp.stream.comments():
  293.         if datetime.now() > next_dead_user_update:
  294.             drop_dead_users()
  295.         if datetime.now() > next_flair_update:
  296.             update_flairs()
  297.             next_flair_update = datetime.now() + FLAIR_UPDATE_INTERVAL
  298.             log.debug("Flair update complete (next update due on/after: {0:%Y/%m/%d %I:%M:%S%p})".format(next_flair_update))
  299.         # Grab submission link flair, assign one randomly if blank
  300.         threat_level = comment.submission.link_flair_css_class
  301.         if threat_level == None:
  302.             if randint(0,1) == 0: # 50-50 shot of getting "Severe"
  303.                 threat_selector = 0
  304.             else:
  305.                 threat_selector = randint(1, len(TODAYS_THREAT_LEVEL) - 3) # Won't randomly assign the last two link flairs
  306.             threat_level = list(TODAYS_THREAT_LEVEL.keys())[threat_selector]
  307.             set_link_flair(comment.submission, TODAYS_THREAT_LEVEL[threat_level], threat_level)
  308.             log.info("Set threat level for submission '{0}' [id: {1}] to {2}".format(comment.submission.title, comment.submission.id, TODAYS_THREAT_LEVEL[threat_level]))
  309.         else:
  310.             log.debug("Threat level for submission '{0}' [id: {1}] is {2})".format(comment.submission.title, comment.submission.id, TODAYS_THREAT_LEVEL[threat_level]))
  311.         # Skip over comments from deleted users
  312.         if comment.author:
  313.             dirty = False
  314.             name = comment.author.name
  315.             author_flair = comment.author_flair_css_class
  316.             # Update tracker
  317.             if not name in users.keys():
  318.                 users[name] = Tracker(name)
  319.             else:
  320.                 if not users[name].update():
  321.                     log.info("User: {0} is deleted or suspended, dropping from userlist".format(name))
  322.                     del users[name]
  323.                     del user_data[name]
  324.                     continue
  325.             # Update tracked flair if comment flair doesn't match tracked flair
  326.             user_flair = users[name].data["flair"]
  327.             if author_flair and user_flair != author_flair:
  328.                 dirty = True
  329.                 log.info("Updating tracked flair for user: {0} -- was {1}, now {2}".format(name, user_flair, author_flair))
  330.                 user_flair = author_flair
  331.                 users[name].data["flair"] = user_flair
  332.             # Handle unflaired users and their comments
  333.             if not author_flair and users[name].data["scan_count"] > 3:
  334.                 dirty = True
  335.                 log.debug("User: {0} should have flair: {1}, but comment flair is {2}".format(name, user_flair, author_flair))
  336.                 if "unflaired" not in users[name].data["comment_eater"].keys():
  337.                     users[name].data["comment_eater"]["unflaired"] = {"start":datetime.now(), "eaten": []}
  338.                     reply = comment.reply("Apologies for the inconvenience, but user flairs are mandatory on /r/TheBluePill. Please enable your user flair at your earliest convenience:\n\n- If you're on mobile Reddit, you'll need to first flip back to the desktop site (Burger Menu -> Desktop Site), then:\n- If you're on classic Reddit, you'll need to disable this Subreddit's custom CSS (preferences -> display options -> allow subreddits to show me custom themes)\n- If you're using an app to access Reddit, check the app's documentation, but you'll probably need to access the desktop site from a browser.\n\nYour comments will only be automatically removed if your SMV is too low, or the comment has negative karma. Once you're properly flaired again, these automatically removed comments will be restored.\n\nThanks for choosing r/TheBluePill!\n\netc. etc.,\n\n-Management\n\n*NOTE: This is an automated action, but this account is also attached to a human operator if you have any concerns. We appreciate your continued business!*")
  339.                     reply.mod.distinguish()
  340.                     log.info("Warned user: {0}, flairs are mandatory.".format(name))
  341.                 else:
  342.                     unflaired = users[name].data["comment_eater"]["unflaired"]
  343.                     exempt = name not in FLAIR_EXEMPT
  344.                     countdown_not_exceeded = datetime.now() < unflaired["start"] + timedelta(minutes=15)
  345.                     too_high_value = users[name].data["smv"] > 3 or comment.score > 0 or user_flair not in BAD_FLAIRS
  346.                     handled = comment.id in unflaired["eaten"] or comment.approved
  347.                     if exempt:
  348.                         log.info("Not removing comment {0} by unflaired user: {1} (exempt)".format(comment.id, name))
  349.                     elif countdown_not_exceeded:
  350.                         log.info("Not removing comment {0} by unflaired user: {1} (still in grace period after warning)".format(comment.id, name))
  351.                     elif too_high_value:
  352.                         log.info("Not removing comment {0} by unflaired user: {1} (too high value)".format(comment.id, name))
  353.                     elif handled:
  354.                         log.info("Not removing comment {0} by unflaired user: {1} (already removed or manually approved)".format(comment.id, name))
  355.                     else:
  356.                         unflaired["eaten"].append(comment.id)
  357.                         comment.mod.remove()
  358.                         log.info("Removed comment {0} by unflaired user: {1}".format(comment.id, name))
  359.             elif author_flair and "unflaired" in users[name].data["comment_eater"].keys() and len(users[name].data["comment_eater"]["unflaired"]["eaten"]) > 0:
  360.                 dirty = True
  361.                 for comment in users[name].data["comment_eater"]["unflaired"]["eaten"]:
  362.                     reddit.comment(id=comment).approve()
  363.                     log.info("Restoring comment {0} for user: {1} (is wearing flair now)".format(comment, name))
  364.                 users[name].data["comment_eater"]["unflaired"]["eaten"] = []
  365.             # Remove comments if user's SMV is too low for submission's threat level
  366.             if author_flair in THREAT_MATRIX[threat_level] and name != comment.submission.author and not comment.approved:
  367.                 dirty = True
  368.                 if "low_smv" not in users[name].data["comment_eater"].keys():
  369.                     users[name].data["comment_eater"]["low_smv"] = {"eaten": [comment.id]}
  370.                 else:
  371.                     users[name].data["comment_eater"]["low_smv"]["eaten"].append(comment.id)
  372.                 comment.mod.remove()
  373.                 log.info("Removing comment by user: {0} ({1}) on submission '{2}' [id: {3}] ({4}), SMV too low!".format(name, author_flair, comment.submission.title, comment.id, TODAYS_THREAT_LEVEL[threat_level]))
  374.             # TODO: Assign shit tests
  375.             if dirty:
  376.                 user_data[name] = users[name].data
  377.         else:
  378.             log.debug("Skipping comment {0}, author no longer exists".format(comment.id))
  379.  
  380. with shelve.open("user_data") as user_data:
  381.     users = {}
  382.     for user in user_data.keys():
  383.         users[user] = Tracker(user, False)
  384.     try:
  385.         botloop()
  386.     except prawcore.PrawcoreException:
  387.         praw_fucked_up()
  388.         botloop()
  389.     except KeyboardInterrupt:
  390.         log.info("VirtuteTron going off-line")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement