| @@ -138,9 +138,9 @@ The most useful attributes are: | |||||
| `earwigbot.config.BotConfig`_ stores configuration information for the bot. Its | `earwigbot.config.BotConfig`_ stores configuration information for the bot. Its | ||||
| docstring explains what each attribute is used for, but essentially each "node" | 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: | irc: | ||||
| frontend: | frontend: | ||||
| @@ -158,7 +158,8 @@ Custom IRC commands | |||||
| ~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~ | ||||
| Custom commands are subclasses of `earwigbot.commands.BaseCommand`_ that | 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 | ``BaseCommand``'s docstrings should explain what each attribute and method is | ||||
| for and what they should be overridden with, but these are the basics: | 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 | 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. | 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 | - Method ``check()`` is passed a ``Data`` [2]_ object, and should return | ||||
| ``True`` if you want to respond to this message, or ``False`` otherwise. The | ``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 | 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.action(chan_or_user, msg)``, ``self.notice(chan_or_user, msg)``, | ||||
| ``self.join(chan)``, and ``self.part(chan)``. | ``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 | 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 | 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 | 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 | 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 | 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 | user talk pages, so that these can be easily changed without modifying the task | ||||
| itself. | itself. | ||||
| @@ -58,8 +58,13 @@ The most useful attributes are: | |||||
| :py:class:`earwigbot.config.BotConfig` stores configuration information for the | :py:class:`earwigbot.config.BotConfig` stores configuration information for the | ||||
| bot. Its docstrings explains what each attribute is used for, but essentially | 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` | of the bot's :file:`config.yml` file. For example, if :file:`config.yml` | ||||
| includes something like:: | includes something like:: | ||||
| @@ -81,7 +86,8 @@ Custom IRC commands | |||||
| Custom commands are subclasses of :py:class:`earwigbot.commands.BaseCommand` | Custom commands are subclasses of :py:class:`earwigbot.commands.BaseCommand` | ||||
| that override :py:class:`~earwigbot.commands.BaseCommand`'s | that override :py:class:`~earwigbot.commands.BaseCommand`'s | ||||
| :py:meth:`~earwigbot.commands.BaseCommand.process` (and optionally | :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 | :py:class:`~earwigbot.commands.BaseCommand`'s docstrings should explain what | ||||
| each attribute and method is for and what they should be overridden with, but | 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 | a user joins a channel). See the afc_status_ plugin for a command that | ||||
| responds to other hook types. | 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 | - Method :py:meth:`~earwigbot.commands.BaseCommand.check` is passed a | ||||
| :py:class:`~earwigbot.irc.data.Data` [1]_ object, and should return ``True`` | :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 | 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 | <earwigbot.irc.connection.IRCConnection.join>`, and | ||||
| :py:meth:`part(chan) <earwigbot.irc.connection.IRCConnection.part>`. | :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, | 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 | 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 | 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, | 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 | 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 | or templates to append to user talk pages, so that these can be easily changed | ||||
| without modifying the task itself. | without modifying the task itself. | ||||
| @@ -49,9 +49,10 @@ class BaseCommand(object): | |||||
| This is called once when the command is loaded (from | This is called once when the command is loaded (from | ||||
| :py:meth:`commands.load() <earwigbot.managers._ResourceManager.load>`). | :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.bot = bot | ||||
| self.config = bot.config | 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.mode = lambda t, level, msg: self.bot.frontend.mode(t, level, msg) | ||||
| self.pong = lambda target: self.bot.frontend.pong(target) | 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): | def check(self, data): | ||||
| """Return whether this command should be called in response to *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/).""" | """Geolocate an IP address (via http://ipinfodb.com/).""" | ||||
| name = "geolocate" | 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): | def check(self, data): | ||||
| commands = ["geolocate", "locate", "geo", "ip"] | commands = ["geolocate", "locate", "geo", "ip"] | ||||
| return data.is_command and data.command in commands | 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.") | self.reply(data, "please specify an IP to lookup.") | ||||
| return | 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.reply(data, msg.format(self.name) + ".") | ||||
| self.logger.error(log.format(self.name)) | self.logger.error(log.format(self.name)) | ||||
| return | return | ||||
| address = data.args[0] | address = data.args[0] | ||||
| url = "http://api.ipinfodb.com/v3/ip-city/?key={0}&ip={1}&format=json" | 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) | res = json.loads(query) | ||||
| try: | try: | ||||
| @@ -48,8 +48,9 @@ class BotConfig(object): | |||||
| - :py:attr:`path`: path to the bot's config file | - :py:attr:`path`: path to the bot's config file | ||||
| - :py:attr:`components`: enabled components | - :py:attr:`components`: enabled components | ||||
| - :py:attr:`wiki`: information about wiki-editing | - :py:attr:`wiki`: information about wiki-editing | ||||
| - :py:attr:`tasks`: information for bot tasks | |||||
| - :py:attr:`irc`: information about IRC | - :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:attr:`metadata`: miscellaneous information | ||||
| - :py:meth:`schedule`: tasks scheduled to run at a given time | - :py:meth:`schedule`: tasks scheduled to run at a given time | ||||
| @@ -69,12 +70,13 @@ class BotConfig(object): | |||||
| self._components = _ConfigNode() | self._components = _ConfigNode() | ||||
| self._wiki = _ConfigNode() | self._wiki = _ConfigNode() | ||||
| self._tasks = _ConfigNode() | |||||
| self._irc = _ConfigNode() | self._irc = _ConfigNode() | ||||
| self._commands = _ConfigNode() | |||||
| self._tasks = _ConfigNode() | |||||
| self._metadata = _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._decryptable_nodes = [ # Default nodes to decrypt | ||||
| (self._wiki, ("password",)), | (self._wiki, ("password",)), | ||||
| @@ -196,16 +198,21 @@ class BotConfig(object): | |||||
| return self._wiki | return self._wiki | ||||
| @property | @property | ||||
| def tasks(self): | |||||
| """A dict of information for bot tasks.""" | |||||
| return self._tasks | |||||
| @property | |||||
| def irc(self): | def irc(self): | ||||
| """A dict of information about IRC.""" | """A dict of information about IRC.""" | ||||
| return self._irc | return self._irc | ||||
| @property | @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): | def metadata(self): | ||||
| """A dict of miscellaneous information.""" | """A dict of miscellaneous information.""" | ||||
| return self._metadata | return self._metadata | ||||
| @@ -225,14 +232,14 @@ class BotConfig(object): | |||||
| user. If there is no config file at all, offer to make one, otherwise | user. If there is no config file at all, offer to make one, otherwise | ||||
| exit. | 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: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): | if not path.exists(self._config_path): | ||||
| print "Config file not found:", self._config_path | print "Config file not found:", self._config_path | ||||
| @@ -246,8 +253,9 @@ class BotConfig(object): | |||||
| data = self._data | data = self._data | ||||
| self.components._load(data.get("components", {})) | self.components._load(data.get("components", {})) | ||||
| self.wiki._load(data.get("wiki", {})) | self.wiki._load(data.get("wiki", {})) | ||||
| self.tasks._load(data.get("tasks", {})) | |||||
| self.irc._load(data.get("irc", {})) | 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.metadata._load(data.get("metadata", {})) | ||||
| self._setup_logging() | self._setup_logging() | ||||