A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.

229 righe
7.7 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. EarwigBot's JSON Config File Parser
  4. This handles all tasks involving reading and writing to our config file,
  5. including encrypting and decrypting passwords and making a new config file from
  6. scratch at the inital bot run.
  7. Usually you'll just want to do "from core import config" and access config data
  8. from within config's global variables and functions:
  9. * config.components - a list of enabled components
  10. * config.wiki - a dict of information about wiki-editing
  11. * config.tasks - a dict of information for bot tasks
  12. * config.irc - a dict of information about IRC
  13. * config.metadata - a dict of miscellaneous information
  14. * config.schedule() - returns a list of tasks scheduled to run at a given time
  15. Additionally, there are functions used in config loading:
  16. * config.load() - loads and parses our config file, returning True if
  17. passwords are stored encrypted or False otherwise
  18. * config.decrypt() - given a key, decrypts passwords inside our config
  19. variables; won't work if passwords aren't encrypted
  20. """
  21. import json
  22. import logging
  23. import logging.handlers
  24. from os import mkdir, path
  25. import blowfish
  26. script_dir = path.dirname(path.abspath(__file__))
  27. root_dir = path.split(script_dir)[0]
  28. config_path = path.join(root_dir, "config.json")
  29. log_dir = path.join(root_dir, "logs")
  30. _config = None # Holds data loaded from our config file
  31. # Set our easy-config-access global variables to None
  32. components, wiki, tasks, irc, metadata = None, None, None, None, None
  33. def _load():
  34. """Load data from our JSON config file (config.json) into _config."""
  35. global _config
  36. with open(config_path, 'r') as fp:
  37. try:
  38. _config = json.load(fp)
  39. except ValueError as error:
  40. print "Error parsing config file {0}:".format(config_path)
  41. print error
  42. exit(1)
  43. def _setup_logging():
  44. """Configures the logging module so it works the way we want it to."""
  45. logger = logging.getLogger()
  46. logger.setLevel(logging.DEBUG)
  47. if metadata.get("enableLogging"):
  48. hand = logging.handlers.TimedRotatingFileHandler
  49. formatter = BotFormatter()
  50. color_formatter = BotFormatter(color=True)
  51. logfile = lambda f: path.join(log_dir, f)
  52. if not path.isdir(log_dir):
  53. if not path.exists(log_dir):
  54. mkdir(log_dir, 0700)
  55. else:
  56. msg = "log_dir ({0}) exists but is not a directory!"
  57. print msg.format(log_dir)
  58. exit(1)
  59. main_handler = hand(logfile("bot.log"), "midnight", 1, 7)
  60. error_handler = hand(logfile("error.log"), "W6", 1, 4)
  61. debug_handler = hand(logfile("debug.log"), "H", 1, 6)
  62. main_handler.setLevel(logging.INFO)
  63. error_handler.setLevel(logging.WARNING)
  64. debug_handler.setLevel(logging.DEBUG)
  65. for h in (main_handler, error_handler, debug_handler):
  66. h.setFormatter(formatter)
  67. logger.addHandler(h)
  68. stream_handler = logging.StreamHandler()
  69. stream_handler.setLevel(logging.DEBUG)
  70. stream_handler.setFormatter(color_formatter)
  71. logger.addHandler(stream_handler)
  72. else:
  73. logger.addHandler(logging.NullHandler())
  74. def _make_new():
  75. """Make a new config file based on the user's input."""
  76. encrypt = raw_input("Would you like to encrypt passwords stored in config.json? [y/n] ")
  77. if encrypt.lower().startswith("y"):
  78. is_encrypted = True
  79. else:
  80. is_encrypted = False
  81. return is_encrypted
  82. def is_loaded():
  83. """Return True if our config file has been loaded, otherwise False."""
  84. return _config is not None
  85. def load():
  86. """Load, or reload, our config file.
  87. First, check if we have a valid config file, and if not, notify the user.
  88. If there is no config file at all, offer to make one, otherwise exit.
  89. Store data from our config file in five global variables (components, wiki,
  90. tasks, irc, metadata) for easy access (as well as the internal _config
  91. variable).
  92. If everything goes well, return True if stored passwords are
  93. encrypted in the file, or False if they are not.
  94. """
  95. global components, wiki, tasks, irc, metadata
  96. if not path.exists(config_path):
  97. print "You haven't configured the bot yet!"
  98. choice = raw_input("Would you like to do this now? [y/n] ")
  99. if choice.lower().startswith("y"):
  100. return _make_new()
  101. else:
  102. exit(1)
  103. _load()
  104. components = _config.get("components", [])
  105. wiki = _config.get("wiki", {})
  106. tasks = _config.get("tasks", {})
  107. irc = _config.get("irc", {})
  108. metadata = _config.get("metadata", {})
  109. _setup_logging()
  110. # Are passwords encrypted?
  111. return metadata.get("encryptPasswords", False)
  112. def decrypt(key):
  113. """Use the key to decrypt passwords in our config file.
  114. Call this if load() returns True. Catch password decryption errors and
  115. report them to the user.
  116. """
  117. global irc, wiki
  118. try:
  119. item = wiki.get("password")
  120. if item:
  121. wiki["password"] = blowfish.decrypt(key, item)
  122. item = irc.get("frontend").get("nickservPassword")
  123. if item:
  124. irc["frontend"]["nickservPassword"] = blowfish.decrypt(key, item)
  125. item = irc.get("watcher").get("nickservPassword")
  126. if item:
  127. irc["watcher"]["nickservPassword"] = blowfish.decrypt(key, item)
  128. except blowfish.BlowfishError as error:
  129. print "\nError decrypting passwords:"
  130. print "{0}: {1}.".format(error.__class__.__name__, error)
  131. exit(1)
  132. def schedule(minute, hour, month_day, month, week_day):
  133. """Return a list of tasks scheduled to run at the specified time.
  134. The schedule data comes from our config file's 'schedule' field, which is
  135. stored as _config["schedule"]. Call this function as config.schedule(args).
  136. """
  137. # Tasks to run this turn, each as a list of either [task_name, kwargs], or
  138. # just the task_name:
  139. tasks = []
  140. now = {"minute": minute, "hour": hour, "month_day": month_day,
  141. "month": month, "week_day": week_day}
  142. data = _config.get("schedule", [])
  143. for event in data:
  144. do = True
  145. for key, value in now.items():
  146. try:
  147. requirement = event[key]
  148. except KeyError:
  149. continue
  150. if requirement != value:
  151. do = False
  152. break
  153. if do:
  154. try:
  155. tasks.extend(event["tasks"])
  156. except KeyError:
  157. pass
  158. return tasks
  159. class BotFormatter(logging.Formatter):
  160. def __init__(self, color=False):
  161. self._format = super(BotFormatter, self).format
  162. if color:
  163. fmt = "[%(asctime)s %(lvl)s] %(name)s: %(message)s"
  164. self.format = lambda record: self._format(self.format_color(record))
  165. else:
  166. fmt = "[%(asctime)s %(levelname)-8s] %(name)s: %(message)s"
  167. self.format = self._format
  168. datefmt = "%Y-%m-%d %H:%M:%S"
  169. super(BotFormatter, self).__init__(fmt=fmt, datefmt=datefmt)
  170. def format_color(self, record):
  171. l = record.levelname.ljust(8)
  172. if record.levelno == logging.DEBUG:
  173. record.lvl = l.join(("\x1b[34m", "\x1b[0m")) # Blue
  174. if record.levelno == logging.INFO:
  175. record.lvl = l.join(("\x1b[32m", "\x1b[0m")) # Green
  176. if record.levelno == logging.WARNING:
  177. record.lvl = l.join(("\x1b[33m", "\x1b[0m")) # Yellow
  178. if record.levelno == logging.ERROR:
  179. record.lvl = l.join(("\x1b[31m", "\x1b[0m")) # Red
  180. if record.levelno == logging.CRITICAL:
  181. record.lvl = l.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red
  182. return record