@@ -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 | ||||
@@ -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 | ||||
@@ -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): | ||||
@@ -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(' '): | if thing.endswith(' '): | ||||
@@ -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" | ||||
@@ -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"]: | ||||
@@ -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": | ||||
@@ -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": | ||||
@@ -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: | ||||
@@ -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.") | ||||
@@ -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): | ||||
@@ -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: | ||||
@@ -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": | ||||
@@ -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"]: | ||||
@@ -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: | ||||
@@ -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) |
@@ -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: | ||||
@@ -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)) | ||||
@@ -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) |
@@ -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): | ||||