diff --git a/README.rst b/README.rst index 65f5d37..f14c1d5 100644 --- a/README.rst +++ b/README.rst @@ -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. diff --git a/docs/customizing.rst b/docs/customizing.rst index fa4be59..f5b27f4 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -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 +`, +: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: `, and :py:meth:`part(chan) `. +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. diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index 7a70244..e3ba449 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -49,9 +49,10 @@ class BaseCommand(object): This is called once when the command is loaded (from :py:meth:`commands.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*. diff --git a/earwigbot/commands/geolocate.py b/earwigbot/commands/geolocate.py index 73dee91..67ba7c8 100644 --- a/earwigbot/commands/geolocate.py +++ b/earwigbot/commands/geolocate.py @@ -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: diff --git a/earwigbot/config.py b/earwigbot/config.py index cee3570..6e7387f 100644 --- a/earwigbot/config.py +++ b/earwigbot/config.py @@ -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()