A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

262 řádky
9.5 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. EarwigBot's XML 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.config import config" and access
  8. config data from within that object.
  9. """
  10. from collections import defaultdict
  11. from os import makedirs, path
  12. from xml.dom import minidom
  13. from xml.parsers.expat import ExpatError
  14. script_dir = path.dirname(path.abspath(__file__))
  15. root_dir = path.split(script_dir)[0]
  16. config_path = path.join(root_dir, "config.xml")
  17. _config = None # holds the parsed DOM object for our config file
  18. config = None # holds an instance of Container() with our config data
  19. class ConfigParseException(Exception):
  20. """Base exception for when we could not parse the config file."""
  21. class TypeMismatchException(ConfigParseException):
  22. """A field does not fit to its expected type; e.g., an arbitrary string
  23. where we expected a boolean or integer."""
  24. class MissingElementException(ConfigParseException):
  25. """An element in the config file is missing a required sub-element."""
  26. class MissingAttributeException(ConfigParseException):
  27. """An element is missing a required attribute to be parsed correctly."""
  28. class Container(object):
  29. """A class to hold information in a nice, accessable manner."""
  30. def _load_config():
  31. """Load data from our XML config file (config.xml) into a DOM object."""
  32. global _config
  33. _config = minidom.parse(config_path)
  34. def verify_config():
  35. """Check to see if we have a valid config file, and if not, notify the
  36. user. If there is no config file at all, offer to make one; otherwise,
  37. exit."""
  38. if path.exists(config_path):
  39. try:
  40. _load_config()
  41. except ExpatError as error:
  42. print "Could not parse config file {0}:\n{1}".format(config_path,
  43. error)
  44. exit()
  45. else:
  46. if not _config.getElementsByTagName("config"):
  47. e = "Config file is missing a <config> tag."
  48. raise MissingElementException(e)
  49. return are_passwords_encrypted()
  50. else:
  51. print "You haven't configured the bot yet!"
  52. choice = raw_input("Would you like to do this now? [y/n] ")
  53. if choice.lower().startswith("y"):
  54. return make_new_config()
  55. else:
  56. exit()
  57. def make_new_config():
  58. """Make a new XML config file based on the user's input."""
  59. makedirs(config_dir)
  60. encrypt = raw_input("Would you like to encrypt passwords stored in " +
  61. "config.xml? [y/n] ")
  62. if encrypt.lower().startswith("y"):
  63. is_encrypted = True
  64. else:
  65. is_encrypted = False
  66. return is_encrypted
  67. def encrypt_password(password, key):
  68. """If passwords are supposed to be encrypted, use this function to do that
  69. using a user-provided key."""
  70. # TODO: stub
  71. return password
  72. def decrypt_password(password, key):
  73. """If passwords are encrypted, use this function to decrypt them using a
  74. user-provided key."""
  75. # TODO: stub
  76. return password
  77. def are_passwords_encrypted():
  78. """Determine if the passwords in our config file are encrypted, returning
  79. either True or False."""
  80. element = _config.getElementsByTagName("config")[0]
  81. return attribute_to_bool(element, "encrypt-passwords", default=False)
  82. def attribute_to_bool(element, attribute, default=None):
  83. """Return True if the value of element's attribute is 'true', '1', or 'on';
  84. return False if it is 'false', '0', or 'off' (regardless of
  85. capitalization); return default if it is empty; raise TypeMismatchException
  86. if it does match any of those."""
  87. value = element.getAttribute(attribute).lower()
  88. if value in ["true", "1", "on"]:
  89. return True
  90. elif value in ["false", "0", "off"]:
  91. return False
  92. elif value == '':
  93. return default
  94. else:
  95. e = ("Expected a bool in attribute '{0}' of element '{1}', but " +
  96. "got '{2}'.").format(attribute, element.tagName, value)
  97. raise TypeMismatchException(e)
  98. def get_first_element(parent, tag_name):
  99. """Return the first child of the parent element with the given tag name, or
  100. return None if no child of that name exists."""
  101. try:
  102. return parent.getElementsByTagName(tag_name)[0]
  103. except IndexError:
  104. return None
  105. def get_required_element(parent, tag_name):
  106. """Return the first child of the parent element with the given tag name, or
  107. raise MissingElementException() if no child of that name exists."""
  108. element = get_first_element(parent, tag_name)
  109. if not element:
  110. e = "A <{0}> tag is missing a required <{1}> child tag.".format(
  111. parent.tagName, tag_name)
  112. raise MissingElementException(e)
  113. return element
  114. def get_required_attribute(element, attr_name):
  115. """Return the value of the attribute 'attr_name' in 'element'. If
  116. undefined, raise MissingAttributeException()."""
  117. attribute = element.getAttribute(attr_name)
  118. if not attribute:
  119. e = "A <{0}> tag is missing the required attribute '{1}'.".format(
  120. element.tagName, attr_name)
  121. raise MissingAttributeException(e)
  122. return attribute
  123. def parse_config(key):
  124. """A thin wrapper for the actual config parser in _parse_config(): catch
  125. parsing exceptions and report them to the user cleanly."""
  126. try:
  127. _parse_config(key)
  128. except ConfigParseException as e:
  129. print "\nError parsing config file:"
  130. print e
  131. exit(1)
  132. def _parse_config(key):
  133. """Parse config data from a DOM object into the 'config' global variable.
  134. The key is used to unencrypt passwords stored in the XML config file."""
  135. _load_config() # we might be re-loading unnecessarily here, but no harm in
  136. # that!
  137. data = _config.getElementsByTagName("config")[0]
  138. cfg = Container()
  139. cfg.components = parse_components(data)
  140. cfg.wiki = parse_wiki(data, key)
  141. cfg.irc = parse_irc(data, key)
  142. cfg.schedule = parse_schedule(data)
  143. cfg.watcher = parse_watcher(data)
  144. global config
  145. config = cfg
  146. def parse_components(data):
  147. """Parse everything within the <components> XML tag of our config file.
  148. The components object here will exist as config.components, and is a dict
  149. of our enabled components: components[name] = True if it is enabled, False
  150. if it is disabled."""
  151. components = defaultdict(lambda: False) # all components are disabled by
  152. # default
  153. element = get_required_element(data, "components")
  154. for component in element.getElementsByTagName("component"):
  155. name = get_required_attribute(component, "name")
  156. components[name] = True
  157. return components
  158. def parse_wiki(data, key):
  159. """Parse everything within the <wiki> tag of our XML config file."""
  160. pass
  161. def parse_irc_server(data, key):
  162. """Parse everything within a <server> tag."""
  163. server = Container()
  164. connection = get_required_element(data, "connection")
  165. server.host = get_required_attribute(connection, "host")
  166. server.port = get_required_attribute(connection, "port")
  167. server.nick = get_required_attribute(connection, "nick")
  168. server.ident = get_required_attribute(connection, "ident")
  169. server.realname = get_required_attribute(connection, "realname")
  170. nickserv = get_first_element(data, "nickserv")
  171. if nickserv:
  172. server.nickserv = Container()
  173. server.nickserv.username = get_required_attribute(nickserv, "username")
  174. password = get_required_attribute(nickserv, "password")
  175. if are_passwords_encrypted():
  176. server.nickserv.password = decrypt_password(password, key)
  177. else:
  178. server.nickserv.password = password
  179. channels = get_first_element(data, "channels")
  180. if channels:
  181. server.channels = list()
  182. for channel in channels.getElementsByTagName("channel"):
  183. name = get_required_attribute(channel, "name")
  184. server.channels.append(name)
  185. return server
  186. def parse_irc(data, key):
  187. """Parse everything within the <irc> tag of our XML config file."""
  188. irc = Container()
  189. element = get_first_element(data, "irc")
  190. if not element:
  191. return irc
  192. servers = get_first_element(element, "servers")
  193. if servers:
  194. for server in servers.getElementsByTagName("server"):
  195. server_name = get_required_attribute(server, "name")
  196. if server_name == "frontend":
  197. irc.frontend = parse_irc_server(server, key)
  198. elif server_name == "watcher":
  199. irc.watcher = parse_irc_server(server, key)
  200. else:
  201. print ("Warning: config file specifies a <server> with " +
  202. "unknown name '{0}'. Ignoring.").format(server_name)
  203. permissions = get_first_element(element, "permissions")
  204. if permissions:
  205. irc.permissions = dict()
  206. for group in permissions.getElementsByTagName("group"):
  207. group_name = get_required_attribute(group, "name")
  208. irc.permissions[group_name] = list()
  209. for user in group.getElementsByTagName("user"):
  210. hostname = get_required_attribute(user, "host")
  211. irc.permissions[group_name].append(hostname)
  212. return irc
  213. def parse_schedule(data):
  214. """Parse everything within the <schedule> tag of our XML config file."""
  215. pass
  216. def parse_watcher(data):
  217. """Parse everything within the <watcher> tag of our XML config file."""
  218. pass