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