@@ -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() | ||||