A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

233 satır
9.0 KiB

  1. #! /usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net>
  5. #
  6. # Permission is hereby granted, free of charge, to any person obtaining a copy
  7. # of this software and associated documentation files (the "Software"), to deal
  8. # in the Software without restriction, including without limitation the rights
  9. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. # copies of the Software, and to permit persons to whom the Software is
  11. # furnished to do so, subject to the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included in
  14. # all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22. # SOFTWARE.
  23. import imp
  24. from os import listdir, path
  25. from re import sub
  26. from threading import Lock, Thread
  27. from time import gmtime, strftime
  28. from earwigbot.commands import BaseCommand
  29. from earwigbot.tasks import BaseTask
  30. __all__ = ["CommandManager", "TaskManager"]
  31. class _ResourceManager(object):
  32. """
  33. EarwigBot's Base Resource Manager
  34. Resources are essentially objects dynamically loaded by the bot, both
  35. packaged with it (built-in resources) and created by users (plugins, aka
  36. custom resources). Currently, the only two types of resources are IRC
  37. commands and bot tasks. These are both loaded from two locations: the
  38. earwigbot.commands and earwigbot.tasks packages, and the commands/ and
  39. tasks/ directories within the bot's working directory.
  40. This class handles the low-level tasks of (re)loading resources via load(),
  41. retrieving specific resources via get(), and iterating over all resources
  42. via __iter__(). If iterating over resources, it is recommended to acquire
  43. self.lock beforehand and release it afterwards (alternatively, wrap your
  44. code in a `with` statement) so an attempt at reloading resources in another
  45. thread won't disrupt your iteration.
  46. """
  47. def __init__(self, bot, name, attribute, base):
  48. self.bot = bot
  49. self.logger = bot.logger.getChild(name)
  50. self._resources = {}
  51. self._resource_name = name # e.g. "commands" or "tasks"
  52. self._resource_attribute = attribute # e.g. "Command" or "Task"
  53. self._resource_base = base # e.g. BaseCommand or BaseTask
  54. self._resource_access_lock = Lock()
  55. @property
  56. def lock(self):
  57. return self._resource_access_lock
  58. def __iter__(self):
  59. for name in self._resources:
  60. yield name
  61. def _load_resource(self, name, path):
  62. """Load a specific resource from a module, identified by name and path.
  63. We'll first try to import it using imp magic, and if that works, make
  64. an instance of the 'Command' class inside (assuming it is an instance
  65. of BaseCommand), add it to self._commands, and log the addition. Any
  66. problems along the way will either be ignored or logged.
  67. """
  68. f, path, desc = imp.find_module(name, [path])
  69. try:
  70. module = imp.load_module(name, f, path, desc)
  71. except Exception:
  72. e = "Couldn't load module {0} (from {1})"
  73. self.logger.exception(e.format(name, path))
  74. return
  75. finally:
  76. f.close()
  77. attr = self._resource_attribute
  78. if not hasattr(module, attr):
  79. return # No resources in this module
  80. resource_class = getattr(module, attr)
  81. try:
  82. resource = resource_class(self.bot) # Create instance of resource
  83. except Exception:
  84. e = "Error instantiating {0} class in {1} (from {2})"
  85. self.logger.exception(e.format(attr, name, path))
  86. return
  87. if not isinstance(resource, self._resource_base):
  88. return
  89. self._resources[resource.name] = resource
  90. self.logger.debug("Loaded {0} {1}".format(attr.lower(), resource.name))
  91. def _load_directory(self, dir):
  92. """Load all valid resources in a given directory."""
  93. processed = []
  94. for name in listdir(dir):
  95. if not name.endswith(".py") and not name.endswith(".pyc"):
  96. continue
  97. if name.startswith("_") or name.startswith("."):
  98. continue
  99. modname = sub("\.pyc?$", "", name) # Remove extension
  100. if modname not in processed:
  101. self._load_resource(modname, dir)
  102. processed.append(modname)
  103. def load(self):
  104. """Load (or reload) all valid resources into self._resources."""
  105. name = self._resource_name # e.g. "commands" or "tasks"
  106. with self.lock:
  107. self._resources.clear()
  108. builtin_dir = path.join(path.dirname(__file__), name)
  109. plugins_dir = path.join(self.bot.config.root_dir, name)
  110. self._load_directory(builtin_dir) # Built-in resources
  111. self._load_directory(plugins_dir) # Custom resources, aka plugins
  112. msg = "Loaded {0} {1}: {2}"
  113. resources = ", ".join(self._resources.keys())
  114. self.logger.info(msg.format(len(self._resources), name, resources))
  115. def get(self, key):
  116. """Return the class instance associated with a certain resource.
  117. Will raise KeyError if the resource (command or task) is not found.
  118. """
  119. return self._resources[key]
  120. class CommandManager(_ResourceManager):
  121. """
  122. EarwigBot's IRC Command Manager
  123. Manages (i.e., loads, reloads, and calls) IRC commands.
  124. """
  125. def __init__(self, bot):
  126. base = super(CommandManager, self)
  127. base.__init__(bot, "commands", "Command", BaseCommand)
  128. def _wrap_check(self, command, data):
  129. """Check whether a command should be called, catching errors."""
  130. try:
  131. return command.check(data)
  132. except Exception:
  133. e = "Error checking command '{0}' with data: {1}:"
  134. self.logger.exception(e.format(command.name, data))
  135. def _wrap_process(self, command, data):
  136. """process() the message, catching and reporting any errors."""
  137. try:
  138. command.process(data)
  139. except Exception:
  140. e = "Error executing command '{0}':"
  141. self.logger.exception(e.format(data.command))
  142. def check(self, hook, data):
  143. """Given an IRC event, check if there's anything we can respond to."""
  144. self.lock.acquire()
  145. for command in self._resources.itervalues():
  146. if hook in command.hooks and self._wrap_check(command, data):
  147. self.lock.release()
  148. self._wrap_process(command, data)
  149. return
  150. self.lock.release()
  151. class TaskManager(_ResourceManager):
  152. """
  153. EarwigBot's Bot Task Manager
  154. Manages (i.e., loads, reloads, schedules, and runs) wiki bot tasks.
  155. """
  156. def __init__(self, bot):
  157. super(TaskManager, self).__init__(bot, "tasks", "Task", BaseTask)
  158. def _wrapper(self, task, **kwargs):
  159. """Wrapper for task classes: run the task and catch any errors."""
  160. try:
  161. task.run(**kwargs)
  162. except Exception:
  163. msg = "Task '{0}' raised an exception and had to stop:"
  164. self.logger.exception(msg.format(task.name))
  165. else:
  166. msg = "Task '{0}' finished successfully"
  167. self.logger.info(msg.format(task.name))
  168. def start(self, task_name, **kwargs):
  169. """Start a given task in a new daemon thread, and return the thread.
  170. kwargs are passed to task.run(). If the task is not found, None will be
  171. returned.
  172. """
  173. msg = "Starting task '{0}' in a new thread"
  174. self.logger.info(msg.format(task_name))
  175. try:
  176. task = self.get(task_name)
  177. except KeyError:
  178. e = "Couldn't find task '{0}'"
  179. self.logger.error(e.format(task_name))
  180. return
  181. task_thread = Thread(target=self._wrapper, args=(task,), kwargs=kwargs)
  182. start_time = strftime("%b %d %H:%M:%S")
  183. task_thread.name = "{0} ({1})".format(task_name, start_time)
  184. task_thread.daemon = True
  185. task_thread.start()
  186. return task_thread
  187. def schedule(self, now=None):
  188. """Start all tasks that are supposed to be run at a given time."""
  189. if not now:
  190. now = gmtime()
  191. # Get list of tasks to run this turn:
  192. tasks = self.bot.config.schedule(now.tm_min, now.tm_hour, now.tm_mday,
  193. now.tm_mon, now.tm_wday)
  194. for task in tasks:
  195. if isinstance(task, list): # They've specified kwargs,
  196. self.start(task[0], **task[1]) # so pass those to start
  197. else: # Otherwise, just pass task_name
  198. self.start(task)