Advertisement
CrashARuntimeToday

VirtueTron9000 v0.4.0a

Jul 26th, 2018
545
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 41.65 KB | None | 0 0
  1. #!/usr/bin/python
  2. LICENCE = "WTFPL", "http://www.wtfpl.net/about/"
  3. VERSION = "v0.4.0a"
  4.  
  5. import atexit
  6. import logging
  7. import pickle
  8. import praw
  9. import prawcore
  10. import random
  11. import shelve
  12. import string
  13. import sys
  14. import unicodedata
  15.  
  16. from datetime import datetime, timedelta
  17. from math import ceil
  18. from time import sleep
  19. from random import randint
  20. from statistics import mean
  21.  
  22.  
  23. ## Constants
  24. FLAIR_UPDATE_INTERVAL = timedelta(minutes=15)
  25. USER_UPDATE_INTERVAL = timedelta(hours=6)
  26.  
  27. # TODO: add allies
  28. ALLIES = []
  29.  
  30. # The Book of Grudges
  31. GRUDGE_REDPILL = ["theredpill", "marriedredpill", "asktrp", "askmrp", "rpchristians"]
  32. GRUDGE_STEPFORD = ["redpillwomen", "redpillwives", "femininenotfeminist"]
  33. GRUDGE_MGTOW = ["mgtow"]
  34. GRUDGE_INCEL = ["braincels"]
  35. GRUDGE_LOBSTER = ["jordanpeterson", "jordan_peterson_memes"]
  36. GRUDGE_EDGELORD = ["the_donald", "cringeanarchy", "milliondollarextreme", "4chan"] #TODO: "Russian Agent" for the_donald?
  37. GRUDGE_GENERAL = ["mensrights", "pussypassdenied", "feministpassdenied", "whereareallthegoodmen", "kotakuinaction", "tumblrinaction", "sjwhate"]
  38. BOOK_OF_GRUDGES = GRUDGE_REDPILL + GRUDGE_STEPFORD + GRUDGE_MGTOW + GRUDGE_INCEL + GRUDGE_LOBSTER + GRUDGE_EDGELORD + GRUDGE_GENERAL
  39.  
  40. # Flairs
  41. BAD_FLAIRS =    {   "vex-terp": "VEXATIOUS LIFTER",
  42.                     "vex-terp-flaired": "VEXATIOUS ROYALTY",
  43.                     "vex-stepford": "STEPFORD WIFE",
  44.                     "vex-mgtow": "LOST MGTOW",
  45.                     "vex-incel": "VOLUNTARY ASSHOLE",
  46.                     "vex-lobster": "ROGUE LOBSTER",
  47.                     "vex-edgy": "VEXATIOUS EDGELORD",
  48.                     "vexatious": "VEXATIOUS LITIGANT"
  49.                 }
  50. BAD_FLAIR_LIST = list(BAD_FLAIRS.keys())
  51. GOOD_FLAIR_LIST = ["endorsedflair", "vanguard", "alpha", "betaasfuck", "feeemale", "robot"]
  52. IMMUTABLE_FLAIRS = BAD_FLAIR_LIST + GOOD_FLAIR_LIST + ["purged"]
  53.  
  54. # Threat levels
  55. THREAT_LEVELS = {"tlsevere":"Severe", "tlhigh":"High", "tlelevated":"Elevated", "tlguarded":"Guarded", "tllow":"Low"}
  56. THREAT_MATRIX = {   "tllow": BAD_FLAIR_LIST + ["hb{0}".format(x) for x in range(1,7)],
  57.                     "tlguarded": BAD_FLAIR_LIST + ["hb{0}".format(x) for x in range(1,5)],
  58.                     "tlelevated": BAD_FLAIR_LIST + ["hb{0}".format(x) for x in range(1,3)],
  59.                     "tlhigh": BAD_FLAIR_LIST,
  60.                     "tlsevere": []
  61.                 }
  62.  
  63. # Special Snowflake users
  64. FLAIR_EXEMPT = []
  65. BETA_TESTERS = ["FailAShitTestToday", "Black_m0ngoose", "MrPerfidy", "frankwashere44", "movielover2018", "SufficientGlass"]
  66.  
  67.  
  68. ## Functions
  69. delay = 2
  70. last_fuck_up = None
  71. def sleep_it_off():
  72.     # The sloppy, lazy way of making a static function in Python
  73.     global delay
  74.     global last_fuck_up
  75.     log.error("Reddit API error: waiting {0} seconds".format(delay))
  76.     try:
  77.         if delay > 128 or datetime.now() > last_fuck_up + timedelta(minutes=2): delay = 2
  78.     except TypeError:
  79.         delay = 2
  80.     sleep(delay)
  81.     delay *= 2
  82.     last_fuck_up = datetime.now()
  83.  
  84. @atexit.register
  85. def sync_data():
  86.     # Hopefully this will keep VirtueTron from destroying its data files when it crashes
  87.     global_data.close()
  88.     user_data.close()
  89.  
  90. def drop_user(name):
  91.     log.info("User: {0} is deleted or suspended, dropping from userlist".format(name))
  92.     try:
  93.         del users[name]
  94.         del user_data[name]
  95.     except KeyError:
  96.         log.warn("Tried to delete nonexistant user: {0}".format(name))
  97.  
  98. def ban_user(name, ban_reason, ban_message):
  99.     tbp.banned.add(name, ban_reason=ban_reason, ban_message=ban_message)
  100.     set_flair(user.name, "PURGED", "purged")
  101.     del user.data["comment_eater"]["scrabble"]
  102.     user.data["flair"] = "purged"
  103.     user_data[user.name] = user.data
  104.  
  105. def get_flair(name):
  106.     while True:
  107.         try:
  108.             for flair in tbp.flair(redditor=name):
  109.                 return flair["flair_css_class"]
  110.         except prawcore.exceptions.ServerError:
  111.             sleep_it_off()
  112.         except (prawcore.exceptions.Forbidden, prawcore.exceptions.NotFound):
  113.             # It looks like Reddit always returns Forbidden if you try to get a dead user's flair, but better safe than sorry
  114.             return False
  115.  
  116. def set_flair(name, flair_text, flair_css):
  117.     while True:
  118.         try:
  119.             tbp.flair.set(name, flair_text, flair_css)
  120.             return True
  121.         except prawcore.exceptions.ServerError:
  122.             sleep_it_off()
  123.         # We won't get errors for trying to set flair on a dead user, so there's no point trying to catch Forbidden/NotFound here
  124.  
  125. def set_link_flair(submission, flair_text, flair_css):
  126.     while True:
  127.         try:
  128.             submission.mod.flair(css_class=flair_css, text=flair_text)
  129.             return
  130.         except prawcore.exceptions.ServerError:
  131.             sleep_it_off()
  132.  
  133. def add_approved(name):
  134.     while True:
  135.         try:
  136.             tbp.contributor.add(name)
  137.             return True
  138.         except praw.exceptions.APIException as e:
  139.             if e.error_type == "USER_DOESNT_EXIST":
  140.                 drop_user(name)
  141.                 return False
  142.         except prawcore.exceptions.ServerError:
  143.             sleep_it_off()
  144.  
  145. def del_approved(name):
  146.     while True:
  147.         try:
  148.             tbp.contributor.remove(name)
  149.             return True
  150.         except praw.exceptions.APIException as e:
  151.             if e.error_type == "USER_DOESNT_EXIST":
  152.                 drop_user(name)
  153.                 return False
  154.             else:
  155.                 log.error("Unhandled API exception: {0}".format(e.error_type))
  156.                 raise e
  157.         except prawcore.exceptions.ServerError:
  158.             sleep_it_off()
  159.  
  160.  
  161. ## Shit tests
  162. def scrabble_get_draw(scrabble):
  163.     BURN_RATE = 3
  164.     VOWELS = ['u', 'a', 'i', 'o', 'e']
  165.     consonants = [letter for letter in scrabble["safe_letters"] if letter not in VOWELS]
  166.     log.debug("Safe letters are {0}, consonants are: {1}".format(scrabble["safe_letters"], consonants))
  167.     if len(consonants) <= BURN_RATE:
  168.         if len(scrabble["safe_letters"]) <= BURN_RATE:
  169.             return False
  170.         else:
  171.             scrabble["banned_letters"] += scrabble["safe_letters"]
  172.             scrabble["safe_letters"] = []
  173.             return scrabble
  174.     new_len = len(scrabble["banned_letters"]) + BURN_RATE
  175.     log.debug("Should end up with up to {0} letters in banned_letters".format(new_len))
  176.     letter = random.choice(scrabble["safe_letters"])
  177.     log.debug("Picked '{0}'".format(letter))
  178.     scrabble["safe_letters"].remove(letter)
  179.     try:
  180.         consonants.remove(letter)
  181.     except ValueError:
  182.         pass
  183.     scrabble["banned_letters"].append(letter)
  184.     if letter not in VOWELS:
  185.         while len(scrabble["banned_letters"]) < new_len:
  186.             letter = random.choice(consonants)
  187.             log.debug("Picked '{0}'".format(letter))
  188.             scrabble["safe_letters"].remove(letter)
  189.             scrabble["banned_letters"].append(letter)
  190.     scrabble["banned_letters"] = sorted(scrabble["banned_letters"])
  191.     return scrabble
  192.  
  193. def scrabble_test(user, comment):
  194.     # Start the game if the user isn't already playing scrabble
  195.     if "scrabble" not in user.data["comment_eater"].keys():
  196.         scrabble = {"start": datetime.now(), "end": datetime.now() + timedelta(weeks=1), "banned_letters": [], "safe_letters": list(string.ascii_lowercase), "eaten": []}
  197.         scrabble_get_draw(scrabble)
  198.         reply = comment.reply("As an unmutual member of our community, you've been chosen for a shit test:\n\n Your comments are no longer allowed to contain the letters: {0}. If your comment contains forbidden letters, it will be removed and bonus forbidden letters will be added. If you manage to run out of letters within a week, you will be hard nexted.\n\n**Good luck!**\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!*".format(scrabble["banned_letters"]))
  199.         reply.mod.distinguish()
  200.         log.info("User: {0} now playing scrabble! Banned letters are {1}".format(user.name, scrabble["banned_letters"]))
  201.         user.data["comment_eater"]["scrabble"] = scrabble
  202.         user_data[user.name] = user.data
  203.         return
  204.     # Stop the game if the time limit's up
  205.     scrabble = user.data["comment_eater"]["scrabble"]
  206.     if datetime.now() > scrabble["end"]:
  207.         reply = comment.reply("You've successfully passed your shit test! Congratulations! As recompense you will receive a token SMV boost on r/TheBluePill.\n\n**Thanks for playing!**")
  208.         reply.mod.distinguish()
  209.         user.data["manual_score_bias"] += 1
  210.         del user.data["comment_eater"]["scrabble"]
  211.         user_data[user.name] = user.data
  212.         log.warn("User: {0} successfully passed scrabble!".format(user.name))
  213.         return
  214.     # Continue the game
  215.     body = unicodedata.normalize("NFKD", comment.body.lower()) # Decompose accented characters and flatten case to catch cheaters
  216.     if comment.id not in scrabble["eaten"] and any(letter in body for letter in scrabble["banned_letters"]):
  217.         scrabble = scrabble_get_draw(scrabble)
  218.         if scrabble == False: # Out of letters to draw, test has been failed
  219.             mesg = "Oh, no! It looks like you've run out of letters. This game is over.\n\n**Thanks for playing!**"
  220.             reply = comment.reply(mesg)
  221.             reply.mod.distinguish()
  222.             ban_user(user.name, ban_reason="AUTOBANNED: failed at scrabble", ban_message=mesg)
  223.             log.warn("User: {0} is purged (failed at scrabble)".format(user.name))
  224.             return
  225.         if any([unicodedata.category(char) == "Mn" for char in body]):
  226.             insert = " Putting accent marks on letters doesn't make them different letters, but nice try at cheating!"
  227.         else:
  228.             insert = ""
  229.         reply = comment.reply("Whoops! It looks like [you](https://www.reddit.com/u/{0}) used a banned letter.{1} Your banned letters are now: {2}\n\n**Better luck next time!**".format(user.name, insert, scrabble["banned_letters"]))
  230.         reply.mod.distinguish()
  231.         if not comment.approved:
  232.             scrabble["eaten"].append(comment.id)
  233.             comment.mod.remove()
  234.             log.info("Removing comment by user: {0} ({1}) [id: {2}] on submission '{3}' [id: {4}], failed scrabble test. Banned letters are now {5}".format(user.name, user.data["flair"], comment.id, comment.submission.title, comment.submission.id, scrabble["banned_letters"]))
  235.         user.data["comment_eater"]["scrabble"] = scrabble
  236.         user_data[user.name] = user.data
  237.  
  238. def brevity_test(user, comment):
  239.     # Start the game if the user isn't already playing brevity
  240.     if "brevity" not in user.data["comment_eater"].keys():
  241.         brevity = {"start": datetime.now(), "end": datetime.now() + timedelta(weeks=1), "length": 160, "eaten": []}
  242.         reply = comment.reply("As an unmutual member of our community, you've been chosen for a shit test:\n\n Your comments are no longer allowed to be longer than 160 characters. If your comment is too long, it will be removed and the limit will be halved. If the limit hits 1 character within a week, you will be hard nexted.\n\n**Good luck!**\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!*")
  243.         user.data["comment_eater"]["brevity"] = brevity
  244.         user_data[user.name] = user.data
  245.         log.info("User: {0} now playing brevity! Max comment length 160".format(user.name))
  246.         return
  247.     # Stop the game if the time limit's up
  248.     brevity = user.data["comment_eater"]["brevity"]
  249.     if datetime.now() > brevity["end"]:
  250.         reply = comment.reply("You've successfully passed your shit test! Congratulations! As recompense you will receive a token SMV boost on r/TheBluePill.\n\n**Thanks for playing!**")
  251.         reply.mod.distinguish()
  252.         user.data["manual_score_bias"] += 1
  253.         del user.data["comment_eater"]["brevity"]
  254.         user_data[user.name] = user.data
  255.         return
  256.     # Continue the game
  257.     if len(comment.body) > brevity["length"]:
  258.         brevity["length"] /= 2
  259.         if brevity["length"] < 2:
  260.             mesg = "Oh, no! It looks like you've failed to master the virtue of brevity. This game is over.\n\n**Thanks for playing!**"
  261.             reply = comment.replay(mesg)
  262.             reply.mod.distinguish()
  263.             ban_user(user.name, ban_reason="AUTOBANNED: failed at brevity", ban_message=mesg)
  264.             return
  265.         else:
  266.             reply = comment.reply("Whoops! It looks like that comment was too long. [Your](https://www.reddit.com/u/{0}) comments can't be longer than {1} characters now.\n\n**Good luck!**\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!*".format(user.name, brevity["length"]))
  267.             reply.mod.distinguish()
  268.             if not comment.approved:
  269.                 brevity["eaten"].append(comment.id)
  270.                 comment.mod.remove()
  271.                 log.info("Removing comment by user: {0} ({1}) [id: {2}] on submission '{3}' [id: {4}], failed brevity test. Max comment length is now {5}".format(user.name, user.data["flair"], comment.id, comment.submission.title, comment.submission.id, brevity["length"]))
  272.         user.data["comment_eater"]["brevity"] = brevity
  273.         user_data[user.name] = user.data
  274.  
  275. SHIT_TESTS = {"scrabble": scrabble_test, "brevity": brevity_test}    
  276.  
  277.  
  278. # If I use a class that makes this object oriented, right?
  279. class Tracker:
  280.     SCAN_INTERVAL = timedelta(minutes=10)
  281.     SCAN_DEPTH = 16
  282.     DROP_THRESHOLD = 16
  283.     def __init__(self, name, new_user=True):
  284.         self.name = name
  285.         if new_user:
  286.             self.data = {   "name": name,
  287.                             "scan_count": 0,
  288.                             "last_seen": None,
  289.                             "next_scan": None,
  290.                             "smv": 0,
  291.                             "flair": get_flair(name),
  292.                             "good_score": [],
  293.                             "ally_score": [],
  294.                             "bad_score": [],
  295.                             "manual_score_bias": 0,
  296.                             "best_comment": { "score": 0, "id": None },
  297.                             "worst_comment": { "score": 0, "id": None },
  298.                             "comment_eater": {},
  299.                             "zero_count": 0
  300.                         }  
  301.             self.update()
  302.             log.info("Adding new user: {0}".format(name))
  303.         else:
  304.             self.data = user_data[name]
  305.            
  306.     @property
  307.     def good_score(self):
  308.         if len(self.data["good_score"]) > 0:
  309.             try:
  310.                 return mean(self.data["good_score"]) + mean(self.data["ally_score"]) / 2 + self.data["manual_score_bias"]
  311.             except KeyError:
  312.                 return mean(self.data["good_score"]) + self.data["manual_score_bias"]
  313.         else:
  314.             log.warn("User: {0} has no tracked good score?".format(self.name))
  315.             return 0
  316.     @property
  317.     def bad_score(self):
  318.         if len(self.data["bad_score"]) > 0:
  319.             return mean(self.data["bad_score"])
  320.         else:
  321.             log.warn("User: {0} has no tracked bad score?".format(self.name))
  322.             return 0
  323.     @property
  324.     def score(self):
  325.         return self.good_score + self.bad_score
  326.     def _tally(self):
  327.         good, ally, bad = 0, 0, 0
  328.         count = {   "good": 0,
  329.                     "ally": 0,
  330.                     "vex-terp": 0,
  331.                     "vex-stepford": 0,
  332.                     "vex-mgtow": 0,
  333.                     "vex-incel": 0,
  334.                     "vex-lobster": 0,
  335.                     "vex-edgy": 0,
  336.                     "vexatious": 0,
  337.                     "total_bad": 0
  338.                 }
  339.         for comment in reddit.redditor(self.name).comments.new(limit=100):
  340.             sub = comment.subreddit.display_name.lower()
  341.             karma = comment.score - 1
  342.             if sub == "thebluepill":
  343.                 good += karma
  344.                 count["good"] += 1
  345.                 stats = global_data["stats"]
  346.                 dirty = False
  347.                 # Per user best/worst comment stats
  348.                 try:
  349.                     if karma > self.data["best_comment"]["score"]:
  350.                         self.data["best_comment"] = {"score": karma, "id": comment.id}
  351.                         dirty = True
  352.                 except (TypeError, KeyError):
  353.                     self.data["best_comment"] = {"score": karma, "id": comment.id}
  354.                     dirty = True
  355.                 try:
  356.                     if karma < self.data["worst_comment"]["score"]:
  357.                         self.data["worst_comment"] = {"score": karma, "id": comment.id}
  358.                         dirty = True
  359.                 except (TypeError, KeyError):
  360.                     self.data["worst_comment"] = {"score": karma, "id": comment.id}
  361.                     dirty = True
  362.                 if dirty:
  363.                     user_data[self.name] = self.data
  364.                     dirty = False
  365.                 # Global best/worst comment stats
  366.                 try:
  367.                     if karma > stats["best_comment"]["score"]:
  368.                         stats["best_comment"] = {"score": karma, "id": comment.id, "name": self.name}
  369.                         dirty = True
  370.                 except (TypeError, KeyError):
  371.                     stats["best_comment"] = {"score": karma, "id": comment.id, "name": self.name}
  372.                     dirty = True
  373.                 try:
  374.                     if karma < stats["worst_comment"]["score"]:
  375.                         stats["worst_comment"] = {"score": karma, "id": comment.id, "name": self.name}
  376.                         dirty = True
  377.                 except (TypeError, KeyError):
  378.                     stats["worst_comment"] = {"score": karma, "id": comment.id, "name": self.name}
  379.                     dirty = True
  380.                 if dirty:
  381.                     global_data["stats"] = stats
  382.             elif sub in ALLIES:
  383.                 count["ally"] += 1
  384.                 ally += karma
  385.             elif sub in BOOK_OF_GRUDGES and karma > 0:
  386.                 bad -= karma
  387.                 if sub in GRUDGE_REDPILL:
  388.                     count["vex-terp"] += 1
  389.                     count["total_bad"] += 1
  390.                 elif sub in GRUDGE_STEPFORD:
  391.                     count["vex-stepford"] += 1
  392.                     count["total_bad"] += 1
  393.                 elif sub in GRUDGE_MGTOW:
  394.                     count["vex-mgtow"] += 1
  395.                     count["total_bad"] += 1
  396.                 elif sub in GRUDGE_INCEL:
  397.                     count["vex-incel"] += 1
  398.                     count["total_bad"] += 1
  399.                 elif sub in GRUDGE_LOBSTER:
  400.                     count["vex-lobster"] += 1
  401.                     count["total_bad"] += 1
  402.                 elif sub in GRUDGE_EDGELORD:
  403.                     count["vex-edgy"] += 1
  404.                     count["total_bad"] += 1
  405.                 else:
  406.                     count["vexatious"] += 1
  407.                     count["total_bad"] += 1
  408.         if count["good"] > 0:
  409.             good /= count["good"]
  410.             good *= count["good"] / 10 # Every 20 posts in r/TheBluePill doubles your score
  411.         if count["total_bad"] > 0:
  412.             bad /= count["total_bad"]
  413.             bad *= count["total_bad"] / 5 # Every 10 posts in NAUGHTY_LIST doubles your bad score
  414.         return good, bad, ally, count
  415.  
  416.     def update(self):
  417.         try:
  418.             if self.data["last_seen"] < self.data["next_scan"]:
  419.                 log.debug("Skipping user: {0} (next scan on {1:%Y/%m/%d %I:%M:%S%p})".format(self.name, self.data["next_scan"]))
  420.                 return True
  421.         except TypeError:
  422.             pass
  423.         tally = False
  424.         while not tally:
  425.             try:
  426.                 good, bad, ally, count = self._tally()
  427.                 tally = True
  428.             except (prawcore.exceptions.NotFound, prawcore.exceptions.Forbidden):
  429.                 # Dead user
  430.                 return False
  431.             except prawcore.exceptions.ServerError:
  432.                 sleep_it_off()
  433.         # Are they vexatious? (Of course they are!)
  434.         if count["good"] == 0:
  435.             try:
  436.                 self.data["zero_count"] += 1
  437.             except KeyError:
  438.                 self.data["zero_count"] = 1
  439.             if self.data["zero_count"] > Tracker.DROP_THRESHOLD:
  440.                 log.info("Dropping user: {0} (zero posts on TBP in the last {1} scans".format(self.name, Tracker.DROP_THRESHOLD))
  441.                 return False
  442.         bad_flairs = list(BAD_FLAIRS.keys())
  443.         bad_flairs.remove("vexatious") # Transitional, specific vexatious flairs are new
  444.         vexatious_count = dict(count)
  445.         log.debug("Vexatious score: {0}".format(vexatious_count))
  446.         del vexatious_count["good"]
  447.         del vexatious_count["ally"]
  448.         del vexatious_count["total_bad"]
  449.         if self.data["flair"] not in bad_flairs and count["total_bad"] > 10 and abs(bad) > good:
  450.             vexatious_type = sorted(vexatious_count.keys(), key=lambda x: vexatious_count[x], reverse=True)[0]
  451.             #Disabling this for now -- do I need moderator access on a subreddit to read a user's flair? It seems like it.
  452.             #if vexatious_type == "vex-terp":
  453.                 #for flair in reddit.subreddit("theredpill").flair(redditor=self.name):
  454.                 #    terp_flair = flair
  455.                 #if flair != None:
  456.                 #    log.debug("User: {0} has a flair ('{1}') on r/TheRedPill".format(self.name, terp_flair))
  457.                 #    vexatious_type = "vex-terp-flaired"
  458.                 #    vexatious_count["vex-terp-flaired"] = vexatious_count["vex-terp"]
  459.                 #    vexatious_count["vex-terp"] = 0
  460.             log.info("User: {0} is a '{1}' ({2} of {3} posts in The Book of Grudges)".format(self.name, BAD_FLAIRS[vexatious_type], vexatious_count[vexatious_type], count["total_bad"]))
  461.             if not self.data["flair"] == vexatious_type:
  462.                 set_flair(self.name, BAD_FLAIRS[vexatious_type], vexatious_type)
  463.                 self.data["flair"] = vexatious_type
  464.             else:
  465.                 log.debug("User: {0} already assigned flair '{1}'".format(self.name, vexatious_type))
  466.         self.data["good_score"].insert(0, good)
  467.         if len(self.data["good_score"]) > Tracker.SCAN_DEPTH:
  468.             self.data["good_score"].pop()
  469.         try:
  470.             self.data["ally_score"].insert(0, ally)
  471.         except KeyError:  # Transitional, wasn't tracking this separately before
  472.             self.data["ally_score"] = [ally]
  473.         if len(self.data["ally_score"]) > Tracker.SCAN_DEPTH:
  474.             self.data["ally_score"].pop()
  475.         self.data["bad_score"].insert(0, bad)
  476.         if len(self.data["bad_score"]) > Tracker.SCAN_DEPTH:
  477.             self.data["bad_score"].pop()
  478.         self.data["scan_count"] += 1
  479.         self.data["next_scan"] = datetime.now() + Tracker.SCAN_INTERVAL
  480.         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, count["good"] + count["ally"], bad, count["total_bad"]))
  481.         user_data[self.name] = self.data
  482.         return True
  483.  
  484.  
  485. # Sorting hat
  486. def calc_sjw_market():
  487.     log.info("Recalculating SJW marketplace")
  488.     sjw_market = [list() for i in range(10)]
  489.     i = 0
  490.     neg_count = 0
  491.     zero_count = 0
  492.     pos_count = 0
  493.     rank_lowest = 0
  494.     rank_highest = 0
  495.     last_smv = -1
  496.     in_negative = True
  497.     in_positive = False
  498.     for user in users.values():
  499.         if user.score < 0:
  500.             neg_count += 1
  501.         elif user.score == 0:
  502.             zero_count += 1
  503.         else:
  504.             pos_count += 1
  505.     log.info("TBP has {0} tracked users: {1} negative, {2} at zero, {3} positive".format(len(users), neg_count, zero_count, pos_count))
  506.     for user in sorted(users.values(), key=lambda user: user.score):
  507.         if in_negative:
  508.             if i > neg_count:
  509.                 log.debug("Out of negative")
  510.                 in_negative = False
  511.                 i = 1
  512.                 smv = 2
  513.             else:
  514.                 i += 1
  515.                 smv = ceil((i / neg_count) * 2) - 1
  516.         elif in_positive:
  517.             i += 1
  518.             smv = ceil((i / pos_count) * 7) + 2
  519.         else:
  520.             if i > zero_count:
  521.                 log.debug("Out of zero")
  522.                 in_positive = True
  523.                 i = 1
  524.                 smv = ceil((i / pos_count) * 7) + 2
  525.             else:
  526.                 smv = 2
  527.                 i += 1
  528.         if smv > last_smv:
  529.             if last_smv != -1:
  530.                 log.info("{0} Users at rank hb{1} (lowest: {2} at {3:.2f}, highest: {4} at {5:.2f}), now processing rank hb{6}".format(len(sjw_market[last_smv]), last_smv, sjw_market[last_smv][0], rank_lowest, sjw_market[last_smv][-1], rank_highest, smv))
  531.             last_smv = smv
  532.             rank_lowest = user.score
  533.         rank_highest = user.score
  534.         sjw_market[smv].append(user.name)
  535.  
  536.     log.info("{0} Users at rank hb{1} (lowest: {2} at {3:.2f}, highest: {4} at {5:.2f})".format(len(sjw_market[smv]), smv, sjw_market[smv][0], rank_lowest, sjw_market[smv][-1], rank_highest))
  537.     return sjw_market
  538.  
  539. def update_flairs():
  540.     SPECIAL_THRESHOLD = 20
  541.     # I'm so fuckin' dumb, we should've been doing it this way from the beginning
  542.     # (grabbing the entire lists of purged/approved users at the start and checking locally instead of asking Reddit if individual users are purged/approved)
  543.     got_lists = False
  544.     log.info("Retrieving purged/approved lists from Reddit.")
  545.     while not got_lists:
  546.         try:
  547.             purged_users = list(tbp.banned(limit=None))
  548.             approved_users = list(tbp.contributor(limit=None))
  549.             got_lists = True
  550.         except prawcore.exceptions.ServerError:
  551.             sleep_it_off()
  552.     log.info("{0} purged users, {1} approved contributors".format(len(purged_users), len(approved_users)))
  553.     smv = 0
  554.     for rank in calc_sjw_market():
  555.         smv += 1
  556.         log.debug("On rank {0}".format(smv))
  557.         i = 0
  558.         for name in rank:
  559.             i += 1
  560.             user = users[name]
  561.             dirty = False
  562.             flair = user.data["flair"]
  563.             # Check if user should be marked/unmarked as purged
  564.             if flair != "purged" and user.name in purged_users:
  565.                 log.info("Marking user: {0} purged".format(user.name))
  566.                 set_flair(user.name, "PURGED", "purged")
  567.                 flair = "purged"
  568.                 user.data["flair"] = "purged"
  569.                 dirty = True
  570.             elif flair == "purged" and user.name not in purged_users:
  571.                 if get_flair(name) == "purged":
  572.                     log.info("User: {0} was unbanned, correcting flair".format(user.name))
  573.                     set_flair(user.name, "", "")
  574.                     flair = None
  575.                     user.data["flair"] = None
  576.                     dirty = True
  577.             if not flair:
  578.                 log.debug("No cached flair for user: {0}, checking Reddit".format(user.name))
  579.                 flair = get_flair(user.name)
  580.                 if flair == False:
  581.                     drop_user(user.name)
  582.                     continue
  583.                 if flair != None: # Don't update tracked flair if user isn't wearing a flair
  584.                     user.data["flair"] = flair
  585.                     log.debug("Reddit says user: {0}'s flair is {1}".format(user.name, flair))
  586.                     dirty = True
  587.             log.debug("User: {0}, SMV: {1}, score: {2:.2f} (current flair {3}) ".format(user.name, smv, user.score, flair))
  588.             # Update flairs
  589.             if smv == 3:
  590.                 new_flair = "hb3"
  591.             elif i < SPECIAL_THRESHOLD:
  592.                 new_flair = "hb{0}minus".format(smv)
  593.             elif i + SPECIAL_THRESHOLD > len(rank):
  594.                 new_flair = "hb{0}plus".format(smv)
  595.             else:
  596.                 new_flair = "hb{0}".format(smv)
  597.             log.debug("Computed flair for user: {0} is '{1}'".format(user.name, new_flair))
  598.             if flair in IMMUTABLE_FLAIRS:
  599.                 if flair in BAD_FLAIRS.keys() and smv > 2 and user.good_score > abs(user.bad_score):
  600.                     log.warn("Should user: {0} be vexatious? SMV: {1}, good_score: {2:.2f}, bad_score: {3:.2f}".format(user.name, smv, user.good_score, user.bad_score))
  601.                 else:
  602.                     log.debug("Not changing user: {0} (immutable flair {1})".format(user.name, flair))
  603.             elif flair != new_flair:
  604.                 if set_flair(user.name, "Hβ{0}".format(smv), new_flair) == False:
  605.                     drop_user(user.name)
  606.                     continue
  607.                 log.info("Updating user: {0} flair from {1} to {2}".format(user.name, flair, new_flair))
  608.                 flair = new_flair
  609.                 user.data["flair"] = flair
  610.                 user.data["smv"] = smv
  611.                 dirty = True
  612.             else:
  613.                 log.debug("User: {0} still a {1}".format(user.name, flair))
  614.             # Add/remove approved contributors
  615.             if ((smv > 7 or flair in GOOD_FLAIR_LIST) and flair not in BAD_FLAIR_LIST) and user.name not in approved_users:
  616.                 add_approved(user.name)
  617.                 log.info("Adding approved contributor: {0}".format(user.name))
  618.             elif ((smv < 4 or flair in BAD_FLAIR_LIST) and flair not in GOOD_FLAIR_LIST) and user.name in approved_users:
  619.                 del_approved(user.name)
  620.                 log.info("Removing approved contributor: {0}".format(user.name))
  621.             if dirty:
  622.                 user_data[user.name] = user.data
  623.     global_data["next_flair_update"] = datetime.now() + FLAIR_UPDATE_INTERVAL
  624.     log.info("Flair update complete (next update due on/after: {0:%Y/%m/%d %I:%M:%S%p})".format(global_data["next_flair_update"]))
  625.  
  626. def botloop():
  627.     try:
  628.         seen_comments = global_data["seen_comments"]
  629.     except pickle.UnpicklingError:
  630.         seen_comments = []
  631.     # Loop over comment stream indefinitely
  632.     for comment in tbp.stream.comments():
  633.         if datetime.now() > global_data["next_dead_user_update"]:
  634.             dead_user_count = 0
  635.             user_count = len(users)
  636.             for user in list(users.values()): # Python gets cranky if the size of a iterable changes while it's getting iterated
  637.                 if not user.update():
  638.                     dead_user_count += 1
  639.                     drop_user(user.name)
  640.                 # If they're getting shit tested and haven't been seen since the last user update, extend the end time out by USER_UPDATE_INTERVAL
  641.                 test = None
  642.                 for eater in user.data["comment_eater"]:
  643.                     if eater in SHIT_TESTS.keys():
  644.                         test = eater
  645.                         break
  646.                 if test and datetime.now() > user.data["last_seen"] + USER_UPDATE_INTERVAL:
  647.                     try:
  648.                         user.data["comment_eater"][test]["end"] += USER_UPDATE_INTERVAL
  649.                     except KeyError:
  650.                         user.data["comment_eater"][test]["end"] = user.data["comment_eater"][test]["start"] + timedelta(weeks=1) + USER_UPDATE_INTERVAL
  651.  
  652.             log.info("Dead user scan complete: {0} of {1} tracked users dropped (next update due on/after: {2:%Y/%m/%d %I:%M:%S%p})".format(dead_user_count, user_count, global_data["next_dead_user_update"]))
  653.             global_data["next_dead_user_update"] = datetime.now() + USER_UPDATE_INTERVAL
  654.         if datetime.now() > global_data["next_flair_update"]:
  655.             update_flairs()
  656.         # Don't bother processing the comment if it's already been looked at
  657.         if comment.id in seen_comments:
  658.             log.debug("Skipping comment '{0}' on submission '{1}' [id: {2}], already seen.".format(comment.id, comment.submission.title, comment.submission.id))
  659.             continue
  660.         # Grab submission link flair, assign one randomly if blank
  661.         threat_level = comment.submission.link_flair_css_class
  662.         if threat_level == None:
  663.             if randint(0,1) == 0: # 50-50 shot of getting "Severe"
  664.                 threat_selector = 0
  665.             else:
  666.                 threat_selector = randint(1, len(THREAT_LEVELS) - 3) # Won't randomly assign the last two link flairs
  667.             threat_level = list(THREAT_LEVELS.keys())[threat_selector]
  668.             set_link_flair(comment.submission, THREAT_LEVELS[threat_level], threat_level)
  669.             log.info("Set threat level for submission '{0}' [id: {1}] to {2}".format(comment.submission.title, comment.submission.id, THREAT_LEVELS[threat_level]))
  670.         else:
  671.             log.debug("Threat level for submission '{0}' [id: {1}] is {2})".format(comment.submission.title, comment.submission.id, THREAT_LEVELS[threat_level]))
  672.         if not comment.author:  # Skip over comments from deleted users
  673.             log.debug("Skipping comment {0} on submission '{1}' [id: {2}], author no longer exists".format(comment.id, comment.submission.title, comment.submission.id))
  674.             continue
  675.         dirty = False
  676.         name = comment.author.name
  677.         author_flair = comment.author_flair_css_class
  678.         # Update tracker
  679.         if not name in users.keys():
  680.             users[name] = Tracker(name)
  681.         else:
  682.             if not users[name].update():
  683.                 drop_user(name)
  684.                 continue
  685.             users[name].data["last_seen"] = datetime.now()
  686.             dirty = True
  687.         # Update tracked flair if comment flair doesn't match tracked flair
  688.         user_flair = users[name].data["flair"]
  689.         if author_flair and user_flair != author_flair:
  690.             dirty = True
  691.             log.info("Updating tracked flair for user: {0} -- was {1}, now {2}".format(name, user_flair, author_flair))
  692.             user_flair = author_flair
  693.             users[name].data["flair"] = user_flair
  694.         # Handle unflaired users and their comments
  695.         if not author_flair and users[name].data["scan_count"] > 3:
  696.             log.debug("User: {0} should have flair: {1}, but comment flair is {2}".format(name, user_flair, author_flair))
  697.             if "unflaired" not in users[name].data["comment_eater"].keys() and users[name].data["smv"] < 4:
  698.                 users[name].data["comment_eater"]["unflaired"] = {"start":datetime.now(), "eaten": []}
  699.                 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!*")
  700.                 reply.mod.distinguish()
  701.                 log.info("Warned user: {0}, flairs are mandatory (except when they aren't).".format(name))
  702.                 dirty = True
  703.             else:
  704.                 unflaired = users[name].data["comment_eater"]["unflaired"]
  705.                 if name in FLAIR_EXEMPT:
  706.                     log.info("Not removing comment {0} by unflaired user: {1} (exempt)".format(comment.id, name))
  707.                 elif datetime.now() < unflaired["start"] + timedelta(minutes=15):
  708.                     log.info("Not removing comment {0} by unflaired user: {1} (still in grace period after warning)".format(comment.id, name))
  709.                 elif users[name].data["smv"] > 3 or comment.score > 0 or user_flair in GOOD_FLAIR_LIST:
  710.                     log.info("Not removing comment {0} by unflaired user: {1} (too high value)".format(comment.id, name))
  711.                 elif comment.approved:
  712.                     log.info("Not removing comment {0} by unflaired user: {1} (manually approved)".format(comment.id, name))
  713.                 elif comment.id in unflaired["eaten"] or comment.removed:
  714.                     log.info("Not removing comment {0} by unflaired user: {1} (already removed)".format(comment.id, name))
  715.                 else:
  716.                     unflaired["eaten"].append(comment.id)
  717.                     comment.mod.remove()
  718.                     log.info("Removed comment by user {0} ({1}) [id: {2}] on submission {3} [id: {4}], not wearing flair!".format(name, author_flair, comment.id, comment.submission.title, comment.submission.id))
  719.                     dirty = True
  720.         elif author_flair and "unflaired" in users[name].data["comment_eater"].keys() and len(users[name].data["comment_eater"]["unflaired"]["eaten"]) > 0:
  721.             for comment in users[name].data["comment_eater"]["unflaired"]["eaten"]:
  722.                 reddit.comment(id=comment).approve()
  723.                 log.info("Restoring comment {0} for user: {1} (is wearing flair now)".format(comment, name))
  724.             users[name].data["comment_eater"]["unflaired"]["eaten"] = []
  725.             dirty = True
  726.         # Remove comments if user's SMV is too low for submission's threat level
  727.         if author_flair in THREAT_MATRIX[threat_level] and name != comment.submission.author and not comment.approved:
  728.             try:
  729.                 users[name].data["comment_eater"]["low_smv"]["eaten"].append(comment.id)
  730.             except KeyError:
  731.                 users[name].data["comment_eater"]["low_smv"] = {"eaten": [comment.id]}
  732.             comment.mod.remove()
  733.             log.info("Removing comment by user: {0} ({1}) [id: {2}] on submission '{3}' [id: {4}] ({5}), SMV too low!".format(name, author_flair, comment.id, comment.submission.title, comment.submission.id, THREAT_LEVELS[threat_level]))
  734.             dirty = True
  735.         #Shit tests
  736.         if name in BETA_TESTERS:
  737.             test = None
  738.             for eater in users[name].data["comment_eater"]:
  739.                 if eater in SHIT_TESTS.keys():
  740.                     test = eater
  741.                     break
  742.             if test:
  743.                 log.debug("Shit testing comment by user: {0} ({1}) [id: {2}] on submission '{3}' [id: {4}]".format(name, author_flair, comment.id, comment.submission.title, comment.submission.id))
  744.                 SHIT_TESTS[test](users[name], comment)
  745.             else:
  746.                 scrabble_test(users[name], comment)
  747.         # Quick and dirty quarantine
  748.         if name == "movielover2018" and comment.submission.id != "91xefy":
  749.             mesg = "I GAVE YOU ONE FUCKING RULE AND YOU DEFY ME."
  750.             reply = comment.reply(mesg)
  751.             reply.mod.distinguish()
  752.             tbp.banned.add(name, ban_reason="AUTOBANNED: tried to escape quarantine", ban_message=mesg)
  753.             set_flair(name, "PURGED", "purged")
  754.             try:
  755.                 del users[name].data["comment_eater"]["scrabble"]
  756.             except KeyError:
  757.                 pass
  758.             users[name].data["flair"] = "purged"
  759.             user_data[name] = users[name].data
  760.             log.warn("GET FUCKED MOVIELOVER2018")
  761.         if dirty:
  762.             user_data[name] = users[name].data
  763.         seen_comments.insert(0, comment.id)
  764.         if len(seen_comments) > 250:
  765.             seen_comments.pop()
  766.         global_data["seen_comments"] = seen_comments
  767.         global_data.sync()
  768.         user_data.sync()
  769.  
  770. # Set up logging facilities
  771. log = logging.getLogger("VirtueTron")
  772. log.setLevel(logging.DEBUG)
  773. formatter = logging.Formatter(fmt="%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y/%m/%d %I:%M:%S%p")
  774. file_log = logging.FileHandler(filename="VirtueTron.log", mode="a")
  775. file_log.setLevel(logging.INFO)
  776. file_log.setFormatter(formatter)
  777. log.addHandler(file_log)
  778. console_log = logging.StreamHandler(stream=sys.stdout)
  779. console_log.setLevel(logging.DEBUG)
  780. console_log.setFormatter(formatter)
  781. log.addHandler(console_log)
  782. log.info("VirtueTron® 9000™ {0} © CrashARuntimeToday@outlook.com".format(VERSION))
  783.  
  784. # Connect to Reddit
  785. credentials = pickle.load(open("credentials.pickle", "rb")) # { client_id: "VirtueTron9000", client_secret: "🤖🤡🍆💯™", username: "SignalAVirtueToday", password: "https://youtu.be/RCVJ7bujnSc"}
  786. credentials["user_agent"] = "VirtueTron 9000 {0}".format(VERSION)
  787. reddit = praw.Reddit(**credentials)
  788. tbp = reddit.subreddit("TheBluePill")
  789. log.info("Hello, Reddit!")
  790.  
  791. # Get to work
  792. with shelve.open("user_data") as user_data, shelve.open("global_data") as global_data:
  793.     users = {}
  794.     if "seen_comments" not in global_data.keys():
  795.         global_data["seen_comments"] = []
  796.     if "stats" not in global_data.keys():
  797.         stats = {}
  798.         stats["best_comment"] = {"name": None, "score": None, "id": None}
  799.         stats["worst_comment"] = {"name": None, "score": None, "id": None}
  800.         global_data["stats"] = stats
  801.     if "next_flair_update" not in global_data.keys():
  802.         global_data["next_flair_update"] = datetime.now() + FLAIR_UPDATE_INTERVAL
  803.     if "next_dead_user_update" not in global_data.keys():
  804.         global_data["next_dead_user_update"] = datetime.now() + USER_UPDATE_INTERVAL
  805.     for user in user_data.keys():
  806.         users[user] = Tracker(user, False) # Change this to "True" the next time you crash the bot and wipe out user_data you stupid fuckin' hack
  807.     try:
  808.         botloop()
  809.     except prawcore.PrawcoreException:
  810.         sleep_it_off()
  811.         botloop()
  812.     except KeyboardInterrupt:
  813.         log.info("VirtuteTron going off-line")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement