Browse Source

Command.setup() like Task.setup(); config.commands like config.tasks

tags/v0.1^2
Ben Kurtovic 12 years ago
parent
commit
cfdfc49d78
5 changed files with 92 additions and 35 deletions
  1. +18
    -5
      README.rst
  2. +23
    -4
      docs/customizing.rst
  3. +13
    -3
      earwigbot/commands/__init__.py
  4. +13
    -6
      earwigbot/commands/geolocate.py
  5. +25
    -17
      earwigbot/config.py

+ 18
- 5
README.rst View File

@@ -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.




+ 23
- 4
docs/customizing.rst View File

@@ -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.




+ 13
- 3
earwigbot/commands/__init__.py View File

@@ -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*.




+ 13
- 6
earwigbot/commands/geolocate.py View File

@@ -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:


+ 25
- 17
earwigbot/config.py View File

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


Loading…
Cancel
Save