Browse Source

Command.commands; small change in managers; !time

tags/v0.1^2
Ben Kurtovic 12 years ago
parent
commit
b34dd94f0d
20 changed files with 161 additions and 122 deletions
  1. +15
    -5
      README.rst
  2. +20
    -7
      docs/customizing.rst
  3. +12
    -4
      earwigbot/commands/__init__.py
  4. +0
    -5
      earwigbot/commands/_old.py
  5. +1
    -4
      earwigbot/commands/afc_pending.py
  6. +2
    -3
      earwigbot/commands/afc_status.py
  7. +1
    -4
      earwigbot/commands/chanops.py
  8. +1
    -4
      earwigbot/commands/crypt.py
  9. +1
    -4
      earwigbot/commands/editcount.py
  10. +1
    -4
      earwigbot/commands/geolocate.py
  11. +11
    -22
      earwigbot/commands/help.py
  12. +1
    -4
      earwigbot/commands/langcode.py
  13. +2
    -5
      earwigbot/commands/praise.py
  14. +1
    -4
      earwigbot/commands/quit.py
  15. +1
    -4
      earwigbot/commands/registration.py
  16. +3
    -10
      earwigbot/commands/remind.py
  17. +1
    -4
      earwigbot/commands/rights.py
  18. +3
    -6
      earwigbot/commands/threads.py
  19. +70
    -0
      earwigbot/commands/time.py
  20. +14
    -19
      earwigbot/managers.py

+ 15
- 5
README.rst View File

@@ -166,6 +166,13 @@ for and what they should be overridden with, but these are the basics:


- Class attribute ``name`` is the name of the command. This must be specified. - Class attribute ``name`` is the name of the command. This must be specified.


- Class attribute ``commands`` is a list of names that will trigger this
command. It defaults to the command's ``name``, but you can override it with
multiple names to serve as aliases. This is handled by the default
``check()`` implementation (see below), so if ``check()`` is overridden, this
is ignored by everything except the help_ command (so ``!help alias`` will
trigger help for the actual command).

- Class attribute ``hooks`` is a list of the "IRC events" that this command - Class attribute ``hooks`` is a list of the "IRC events" that this command
might respond to. It defaults to ``["msg"]``, but options include might respond to. It defaults to ``["msg"]``, but options include
``"msg_private"`` (for private messages only), ``"msg_public"`` (for channel ``"msg_private"`` (for private messages only), ``"msg_public"`` (for channel
@@ -181,10 +188,12 @@ for and what they should be overridden with, but these are the basics:
- 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
``True`` and ``data.command == self.name``, which is suitable for most cases.
A common, straightforward reason for overriding is if a command has aliases
(see chanops_ for an example). Note that by returning ``True``, you prevent
any other commands from responding to this message.
``True`` and ``data.command`` ``==`` ``self.name`` (or ``data.command`` is in
``self.commands`` if that list is overriden; see above), which is suitable
for most cases. A possible reason for overriding is if you want to do
something in response to events from a specific channel only. Note that by
returning ``True``, you prevent any other commands from responding to this
message.


- Method ``process()`` is passed the same ``Data`` object as ``check()``, but - Method ``process()`` is passed the same ``Data`` object as ``check()``, but
only if ``check()`` returned ``True``. This is where the bulk of your command only if ``check()`` returned ``True``. This is where the bulk of your command
@@ -230,7 +239,7 @@ and what they should be overridden with, but these are the basics:
``"([[WP:BOT|Bot]]; [[User:EarwigBot#Task $1|Task $1]]): $2"``, which the ``"([[WP:BOT|Bot]]; [[User:EarwigBot#Task $1|Task $1]]): $2"``, which the
task class's ``make_summary(comment)`` method will take and replace ``$1`` task class's ``make_summary(comment)`` method will take and replace ``$1``
with the task number and ``$2`` with the details of the edit. with the task number and ``$2`` with the details of the edit.
Additionally, ``shutoff_enabled()`` (which checks whether the bot has been Additionally, ``shutoff_enabled()`` (which checks whether the bot has been
told to stop on-wiki by checking the content of a particular page) can check told to stop on-wiki by checking the content of a particular page) can check
a different page for each task using similar variables. EarwigBot's a different page for each task using similar variables. EarwigBot's
@@ -510,6 +519,7 @@ Footnotes
.. _earwigbot.bot.Bot: https://github.com/earwig/earwigbot/blob/develop/earwigbot/bot.py .. _earwigbot.bot.Bot: https://github.com/earwig/earwigbot/blob/develop/earwigbot/bot.py
.. _earwigbot.config.BotConfig: https://github.com/earwig/earwigbot/blob/develop/earwigbot/config.py .. _earwigbot.config.BotConfig: https://github.com/earwig/earwigbot/blob/develop/earwigbot/config.py
.. _earwigbot.commands.BaseCommand: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/__init__.py .. _earwigbot.commands.BaseCommand: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/__init__.py
.. _help: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/help.py
.. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py .. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py
.. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py .. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py
.. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py .. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py


+ 20
- 7
docs/customizing.rst View File

@@ -96,6 +96,15 @@ these are the basics:
- Class attribute :py:attr:`~earwigbot.commands.BaseCommand.name` is the name - Class attribute :py:attr:`~earwigbot.commands.BaseCommand.name` is the name
of the command. This must be specified. of the command. This must be specified.


- Class attribute :py:attr:`~earwigbot.commands.BaseCommand.commands` is a list
of names that will trigger this command. It defaults to the command's
:py:attr:`~earwigbot.commands.BaseCommand.name`, but you can override it with
multiple names to serve as aliases. This is handled by the default
:py:meth:`~earwigbot.commands.BaseCommand.check` implementation (see below),
so if :py:meth:`~earwigbot.commands.BaseCommand.check` is overridden, this is
ignored by everything except the help_ command (so ``!help alias`` will
trigger help for the actual command).

- Class attribute :py:attr:`~earwigbot.commands.BaseCommand.hooks` is a list of - Class attribute :py:attr:`~earwigbot.commands.BaseCommand.hooks` is a list of
the "IRC events" that this command might respond to. It defaults to the "IRC events" that this command might respond to. It defaults to
``["msg"]``, but options include ``"msg_private"`` (for private messages ``["msg"]``, but options include ``"msg_private"`` (for private messages
@@ -113,12 +122,15 @@ these are the basics:
- 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
behavior is to return ``True`` only if
:py:attr:`data.is_command` is ``True`` and :py:attr:`data.command` ==
:py:attr:`~earwigbot.commands.BaseCommand.name`, which is suitable for most
cases. A common, straightforward reason for overriding is if a command has
aliases (see chanops_ for an example). Note that by returning ``True``, you
prevent any other commands from responding to this message.
behavior is to return ``True`` only if :py:attr:`data.is_command` is ``True``
and :py:attr:`data.command` ``==``
:py:attr:`~earwigbot.commands.BaseCommand.name` (or :py:attr:`data.command
<earwigbot.irc.data.Data.command>` is in
:py:attr:`~earwigbot.commands.BaseCommand.commands` if that list is
overriden; see above), which is suitable for most cases. A possible reason
for overriding is if you want to do something in response to events from a
specific channel only. Note that by returning ``True``, you prevent any other
commands from responding to this message.


- Method :py:meth:`~earwigbot.commands.BaseCommand.process` is passed the same - Method :py:meth:`~earwigbot.commands.BaseCommand.process` is passed the same
:py:class:`~earwigbot.irc.data.Data` object as :py:class:`~earwigbot.irc.data.Data` object as
@@ -179,7 +191,7 @@ are the basics:
task class's :py:meth:`make_summary(comment) task class's :py:meth:`make_summary(comment)
<earwigbot.tasks.BaseTask.make_summary>` method will take and replace <earwigbot.tasks.BaseTask.make_summary>` method will take and replace
``$1`` with the task number and ``$2`` with the details of the edit. ``$1`` with the task number and ``$2`` with the details of the edit.
Additionally, :py:meth:`~earwigbot.tasks.BaseTask.shutoff_enabled` (which Additionally, :py:meth:`~earwigbot.tasks.BaseTask.shutoff_enabled` (which
checks whether the bot has been told to stop on-wiki by checking the content checks whether the bot has been told to stop on-wiki by checking the content
of a particular page) can check a different page for each task using similar of a particular page) can check a different page for each task using similar
@@ -249,6 +261,7 @@ task, or the afc_statistics_ plugin for a more complicated one.
:py:attr:`~earwigbot.irc.data.Data.ident`, :py:attr:`~earwigbot.irc.data.Data.ident`,
and :py:attr:`~earwigbot.irc.data.Data.host`. and :py:attr:`~earwigbot.irc.data.Data.host`.


.. _help: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/help.py
.. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py .. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py
.. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py .. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py
.. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py .. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py


+ 12
- 4
earwigbot/commands/__init__.py View File

@@ -36,9 +36,13 @@ class BaseCommand(object):
This docstring is reported to the user when they type ``"!help This docstring is reported to the user when they type ``"!help
<command>"``. <command>"``.
""" """
# This is the command's name, as reported to the user when they use !help:
# The command's name, as reported to the user when they use !help:
name = None name = None


# A list of names that will trigger this command. If left empty, it will
# be triggered by the command's name and its name only:
commands = []

# Hooks are "msg", "msg_private", "msg_public", and "join". "msg" is the # Hooks are "msg", "msg_private", "msg_public", and "join". "msg" is the
# default behavior; if you wish to override that, change the value in your # default behavior; if you wish to override that, change the value in your
# command subclass: # command subclass:
@@ -86,11 +90,15 @@ class BaseCommand(object):
sent on IRC, it should be cheap to execute and unlikely to throw sent on IRC, it should be cheap to execute and unlikely to throw
exceptions. exceptions.


Most commands return ``True`` if :py:attr:`data.command
Most commands return ``True`` only if :py:attr:`data.command
<earwigbot.irc.data.Data.command>` ``==`` :py:attr:`self.name <name>`, <earwigbot.irc.data.Data.command>` ``==`` :py:attr:`self.name <name>`,
otherwise they return ``False``. This is the default behavior of
:py:meth:`check`; you need only override it if you wish to change that.
or :py:attr:`data.command <earwigbot.irc.data.Data.command>` is in
:py:attr:`self.commands <commands>` if that list is overriden. This is
the default behavior; you should only override it if you wish to change
that.
""" """
if self.commands:
return data.is_command and data.command in self.commands
return data.is_command and data.command == self.name return data.is_command and data.command == self.name


def process(self, data): def process(self, data):


+ 0
- 5
earwigbot/commands/_old.py View File

@@ -21,11 +21,6 @@ def parse(command, line, line2, nick, chan, host, auth, notice, say, reply, s):
u.close() u.close()
say('"' + info['Date'] + '" - tycho.usno.navy.mil', chan) say('"' + info['Date'] + '" - tycho.usno.navy.mil', chan)
return return
if command == "beats":
beats = ((time.time() + 3600) % 86400) / 86.4
beats = int(math.floor(beats))
say('@%03i' % beats, chan)
return
if command == "dict" or command == "dictionary": if command == "dict" or command == "dictionary":
def trim(thing): def trim(thing):
if thing.endswith('&nbsp;'): if thing.endswith('&nbsp;'):


+ 1
- 4
earwigbot/commands/afc_pending.py View File

@@ -25,10 +25,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
"""Link the user to the pending AFC submissions page and category.""" """Link the user to the pending AFC submissions page and category."""
name = "pending" name = "pending"

def check(self, data):
commands = ["pending", "pend"]
return data.is_command and data.command in commands
commands = ["pending", "pend"]


def process(self, data): def process(self, data):
msg1 = "pending submissions status page: http://enwp.org/WP:AFC/ST" msg1 = "pending submissions status page: http://enwp.org/WP:AFC/ST"


+ 2
- 3
earwigbot/commands/afc_status.py View File

@@ -28,13 +28,12 @@ class Command(BaseCommand):
"""Get the number of pending AfC submissions, open redirect requests, and """Get the number of pending AfC submissions, open redirect requests, and
open file upload requests.""" open file upload requests."""
name = "status" name = "status"
commands = ["status", "count", "num", "number"]
hooks = ["join", "msg"] hooks = ["join", "msg"]


def check(self, data): def check(self, data):
commands = ["status", "count", "num", "number"]
if data.is_command and data.command in commands:
if data.is_command and data.command in self.commands:
return True return True

try: try:
if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc": if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc":
if data.nick != self.config.irc["frontend"]["nick"]: if data.nick != self.config.irc["frontend"]["nick"]:


+ 1
- 4
earwigbot/commands/chanops.py View File

@@ -26,10 +26,7 @@ class Command(BaseCommand):
"""Voice, devoice, op, or deop users in the channel, or join or part from """Voice, devoice, op, or deop users in the channel, or join or part from
other channels.""" other channels."""
name = "chanops" name = "chanops"

def check(self, data):
cmnds = ["chanops", "voice", "devoice", "op", "deop", "join", "part"]
return data.is_command and data.command in cmnds
commands = ["chanops", "voice", "devoice", "op", "deop", "join", "part"]


def process(self, data): def process(self, data):
if data.command == "chanops": if data.command == "chanops":


+ 1
- 4
earwigbot/commands/crypt.py View File

@@ -30,10 +30,7 @@ class Command(BaseCommand):
"""Provides hash functions with !hash (!hash list for supported algorithms) """Provides hash functions with !hash (!hash list for supported algorithms)
and blowfish encryption with !encrypt and !decrypt.""" and blowfish encryption with !encrypt and !decrypt."""
name = "crypt" name = "crypt"

def check(self, data):
commands = ["crypt", "hash", "encrypt", "decrypt"]
return data.is_command and data.command in commands
commands = ["crypt", "hash", "encrypt", "decrypt"]


def process(self, data): def process(self, data):
if data.command == "crypt": if data.command == "crypt":


+ 1
- 4
earwigbot/commands/editcount.py View File

@@ -28,10 +28,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
"""Return a user's edit count.""" """Return a user's edit count."""
name = "editcount" name = "editcount"

def check(self, data):
commands = ["ec", "editcount"]
return data.is_command and data.command in commands
commands = ["ec", "editcount"]


def process(self, data): def process(self, data):
if not data.args: if not data.args:


+ 1
- 4
earwigbot/commands/geolocate.py View File

@@ -28,6 +28,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
"""Geolocate an IP address (via http://ipinfodb.com/).""" """Geolocate an IP address (via http://ipinfodb.com/)."""
name = "geolocate" name = "geolocate"
commands = ["geolocate", "locate", "geo", "ip"]


def setup(self): def setup(self):
self.config.decrypt(self.config.commands, (self.name, "apiKey")) self.config.decrypt(self.config.commands, (self.name, "apiKey"))
@@ -38,10 +39,6 @@ class Command(BaseCommand):
log = 'Cannot use without an API key for http://ipinfodb.com/ stored as config.commands["{0}"]["apiKey"]' log = 'Cannot use without an API key for http://ipinfodb.com/ stored as config.commands["{0}"]["apiKey"]'
self.logger.warn(log.format(self.name)) 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

def process(self, data): def process(self, data):
if not data.args: if not data.args:
self.reply(data, "please specify an IP to lookup.") self.reply(data, "please specify an IP to lookup.")


+ 11
- 22
earwigbot/commands/help.py View File

@@ -23,7 +23,6 @@
import re import re


from earwigbot.commands import BaseCommand from earwigbot.commands import BaseCommand
from earwigbot.irc import Data


class Command(BaseCommand): class Command(BaseCommand):
"""Displays help information.""" """Displays help information."""
@@ -48,34 +47,24 @@ class Command(BaseCommand):
def do_main_help(self, data): def do_main_help(self, data):
"""Give the user a general help message with a list of all commands.""" """Give the user a general help message with a list of all commands."""
msg = "Hi, I'm a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help <command>'." msg = "Hi, I'm a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help <command>'."
cmnds = sorted(self.bot.commands)
cmnds = sorted([cmnd.name for cmnd in self.bot.commands])
msg = msg.format(len(cmnds), ', '.join(cmnds)) msg = msg.format(len(cmnds), ', '.join(cmnds))
self.reply(data, msg) self.reply(data, msg)


def do_command_help(self, data): def do_command_help(self, data):
"""Give the user help for a specific command.""" """Give the user help for a specific command."""
command = data.args[0]
target = data.args[0]


# Create a dummy message to test which commands pick up the user's
# input:
msg = ":foo!bar@example.com PRIVMSG #channel :msg".split()
dummy = Data(self.bot, msg)
dummy.command = command.lower()
dummy.is_command = True
for command in self.bot.commands:
if command.name == target or target in command.commands:
if command.__doc__:
doc = command.__doc__.replace("\n", "")
doc = re.sub("\s\s+", " ", doc)
msg = "help for command \x0303{0}\x0301: \"{1}\""
self.reply(data, msg.format(target, doc))
return


for cmnd_name in self.bot.commands:
cmnd = self.bot.commands.get(cmnd_name)
if not cmnd.check(dummy):
continue
if cmnd.__doc__:
doc = cmnd.__doc__.replace("\n", "")
doc = re.sub("\s\s+", " ", doc)
msg = "help for command \x0303{0}\x0301: \"{1}\""
self.reply(data, msg.format(command, doc))
return
break

msg = "sorry, no help for \x0303{0}\x0301.".format(command)
msg = "sorry, no help for \x0303{0}\x0301.".format(target)
self.reply(data, msg) self.reply(data, msg)


def do_hello(self, data): def do_hello(self, data):


+ 1
- 4
earwigbot/commands/langcode.py View File

@@ -26,10 +26,7 @@ class Command(BaseCommand):
"""Convert a language code into its name and a list of WMF sites in that """Convert a language code into its name and a list of WMF sites in that
language.""" language."""
name = "langcode" name = "langcode"

def check(self, data):
commands = ["langcode", "lang", "language"]
return data.is_command and data.command in commands
commands = ["langcode", "lang", "language"]


def process(self, data): def process(self, data):
if not data.args: if not data.args:


+ 2
- 5
earwigbot/commands/praise.py View File

@@ -25,11 +25,8 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
"""Praise people!""" """Praise people!"""
name = "praise" name = "praise"

def check(self, data):
commands = ["praise", "earwig", "leonard", "leonard^bloom", "groove",
"groovedog"]
return data.is_command and data.command in commands
commands = ["praise", "earwig", "leonard", "leonard^bloom", "groove",
"groovedog"]


def process(self, data): def process(self, data):
if data.command == "earwig": if data.command == "earwig":


+ 1
- 4
earwigbot/commands/quit.py View File

@@ -26,10 +26,7 @@ class Command(BaseCommand):
"""Quit, restart, or reload components from the bot. Only the owners can """Quit, restart, or reload components from the bot. Only the owners can
run this command.""" run this command."""
name = "quit" name = "quit"

def check(self, data):
commands = ["quit", "restart", "reload"]
return data.is_command and data.command in commands
commands = ["quit", "restart", "reload"]


def process(self, data): def process(self, data):
if data.host not in self.config.irc["permissions"]["owners"]: if data.host not in self.config.irc["permissions"]["owners"]:


+ 1
- 4
earwigbot/commands/registration.py View File

@@ -28,10 +28,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
"""Return when a user registered.""" """Return when a user registered."""
name = "registration" name = "registration"

def check(self, data):
commands = ["registration", "reg", "age"]
return data.is_command and data.command in commands
commands = ["registration", "reg", "age"]


def process(self, data): def process(self, data):
if not data.args: if not data.args:


+ 3
- 10
earwigbot/commands/remind.py View File

@@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.


import threading
from threading import Timer
import time import time


from earwigbot.commands import BaseCommand from earwigbot.commands import BaseCommand
@@ -28,9 +28,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
"""Set a message to be repeated to you in a certain amount of time.""" """Set a message to be repeated to you in a certain amount of time."""
name = "remind" name = "remind"

def check(self, data):
return data.is_command and data.command in ["remind", "reminder"]
commands = ["remind", "reminder"]


def process(self, data): def process(self, data):
if not data.args: if not data.args:
@@ -58,12 +56,7 @@ class Command(BaseCommand):
msg = msg.format(message, wait, end_time_with_timezone) msg = msg.format(message, wait, end_time_with_timezone)
self.reply(data, msg) self.reply(data, msg)


t_reminder = threading.Thread(target=self.reminder,
args=(data, message, wait))
t_reminder = Timer(wait, self.reply, args=(data, message))
t_reminder.name = "reminder " + end_time t_reminder.name = "reminder " + end_time
t_reminder.daemon = True t_reminder.daemon = True
t_reminder.start() t_reminder.start()

def reminder(self, data, message, wait):
time.sleep(wait)
self.reply(data, message)

+ 1
- 4
earwigbot/commands/rights.py View File

@@ -26,10 +26,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
"""Retrieve a list of rights for a given username.""" """Retrieve a list of rights for a given username."""
name = "rights" name = "rights"

def check(self, data):
commands = ["rights", "groups", "permissions", "privileges"]
return data.is_command and data.command in commands
commands = ["rights", "groups", "permissions", "privileges"]


def process(self, data): def process(self, data):
if not data.args: if not data.args:


+ 3
- 6
earwigbot/commands/threads.py View File

@@ -29,10 +29,7 @@ from earwigbot.exceptions import KwargParseError
class Command(BaseCommand): class Command(BaseCommand):
"""Manage wiki tasks from IRC, and check on thread status.""" """Manage wiki tasks from IRC, and check on thread status."""
name = "threads" name = "threads"

def check(self, data):
commands = ["tasks", "task", "threads", "tasklist"]
return data.is_command and data.command in commands
commands = ["tasks", "task", "threads", "tasklist"]


def process(self, data): def process(self, data):
self.data = data self.data = data
@@ -103,7 +100,7 @@ class Command(BaseCommand):
whether they are currently running or idle.""" whether they are currently running or idle."""
threads = threading.enumerate() threads = threading.enumerate()
tasklist = [] tasklist = []
for task in sorted(self.bot.tasks):
for task in sorted([task.name for task in self.bot.tasks]):
threadlist = [t for t in threads if t.name.startswith(task)] threadlist = [t for t in threads if t.name.startswith(task)]
ids = [str(t.ident) for t in threadlist] ids = [str(t.ident) for t in threadlist]
if not ids: if not ids:
@@ -138,7 +135,7 @@ class Command(BaseCommand):
self.reply(data, msg) self.reply(data, msg)
return return


if task_name not in self.bot.tasks:
if task_name not in [task.name for task in self.bot.tasks]:
# This task does not exist or hasn't been loaded: # This task does not exist or hasn't been loaded:
msg = "task could not be found; either it doesn't exist, or it wasn't loaded correctly." msg = "task could not be found; either it doesn't exist, or it wasn't loaded correctly."
self.reply(data, msg.format(task_name)) self.reply(data, msg.format(task_name))


+ 70
- 0
earwigbot/commands/time.py View File

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from datetime import datetime, timedelta
from math import floor
from time import time

from earwigbot.commands import BaseCommand

class Command(BaseCommand):
"""Report the current time in any timezone (UTC default), or in beats."""
name = "time"
commands = ["time", "beats", "swatch"]
timezones = [
"UTC": 0,
"EST": -5,
"EDT": -4,
"CST": -6,
"CDT": -5,
"MST": -7,
"MDT": -6,
"PST": -8,
"PDT": -7,
]

def process(self, data):
if data.command in ["beats", "swatch"]:
self.do_beats(data)
return
if data.args:
timezone = data.args[0]
else:
timezone = "UTC"
if timezone in ["beats", "swatch"]:
self.do_beats(data)
else:
self.do_time(data, timezone)

def do_beats(self, data):
beats = ((time() + 3600) % 86400) / 86.4
beats = int(floor(beats))
self.reply(data, "@{0:0>3}".format(beats))

def do_time(self, data, timezone):
now = datetime.utcnow()
try:
now += timedelta(hours=self.timezones[timezone]) # Timezone offset
except KeyError:
self.reply(data, "unknown timezone: {0}.".format(timezone))
return
self.reply(data, now.strftime("%Y-%m-%d %H:%M:%S") + " " + timezone)

+ 14
- 19
earwigbot/managers.py View File

@@ -24,7 +24,7 @@
import imp import imp
from os import listdir, path from os import listdir, path
from re import sub from re import sub
from threading import Lock, Thread
from threading import RLock, Thread
from time import gmtime, strftime from time import gmtime, strftime


from earwigbot.commands import BaseCommand from earwigbot.commands import BaseCommand
@@ -46,11 +46,7 @@ class _ResourceManager(object):


This class handles the low-level tasks of (re)loading resources via This class handles the low-level tasks of (re)loading resources via
:py:meth:`load`, retrieving specific resources via :py:meth:`get`, and :py:meth:`load`, retrieving specific resources via :py:meth:`get`, and
iterating over all resources via :py:meth:`__iter__`. If iterating over
resources, it is recommended to acquire :py:attr:`self.lock <lock>`
beforehand and release it afterwards (alternatively, wrap your code in a
``with`` statement) so an attempt at reloading resources in another thread
won't disrupt your iteration.
iterating over all resources via :py:meth:`__iter__`.
""" """
def __init__(self, bot, name, attribute, base): def __init__(self, bot, name, attribute, base):
self.bot = bot self.bot = bot
@@ -60,16 +56,12 @@ class _ResourceManager(object):
self._resource_name = name # e.g. "commands" or "tasks" self._resource_name = name # e.g. "commands" or "tasks"
self._resource_attribute = attribute # e.g. "Command" or "Task" self._resource_attribute = attribute # e.g. "Command" or "Task"
self._resource_base = base # e.g. BaseCommand or BaseTask self._resource_base = base # e.g. BaseCommand or BaseTask
self._resource_access_lock = Lock()

@property
def lock(self):
"""The resource access/modify lock."""
return self._resource_access_lock
self._resource_access_lock = RLock()


def __iter__(self): def __iter__(self):
for name in self._resources:
yield name
with self.lock:
for resource in self._resources.itervalues():
yield resource


def _load_resource(self, name, path): def _load_resource(self, name, path):
"""Load a specific resource from a module, identified by name and path. """Load a specific resource from a module, identified by name and path.
@@ -118,6 +110,11 @@ class _ResourceManager(object):
self._load_resource(modname, dir) self._load_resource(modname, dir)
processed.append(modname) processed.append(modname)


@property
def lock(self):
"""The resource access/modify lock."""
return self._resource_access_lock

def load(self): def load(self):
"""Load (or reload) all valid resources into :py:attr:`_resources`.""" """Load (or reload) all valid resources into :py:attr:`_resources`."""
name = self._resource_name # e.g. "commands" or "tasks" name = self._resource_name # e.g. "commands" or "tasks"
@@ -138,7 +135,8 @@ class _ResourceManager(object):
Will raise :py:exc:`KeyError` if the resource (a command or task) is Will raise :py:exc:`KeyError` if the resource (a command or task) is
not found. not found.
""" """
return self._resources[key]
with self.lock:
return self._resources[key]




class CommandManager(_ResourceManager): class CommandManager(_ResourceManager):
@@ -167,13 +165,10 @@ class CommandManager(_ResourceManager):


def call(self, hook, data): def call(self, hook, data):
"""Respond to a hook type and a :py:class:`Data` object.""" """Respond to a hook type and a :py:class:`Data` object."""
self.lock.acquire()
for command in self._resources.itervalues():
for command in self:
if hook in command.hooks and self._wrap_check(command, data): if hook in command.hooks and self._wrap_check(command, data):
self.lock.release()
self._wrap_process(command, data) self._wrap_process(command, data)
return return
self.lock.release()




class TaskManager(_ResourceManager): class TaskManager(_ResourceManager):


Loading…
Cancel
Save