| @@ -138,9 +138,9 @@ The most useful attributes are: | |||
| `earwigbot.config.BotConfig`_ stores configuration information for the bot. Its | |||
| docstring explains what each attribute is used for, but essentially each "node" | |||
| (one of ``config.components``, ``wiki``, ``tasks``, ``irc``, and ``metadata``) | |||
| maps to a section of the bot's ``config.yml`` file. For example, if | |||
| ``config.yml`` includes something like:: | |||
| (one of ``config.components``, ``wiki``, ``irc``, ``commands``, ``tasks``, and | |||
| ``metadata``) maps to a section of the bot's ``config.yml`` file. For example, | |||
| if ``config.yml`` includes something like:: | |||
| irc: | |||
| frontend: | |||
| @@ -158,7 +158,8 @@ Custom IRC commands | |||
| ~~~~~~~~~~~~~~~~~~~ | |||
| Custom commands are subclasses of `earwigbot.commands.BaseCommand`_ that | |||
| override ``BaseCommand``'s ``process()`` (and optionally ``check()``) methods. | |||
| override ``BaseCommand``'s ``process()`` (and optionally ``check()`` or | |||
| ``setup()``) methods. | |||
| ``BaseCommand``'s docstrings should explain what each attribute and method is | |||
| for and what they should be overridden with, but these are the basics: | |||
| @@ -171,6 +172,12 @@ for and what they should be overridden with, but these are the basics: | |||
| messages only), and ``"join"`` (for when a user joins a channel). See the | |||
| afc_status_ plugin for a command that responds to other hook types. | |||
| - Method ``setup()`` is called *once* with no arguments immediately after the | |||
| command is first loaded. Does nothing by default; treat it like an | |||
| ``__init__()`` if you want (``__init__()`` does things by default and a | |||
| dedicated setup method is often easier than overriding ``__init__()`` and | |||
| using ``super``). | |||
| - Method ``check()`` is passed a ``Data`` [2]_ object, and should return | |||
| ``True`` if you want to respond to this message, or ``False`` otherwise. The | |||
| default behavior is to return ``True`` only if ``data.is_command`` is | |||
| @@ -191,6 +198,12 @@ for and what they should be overridden with, but these are the basics: | |||
| ``self.action(chan_or_user, msg)``, ``self.notice(chan_or_user, msg)``, | |||
| ``self.join(chan)``, and ``self.part(chan)``. | |||
| Commands have access to ``config.commands[command_name]`` for config | |||
| information, which is a node in ``config.yml`` like every other attribute of | |||
| ``bot.config``. This can be used to store, for example, API keys or SQL | |||
| connection info, so that these can be easily changed without modifying the | |||
| command itself. | |||
| It's important to name the command class ``Command`` within the file, or else | |||
| the bot might not recognize it as a command. The name of the file doesn't | |||
| really matter and need not match the command's name, but this is recommended | |||
| @@ -243,7 +256,7 @@ and what they should be overridden with, but these are the basics: | |||
| Tasks have access to ``config.tasks[task_name]`` for config information, which | |||
| is a node in ``config.yml`` like every other attribute of ``bot.config``. This | |||
| can be used to store, for example, edit summaries, or templates to append to | |||
| can be used to store, for example, edit summaries or templates to append to | |||
| user talk pages, so that these can be easily changed without modifying the task | |||
| itself. | |||
| @@ -58,8 +58,13 @@ The most useful attributes are: | |||
| :py:class:`earwigbot.config.BotConfig` stores configuration information for the | |||
| bot. Its docstrings explains what each attribute is used for, but essentially | |||
| each "node" (one of :py:attr:`config.components`, :py:attr:`wiki`, | |||
| :py:attr:`tasks`, :py:attr:`tasks`, or :py:attr:`metadata`) maps to a section | |||
| each "node" (one of :py:attr:`config.components | |||
| <earwigbot.config.BotConfig.components>`, | |||
| :py:attr:`~earwigbot.config.BotConfig.wiki`, | |||
| :py:attr:`~earwigbot.config.BotConfig.irc`, | |||
| :py:attr:`~earwigbot.config.BotConfig.commands`, | |||
| :py:attr:`~earwigbot.config.BotConfig.tasks`, or | |||
| :py:attr:`~earwigbot.config.BotConfig.metadata`) maps to a section | |||
| of the bot's :file:`config.yml` file. For example, if :file:`config.yml` | |||
| includes something like:: | |||
| @@ -81,7 +86,8 @@ Custom IRC commands | |||
| Custom commands are subclasses of :py:class:`earwigbot.commands.BaseCommand` | |||
| that override :py:class:`~earwigbot.commands.BaseCommand`'s | |||
| :py:meth:`~earwigbot.commands.BaseCommand.process` (and optionally | |||
| :py:meth:`~earwigbot.commands.BaseCommand.check`) methods. | |||
| :py:meth:`~earwigbot.commands.BaseCommand.check` or | |||
| :py:meth:`~earwigbot.commands.BaseCommand.setup`) methods. | |||
| :py:class:`~earwigbot.commands.BaseCommand`'s docstrings should explain what | |||
| each attribute and method is for and what they should be overridden with, but | |||
| @@ -97,6 +103,13 @@ these are the basics: | |||
| a user joins a channel). See the afc_status_ plugin for a command that | |||
| responds to other hook types. | |||
| - Method :py:meth:`~earwigbot.commands.BaseCommand.setup` is called *once* with | |||
| no arguments immediately after the command is first loaded. Does nothing by | |||
| default; treat it like an :py:meth:`__init__` if you want | |||
| (:py:meth:`~earwigbot.tasks.BaseCommand.__init__` does things by default and | |||
| a dedicated setup method is often easier than overriding | |||
| :py:meth:`~earwigbot.tasks.BaseCommand.__init__` and using :py:obj:`super`). | |||
| - Method :py:meth:`~earwigbot.commands.BaseCommand.check` is passed a | |||
| :py:class:`~earwigbot.irc.data.Data` [1]_ object, and should return ``True`` | |||
| if you want to respond to this message, or ``False`` otherwise. The default | |||
| @@ -128,6 +141,12 @@ these are the basics: | |||
| <earwigbot.irc.connection.IRCConnection.join>`, and | |||
| :py:meth:`part(chan) <earwigbot.irc.connection.IRCConnection.part>`. | |||
| Commands have access to :py:attr:`config.commands[command_name]` for config | |||
| information, which is a node in :file:`config.yml` like every other attribute | |||
| of :py:attr:`bot.config`. This can be used to store, for example, API keys or | |||
| SQL connection info, so that these can be easily changed without modifying the | |||
| command itself. | |||
| It's important to name the command class :py:class:`Command` within the file, | |||
| or else the bot might not recognize it as a command. The name of the file | |||
| doesn't really matter and need not match the command's name, but this is | |||
| @@ -190,7 +209,7 @@ are the basics: | |||
| Tasks have access to :py:attr:`config.tasks[task_name]` for config information, | |||
| which is a node in :file:`config.yml` like every other attribute of | |||
| :py:attr:`bot.config`. This can be used to store, for example, edit summaries, | |||
| :py:attr:`bot.config`. This can be used to store, for example, edit summaries | |||
| or templates to append to user talk pages, so that these can be easily changed | |||
| without modifying the task itself. | |||
| @@ -49,9 +49,10 @@ class BaseCommand(object): | |||
| This is called once when the command is loaded (from | |||
| :py:meth:`commands.load() <earwigbot.managers._ResourceManager.load>`). | |||
| *bot* is out base :py:class:`~earwigbot.bot.Bot` object. Generally you | |||
| shouldn't need to override this; if you do, call | |||
| ``super(Command, self).__init__()`` first. | |||
| *bot* is out base :py:class:`~earwigbot.bot.Bot` object. Don't override | |||
| this directly; if you do, remember to place | |||
| ``super(Command, self).__init()`` first. Use :py:meth:`setup` for | |||
| typical command-init/setup needs. | |||
| """ | |||
| self.bot = bot | |||
| self.config = bot.config | |||
| @@ -67,6 +68,15 @@ class BaseCommand(object): | |||
| self.mode = lambda t, level, msg: self.bot.frontend.mode(t, level, msg) | |||
| self.pong = lambda target: self.bot.frontend.pong(target) | |||
| self.setup() | |||
| def setup(self): | |||
| """Hook called immediately after the command is loaded. | |||
| Does nothing by default; feel free to override. | |||
| """ | |||
| pass | |||
| def check(self, data): | |||
| """Return whether this command should be called in response to *data*. | |||
| @@ -29,6 +29,15 @@ class Command(BaseCommand): | |||
| """Geolocate an IP address (via http://ipinfodb.com/).""" | |||
| name = "geolocate" | |||
| def setup(self): | |||
| self.config.decrypt(self.config.commands, (self.name, "apiKey")) | |||
| try: | |||
| self.key = self.config.commands[self.name]["apiKey"] | |||
| except KeyError: | |||
| self.key = None | |||
| log = 'Cannot use without an API key for http://ipinfodb.com/ stored as config.commands["{0}"]["apiKey"]' | |||
| self.logger.warn(log.format(self.name)) | |||
| def check(self, data): | |||
| commands = ["geolocate", "locate", "geo", "ip"] | |||
| return data.is_command and data.command in commands | |||
| @@ -38,18 +47,16 @@ class Command(BaseCommand): | |||
| self.reply(data, "please specify an IP to lookup.") | |||
| return | |||
| try: | |||
| key = config.tasks[self.name]["apiKey"] | |||
| except KeyError: | |||
| msg = 'I need an API key for http://ipinfodb.com/ stored as \x0303config.tasks["{0}"]["apiKey"]\x0301.' | |||
| log = 'Need an API key for http://ipinfodb.com/ stored as config.tasks["{0}"]["apiKey"]' | |||
| if not self.key: | |||
| msg = 'I need an API key for http://ipinfodb.com/ stored as \x0303config.commands["{0}"]["apiKey"]\x0301.' | |||
| log = 'Need an API key for http://ipinfodb.com/ stored as config.commands["{0}"]["apiKey"]' | |||
| self.reply(data, msg.format(self.name) + ".") | |||
| self.logger.error(log.format(self.name)) | |||
| return | |||
| address = data.args[0] | |||
| url = "http://api.ipinfodb.com/v3/ip-city/?key={0}&ip={1}&format=json" | |||
| query = urllib2.urlopen(url.format(key, address)).read() | |||
| query = urllib2.urlopen(url.format(self.key, address)).read() | |||
| res = json.loads(query) | |||
| try: | |||
| @@ -48,8 +48,9 @@ class BotConfig(object): | |||
| - :py:attr:`path`: path to the bot's config file | |||
| - :py:attr:`components`: enabled components | |||
| - :py:attr:`wiki`: information about wiki-editing | |||
| - :py:attr:`tasks`: information for bot tasks | |||
| - :py:attr:`irc`: information about IRC | |||
| - :py:attr:`commands`: information about IRC commands | |||
| - :py:attr:`tasks`: information for bot tasks | |||
| - :py:attr:`metadata`: miscellaneous information | |||
| - :py:meth:`schedule`: tasks scheduled to run at a given time | |||
| @@ -69,12 +70,13 @@ class BotConfig(object): | |||
| self._components = _ConfigNode() | |||
| self._wiki = _ConfigNode() | |||
| self._tasks = _ConfigNode() | |||
| self._irc = _ConfigNode() | |||
| self._commands = _ConfigNode() | |||
| self._tasks = _ConfigNode() | |||
| self._metadata = _ConfigNode() | |||
| self._nodes = [self._components, self._wiki, self._tasks, self._irc, | |||
| self._metadata] | |||
| self._nodes = [self._components, self._wiki, self._irc, self._commands, | |||
| self._tasks, self._metadata] | |||
| self._decryptable_nodes = [ # Default nodes to decrypt | |||
| (self._wiki, ("password",)), | |||
| @@ -196,16 +198,21 @@ class BotConfig(object): | |||
| return self._wiki | |||
| @property | |||
| def tasks(self): | |||
| """A dict of information for bot tasks.""" | |||
| return self._tasks | |||
| @property | |||
| def irc(self): | |||
| """A dict of information about IRC.""" | |||
| return self._irc | |||
| @property | |||
| def commands(self): | |||
| """A dict of information for IRC commands.""" | |||
| return self._commands | |||
| @property | |||
| def tasks(self): | |||
| """A dict of information for bot tasks.""" | |||
| return self._tasks | |||
| @property | |||
| def metadata(self): | |||
| """A dict of miscellaneous information.""" | |||
| return self._metadata | |||
| @@ -225,14 +232,14 @@ class BotConfig(object): | |||
| user. If there is no config file at all, offer to make one, otherwise | |||
| exit. | |||
| Data from the config file is stored in five | |||
| Data from the config file is stored in six | |||
| :py:class:`~earwigbot.config._ConfigNode`\ s (:py:attr:`components`, | |||
| :py:attr:`wiki`, :py:attr:`tasks`, :py:attr:`irc`, :py:attr:`metadata`) | |||
| for easy access (as well as the lower-level :py:attr:`data` attribute). | |||
| If passwords are encrypted, we'll use :py:func:`~getpass.getpass` for | |||
| the key and then decrypt them. If the config is being reloaded, | |||
| encrypted items will be automatically decrypted if they were decrypted | |||
| earlier. | |||
| :py:attr:`wiki`, :py:attr:`irc`, :py:attr:`commands`, :py:attr:`tasks`, | |||
| :py:attr:`metadata`) for easy access (as well as the lower-level | |||
| :py:attr:`data` attribute). If passwords are encrypted, we'll use | |||
| :py:func:`~getpass.getpass` for the key and then decrypt them. If the | |||
| config is being reloaded, encrypted items will be automatically | |||
| decrypted if they were decrypted earlier. | |||
| """ | |||
| if not path.exists(self._config_path): | |||
| print "Config file not found:", self._config_path | |||
| @@ -246,8 +253,9 @@ class BotConfig(object): | |||
| data = self._data | |||
| self.components._load(data.get("components", {})) | |||
| self.wiki._load(data.get("wiki", {})) | |||
| self.tasks._load(data.get("tasks", {})) | |||
| self.irc._load(data.get("irc", {})) | |||
| self.commands._load(data.get("commands", {})) | |||
| self.tasks._load(data.get("tasks", {})) | |||
| self.metadata._load(data.get("metadata", {})) | |||
| self._setup_logging() | |||