* _ResourceManager: allow resources to be named anything as long as they inherit from the base resource class; gave resources proper names. * Renamed BaseCommand to Command and BaseTask to Task; applied renames throughout earwigbot.commands and earwigbot.tasks. * Data: refactored argument and command parsing to be completely internal. Added docstrings to attributes. Applied changes to Frontend. * IRCConnection: improved such that we accurately detect disconnects with server pings; timeout support. Applied changes to Bot. * Updated documentation and other minor fixes.tags/v0.1^2
| @@ -78,44 +78,44 @@ and :py:attr:`config.irc["frontend"]["channels"]` will be | |||
| Custom IRC commands | |||
| ------------------- | |||
| Custom commands are subclasses of :py:class:`earwigbot.commands.BaseCommand` | |||
| that override :py:class:`~earwigbot.commands.BaseCommand`'s | |||
| :py:meth:`~earwigbot.commands.BaseCommand.process` (and optionally | |||
| :py:meth:`~earwigbot.commands.BaseCommand.check`) methods. | |||
| :py:class:`~earwigbot.commands.BaseCommand`'s docstrings should explain what | |||
| each attribute and method is for and what they should be overridden with, but | |||
| these are the basics: | |||
| - Class attribute :py:attr:`~earwigbot.commands.BaseCommand.name` is the name | |||
| of the command. This must be specified. | |||
| - Class attribute :py:attr:`~earwigbot.commands.BaseCommand.hooks` is a list of | |||
| the "IRC events" that this command might respond to. It defaults to | |||
| ``["msg"]``, but options include ``"msg_private"`` (for private messages | |||
| only), ``"msg_public"`` (for channel messages only), and ``"join"`` (for when | |||
| a user joins a channel). See the afc_status_ plugin for a command that | |||
| responds to other hook types. | |||
| - Method :py:meth:`~earwigbot.commands.BaseCommand.check` is passed a | |||
| :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 | |||
| 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. | |||
| - Method :py:meth:`~earwigbot.commands.BaseCommand.process` is passed the same | |||
| Custom commands are subclasses of :py:class:`earwigbot.commands.Command` that | |||
| override :py:class:`~earwigbot.commands.Command`'s | |||
| :py:meth:`~earwigbot.commands.Command.process` (and optionally | |||
| :py:meth:`~earwigbot.commands.Command.check`) methods. | |||
| :py:class:`~earwigbot.commands.Command`'s docstrings should explain what each | |||
| attribute and method is for and what they should be overridden with, but these | |||
| are the basics: | |||
| - Class attribute :py:attr:`~earwigbot.commands.Command.name` is the name of | |||
| the command. This must be specified. | |||
| - Class attribute :py:attr:`~earwigbot.commands.Command.hooks` is a list of the | |||
| "IRC events" that this command might respond to. It defaults to ``["msg"]``, | |||
| but options include ``"msg_private"`` (for private messages only), | |||
| ``"msg_public"`` (for channel messages only), and ``"join"`` (for when a user | |||
| joins a channel). See the afc_status_ plugin for a command that responds to | |||
| other hook types. | |||
| - Method :py:meth:`~earwigbot.commands.Command.check` is passed a | |||
| :py:class:`~earwigbot.irc.data.Data` object, and should return ``True`` 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.Command.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. | |||
| - Method :py:meth:`~earwigbot.commands.Command.process` is passed the same | |||
| :py:class:`~earwigbot.irc.data.Data` object as | |||
| :py:meth:`~earwigbot.commands.BaseCommand.check`, but only if | |||
| :py:meth:`~earwigbot.commands.BaseCommand.check` returned ``True``. This is | |||
| where the bulk of your command goes. To respond to IRC messages, there are a | |||
| number of methods of :py:class:`~earwigbot.commands.BaseCommand` at your | |||
| disposal. See the the test_ command for a simple example, or look in | |||
| :py:class:`~earwigbot.commands.BaseCommand`'s | |||
| :py:meth:`~earwigbot.commands.BaseCommand.__init__` method for the full list. | |||
| :py:meth:`~earwigbot.commands.Command.check`, but only if | |||
| :py:meth:`~earwigbot.commands.Command.check` returned ``True``. This is where | |||
| the bulk of your command goes. To respond to IRC messages, there are a number | |||
| of methods of :py:class:`~earwigbot.commands.Command` at your disposal. See | |||
| the test_ command for a simple example, or look in | |||
| :py:class:`~earwigbot.commands.Command`'s | |||
| :py:meth:`~earwigbot.commands.Command.__init__` method for the full list. | |||
| The most common ones are :py:meth:`say(chan_or_user, msg) | |||
| <earwigbot.irc.connection.IRCConnection.say>`, :py:meth:`reply(data, msg) | |||
| @@ -128,10 +128,10 @@ these are the basics: | |||
| <earwigbot.irc.connection.IRCConnection.join>`, and | |||
| :py:meth:`part(chan) <earwigbot.irc.connection.IRCConnection.part>`. | |||
| It's important to name the command class :py:class:`Command` within the file, | |||
| or else the bot might not recognize it as a command. The name of the file | |||
| doesn't really matter and need not match the command's name, but this is | |||
| recommended for readability. | |||
| The command *class* doesn't need a specific name, but it should logically | |||
| follow the command's name. The filename doesn't matter, but it is recommended | |||
| to match the command name for readability. Multiple command classes are allowed | |||
| in one file. | |||
| The bot has a wide selection of built-in commands and plugins to act as sample | |||
| code and/or to give ideas. Start with test_, and then check out chanops_ and | |||
| @@ -140,48 +140,48 @@ afc_status_ for some more complicated scripts. | |||
| Custom bot tasks | |||
| ---------------- | |||
| Custom tasks are subclasses of :py:class:`earwigbot.tasks.BaseTask` that | |||
| override :py:class:`~earwigbot.tasks.BaseTask`'s | |||
| :py:meth:`~earwigbot.tasks.BaseTask.run` (and optionally | |||
| :py:meth:`~earwigbot.tasks.BaseTask.setup`) methods. | |||
| Custom tasks are subclasses of :py:class:`earwigbot.tasks.Task` that | |||
| override :py:class:`~earwigbot.tasks.Task`'s | |||
| :py:meth:`~earwigbot.tasks.Task.run` (and optionally | |||
| :py:meth:`~earwigbot.tasks.Task.setup`) methods. | |||
| :py:class:`~earwigbot.tasks.BaseTask`'s docstrings should explain what each | |||
| :py:class:`~earwigbot.tasks.Task`'s docstrings should explain what each | |||
| attribute and method is for and what they should be overridden with, but these | |||
| are the basics: | |||
| - Class attribute :py:attr:`~earwigbot.tasks.BaseTask.name` is the name of the | |||
| - Class attribute :py:attr:`~earwigbot.tasks.Task.name` is the name of the | |||
| task. This must be specified. | |||
| - Class attribute :py:attr:`~earwigbot.tasks.BaseTask.number` can be used to | |||
| store an optional "task number", possibly for use in edit summaries (to be | |||
| generated with :py:meth:`~earwigbot.tasks.BaseTask.make_summary`). For | |||
| - Class attribute :py:attr:`~earwigbot.tasks.Task.number` can be used to store | |||
| an optional "task number", possibly for use in edit summaries (to be | |||
| generated with :py:meth:`~earwigbot.tasks.Task.make_summary`). For | |||
| example, EarwigBot's :py:attr:`config.wiki["summary"]` is | |||
| ``"([[WP:BOT|Bot]]; [[User:EarwigBot#Task $1|Task $1]]): $2"``, which the | |||
| task class's :py:meth:`make_summary(comment) | |||
| <earwigbot.tasks.BaseTask.make_summary>` method will take and replace | |||
| <earwigbot.tasks.Task.make_summary>` method will take and replace | |||
| ``$1`` with the task number and ``$2`` with the details of the edit. | |||
| Additionally, :py:meth:`~earwigbot.tasks.BaseTask.shutoff_enabled` (which | |||
| 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 | |||
| Additionally, :py:meth:`~earwigbot.tasks.Task.shutoff_enabled` (which 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 | |||
| variables. EarwigBot's :py:attr:`config.wiki["shutoff"]["page"]` is | |||
| ``"User:$1/Shutoff/Task $2"``; ``$1`` is substituted with the bot's username, | |||
| and ``$2`` is substituted with the task number, so, e.g., task #14 checks the | |||
| page ``[[User:EarwigBot/Shutoff/Task 14]].`` If the page's content does *not* | |||
| match :py:attr:`config.wiki["shutoff"]["disabled"]` (``"run"`` by default), | |||
| then shutoff is considered to be *enabled* and | |||
| :py:meth:`~earwigbot.tasks.BaseTask.shutoff_enabled` will return ``True``, | |||
| :py:meth:`~earwigbot.tasks.Task.shutoff_enabled` will return ``True``, | |||
| indicating the task should not run. If you don't intend to use either of | |||
| these methods, feel free to leave this attribute blank. | |||
| - Method :py:meth:`~earwigbot.tasks.BaseTask.setup` is called *once* with no | |||
| - Method :py:meth:`~earwigbot.tasks.Task.setup` is called *once* with no | |||
| arguments immediately after the task is first loaded. Does nothing by | |||
| default; treat it like an :py:meth:`__init__` if you want | |||
| (:py:meth:`~earwigbot.tasks.BaseTask.__init__` does things by default and a | |||
| (:py:meth:`~earwigbot.tasks.Task.__init__` does things by default and a | |||
| dedicated setup method is often easier than overriding | |||
| :py:meth:`~earwigbot.tasks.BaseTask.__init__` and using :py:obj:`super`). | |||
| :py:meth:`~earwigbot.tasks.Task.__init__` and using :py:obj:`super`). | |||
| - Method :py:meth:`~earwigbot.tasks.BaseTask.run` is called with any number of | |||
| - Method :py:meth:`~earwigbot.tasks.Task.run` is called with any number of | |||
| keyword arguments every time the task is executed (by | |||
| :py:meth:`tasks.start(task_name, **kwargs) | |||
| <earwigbot.managers.TaskManager.start>`, usually). This is where the bulk of | |||
| @@ -194,45 +194,15 @@ which is a node in :file:`config.yml` like every other attribute of | |||
| or templates to append to user talk pages, so that these can be easily changed | |||
| without modifying the task itself. | |||
| It's important to name the task class :py:class:`Task` within the file, or else | |||
| the bot might not recognize it as a task. The name of the file doesn't really | |||
| matter and need not match the task's name, but this is recommended for | |||
| readability. | |||
| The task *class* doesn't need a specific name, but it should logically follow | |||
| the task's name. The filename doesn't matter, but it is recommended to match | |||
| the task name for readability. Multiple tasks classes are allowed in one file. | |||
| See the built-in wikiproject_tagger_ task for a relatively straightforward | |||
| task, or the afc_statistics_ plugin for a more complicated one. | |||
| .. rubric:: Footnotes | |||
| .. [1] :py:class:`~earwigbot.irc.data.Data` objects are instances of | |||
| :py:class:`earwigbot.irc.Data <earwigbot.irc.data.Data>` that contain | |||
| information about a single message sent on IRC. Their useful attributes | |||
| are :py:attr:`~earwigbot.irc.data.Data.chan` (channel the message was | |||
| sent from, equal to :py:attr:`~earwigbot.irc.data.Data.nick` if it's a | |||
| private message), :py:attr:`~earwigbot.irc.data.Data.nick` (nickname of | |||
| the sender), :py:attr:`~earwigbot.irc.data.Data.ident` (ident_ of the | |||
| sender), :py:attr:`~earwigbot.irc.data.Data.host` (hostname of the | |||
| sender), :py:attr:`~earwigbot.irc.data.Data.msg` (text of the sent | |||
| message), :py:attr:`~earwigbot.irc.data.Data.is_command` (boolean | |||
| telling whether or not this message is a bot command, e.g., whether it | |||
| is prefixed by ``!``), :py:attr:`~earwigbot.irc.data.Data.command` (if | |||
| the message is a command, this is the name of the command used), and | |||
| :py:attr:`~earwigbot.irc.data.Data.args` (if the message is a command, | |||
| this is a list of the command arguments - for example, if issuing | |||
| "``!part ##earwig Goodbye guys``", | |||
| :py:attr:`~earwigbot.irc.data.Data.args` will equal | |||
| ``["##earwig", "Goodbye", "guys"]``). Note that not all | |||
| :py:class:`~earwigbot.irc.data.Data` objects will have all of these | |||
| attributes: :py:class:`~earwigbot.irc.data.Data` objects generated by | |||
| private messages will, but ones generated by joins will only have | |||
| :py:attr:`~earwigbot.irc.data.Data.chan`, | |||
| :py:attr:`~earwigbot.irc.data.Data.nick`, | |||
| :py:attr:`~earwigbot.irc.data.Data.ident`, | |||
| and :py:attr:`~earwigbot.irc.data.Data.host`. | |||
| .. _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 | |||
| .. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py | |||
| .. _wikiproject_tagger: https://github.com/earwig/earwigbot/blob/develop/earwigbot/tasks/wikiproject_tagger.py | |||
| .. _afc_statistics: https://github.com/earwig/earwigbot-plugins/blob/develop/tasks/afc_statistics.py | |||
| .. _ident: http://en.wikipedia.org/wiki/Ident | |||
| @@ -75,17 +75,20 @@ class Bot(object): | |||
| self.commands.load() | |||
| self.tasks.load() | |||
| def _dispatch_irc_component(self, name, klass): | |||
| """Create a new IRC component, record it internally, and start it.""" | |||
| component = klass(self) | |||
| setattr(self, name, component) | |||
| Thread(name="irc_" + name, target=component.loop).start() | |||
| def _start_irc_components(self): | |||
| """Start the IRC frontend/watcher in separate threads if enabled.""" | |||
| if self.config.components.get("irc_frontend"): | |||
| self.logger.info("Starting IRC frontend") | |||
| self.frontend = Frontend(self) | |||
| Thread(name="irc_frontend", target=self.frontend.loop).start() | |||
| self._dispatch_irc_component("frontend", Frontend) | |||
| if self.config.components.get("irc_watcher"): | |||
| self.logger.info("Starting IRC watcher") | |||
| self.watcher = Watcher(self) | |||
| Thread(name="irc_watcher", target=self.watcher.loop).start() | |||
| self._dispatch_irc_component("watcher", Watcher) | |||
| def _start_wiki_scheduler(self): | |||
| """Start the wiki scheduler in a separate thread if enabled.""" | |||
| @@ -104,6 +107,16 @@ class Bot(object): | |||
| thread.daemon = True # Stop if other threads stop | |||
| thread.start() | |||
| def _keep_irc_component_alive(self, name, klass): | |||
| """Ensure that IRC components stay connected, else restart them.""" | |||
| component = getattr(self, name) | |||
| if component: | |||
| component.keep_alive() | |||
| if component.is_stopped(): | |||
| log = "IRC {0} has stopped; restarting".format(name) | |||
| self.logger.warn(log) | |||
| self._dispatch_irc_component(name, klass) | |||
| def _stop_irc_components(self, msg): | |||
| """Request the IRC frontend and watcher to stop if enabled.""" | |||
| if self.frontend: | |||
| @@ -148,16 +161,8 @@ class Bot(object): | |||
| self._start_wiki_scheduler() | |||
| while self._keep_looping: | |||
| with self.component_lock: | |||
| if self.frontend and self.frontend.is_stopped(): | |||
| name = "irc_frontend" | |||
| self.logger.warn("IRC frontend has stopped; restarting") | |||
| self.frontend = Frontend(self) | |||
| Thread(name=name, target=self.frontend.loop).start() | |||
| if self.watcher and self.watcher.is_stopped(): | |||
| name = "irc_watcher" | |||
| self.logger.warn("IRC watcher has stopped; restarting") | |||
| self.watcher = Watcher(self) | |||
| Thread(name=name, target=self.watcher.loop).start() | |||
| self._keep_irc_component_alive("frontend", Frontend) | |||
| self._keep_irc_component_alive("watcher", Watcher) | |||
| sleep(2) | |||
| def restart(self, msg=None): | |||
| @@ -20,9 +20,9 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| __all__ = ["BaseCommand"] | |||
| __all__ = ["Command"] | |||
| class BaseCommand(object): | |||
| class Command(object): | |||
| """ | |||
| **EarwigBot: Base IRC Command** | |||
| @@ -30,8 +30,8 @@ class BaseCommand(object): | |||
| component. Additional commands can be installed as plugins in the bot's | |||
| working directory. | |||
| This class (import with ``from earwigbot.commands import BaseCommand``), | |||
| can be subclassed to create custom IRC commands. | |||
| This class (import with ``from earwigbot.commands import Command``), can be | |||
| subclassed to create custom IRC commands. | |||
| This docstring is reported to the user when they type ``"!help | |||
| <command>"``. | |||
| @@ -21,9 +21,11 @@ | |||
| # SOFTWARE. | |||
| from earwigbot import wiki | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["AFCReport"] | |||
| class AFCReport(Command): | |||
| """Get information about an AFC submission by name.""" | |||
| name = "report" | |||
| @@ -22,9 +22,11 @@ | |||
| import re | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["AFCStatus"] | |||
| class AFCStatus(Command): | |||
| """Get the number of pending AfC submissions, open redirect requests, and | |||
| open file upload requests.""" | |||
| name = "status" | |||
| @@ -23,9 +23,11 @@ | |||
| import re | |||
| import urllib | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Calc"] | |||
| class Calc(Command): | |||
| """A somewhat advanced calculator: see http://futureboy.us/fsp/frink.fsp | |||
| for details.""" | |||
| name = "calc" | |||
| @@ -20,9 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["ChanOps"] | |||
| class ChanOps(Command): | |||
| """Voice, devoice, op, or deop users in the channel, or join or part from | |||
| other channels.""" | |||
| name = "chanops" | |||
| @@ -24,9 +24,11 @@ import hashlib | |||
| from Crypto.Cipher import Blowfish | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Crypt"] | |||
| class Crypt(Command): | |||
| """Provides hash functions with !hash (!hash list for supported algorithms) | |||
| and blowfish encryption with !encrypt and !decrypt.""" | |||
| name = "crypt" | |||
| @@ -24,9 +24,11 @@ import platform | |||
| import time | |||
| from earwigbot import __version__ | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["CTCP"] | |||
| class CTCP(Command): | |||
| """Not an actual command; this module implements responses to the CTCP | |||
| requests PING, TIME, and VERSION.""" | |||
| name = "ctcp" | |||
| @@ -23,9 +23,11 @@ | |||
| from urllib import quote_plus | |||
| from earwigbot import exceptions | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Editcount"] | |||
| class Editcount(Command): | |||
| """Return a user's edit count.""" | |||
| name = "editcount" | |||
| @@ -24,9 +24,11 @@ import time | |||
| import git | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Git"] | |||
| class Git(Command): | |||
| """Commands to interface with the bot's git repository; use '!git' for a | |||
| sub-command list.""" | |||
| name = "git" | |||
| @@ -22,10 +22,12 @@ | |||
| import re | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| from earwigbot.irc import Data | |||
| class Command(BaseCommand): | |||
| __all__ = ["Help"] | |||
| class Help(Command): | |||
| """Displays help information.""" | |||
| name = "help" | |||
| @@ -23,9 +23,11 @@ | |||
| import re | |||
| from urllib import quote | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Link"] | |||
| class Link(Command): | |||
| """Convert a Wikipedia page name into a URL.""" | |||
| name = "link" | |||
| @@ -20,9 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Praise"] | |||
| class Praise(Command): | |||
| """Praise people!""" | |||
| name = "praise" | |||
| @@ -20,9 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Quit"] | |||
| class Quit(Command): | |||
| """Quit, restart, or reload components from the bot. Only the owners can | |||
| run this command.""" | |||
| name = "quit" | |||
| @@ -23,9 +23,11 @@ | |||
| import time | |||
| from earwigbot import exceptions | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Registration"] | |||
| class Registration(Command): | |||
| """Return when a user registered.""" | |||
| name = "registration" | |||
| @@ -23,9 +23,11 @@ | |||
| import threading | |||
| import time | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Remind"] | |||
| class Remind(Command): | |||
| """Set a message to be repeated to you in a certain amount of time.""" | |||
| name = "remind" | |||
| @@ -24,9 +24,11 @@ from os.path import expanduser | |||
| import oursql | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Replag"] | |||
| class Replag(Command): | |||
| """Return the replag for a specific database on the Toolserver.""" | |||
| name = "replag" | |||
| @@ -21,9 +21,11 @@ | |||
| # SOFTWARE. | |||
| from earwigbot import exceptions | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Rights"] | |||
| class Rights(Command): | |||
| """Retrieve a list of rights for a given username.""" | |||
| name = "rights" | |||
| @@ -22,14 +22,16 @@ | |||
| import random | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Test"] | |||
| class Test(Command): | |||
| """Test the bot!""" | |||
| name = "test" | |||
| def process(self, data): | |||
| user = "\x02{0}\x0F".format(data.nick) | |||
| user = "\x02" + data.nick + "\x0F" # Wrap nick in bold | |||
| hey = random.randint(0, 1) | |||
| if hey: | |||
| self.say(data.chan, "Hey {0}!".format(user)) | |||
| @@ -23,10 +23,11 @@ | |||
| import threading | |||
| import re | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.exceptions import KwargParseError | |||
| from earwigbot.commands import Command | |||
| class Command(BaseCommand): | |||
| __all__ = ["Threads"] | |||
| class Threads(Command): | |||
| """Manage wiki tasks from IRC, and check on thread status.""" | |||
| name = "threads" | |||
| @@ -133,13 +134,6 @@ class Command(BaseCommand): | |||
| self.reply(data, "what task do you want me to start?") | |||
| return | |||
| try: | |||
| data.parse_kwargs() | |||
| except KwargParseError, arg: | |||
| msg = "error parsing argument: \x0303{0}\x0301.".format(arg) | |||
| self.reply(data, msg) | |||
| return | |||
| if task_name not in self.bot.tasks: | |||
| # 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." | |||
| @@ -29,7 +29,6 @@ This module contains all exceptions used by EarwigBot:: | |||
| +-- NoConfigError | |||
| +-- IRCError | |||
| | +-- BrokenSocketError | |||
| | +-- KwargParseError | |||
| +-- WikiToolsetError | |||
| +-- SiteNotFoundError | |||
| +-- SiteAPIError | |||
| @@ -73,17 +72,6 @@ class BrokenSocketError(IRCError): | |||
| <earwigbot.irc.connection.IRCConnection._get>`. | |||
| """ | |||
| class KwargParseError(IRCError): | |||
| """Couldn't parse a certain keyword argument in an IRC message. | |||
| This is usually caused by it being given incorrectly: e.g., no value (abc), | |||
| just a value (=xyz), just an equal sign (=), instead of the correct form | |||
| (abc=xyz). | |||
| Raised by :py:meth:`Data.parse_kwargs | |||
| <earwigbot.irc.data.Data.parse_kwargs>`. | |||
| """ | |||
| class WikiToolsetError(EarwigBotError): | |||
| """Base exception class for errors in the Wiki Toolset.""" | |||
| @@ -22,7 +22,7 @@ | |||
| import socket | |||
| from threading import Lock | |||
| from time import sleep | |||
| from time import sleep, time | |||
| from earwigbot.exceptions import BrokenSocketError | |||
| @@ -32,16 +32,18 @@ class IRCConnection(object): | |||
| """Interface with an IRC server.""" | |||
| def __init__(self, host, port, nick, ident, realname): | |||
| self.host = host | |||
| self.port = port | |||
| self.nick = nick | |||
| self.ident = ident | |||
| self.realname = realname | |||
| self._is_running = False | |||
| self._host = host | |||
| self._port = port | |||
| self._nick = nick | |||
| self._ident = ident | |||
| self._realname = realname | |||
| # A lock to prevent us from sending two messages at once: | |||
| self._is_running = False | |||
| self._send_lock = Lock() | |||
| self._last_recv = time() | |||
| self._last_ping = 0 | |||
| def _connect(self): | |||
| """Connect to our IRC server.""" | |||
| self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| @@ -55,7 +57,7 @@ class IRCConnection(object): | |||
| self._send("USER {0} {1} * :{2}".format(self.ident, self.host, self.realname)) | |||
| def _close(self): | |||
| """Close our connection with the IRC server.""" | |||
| """Completely close our connection with the IRC server.""" | |||
| try: | |||
| self._sock.shutdown(socket.SHUT_RDWR) # Shut down connection first | |||
| except socket.error: | |||
| @@ -73,16 +75,48 @@ class IRCConnection(object): | |||
| def _send(self, msg): | |||
| """Send data to the server.""" | |||
| with self._send_lock: | |||
| self._sock.sendall(msg + "\r\n") | |||
| self.logger.debug(msg) | |||
| try: | |||
| self._sock.sendall(msg + "\r\n") | |||
| except socket.error: | |||
| self._is_running = False | |||
| else: | |||
| self.logger.debug(msg) | |||
| def _quit(self, msg=None): | |||
| """Issue a quit message to the server.""" | |||
| """Issue a quit message to the server. Doesn't close the connection.""" | |||
| if msg: | |||
| self._send("QUIT :{0}".format(msg)) | |||
| else: | |||
| self._send("QUIT") | |||
| @property | |||
| def host(self): | |||
| """The hostname of the IRC server, like ``"irc.freenode.net"``.""" | |||
| return self._host | |||
| @property | |||
| def port(self): | |||
| """The port of the IRC server, like ``6667``.""" | |||
| return self._port | |||
| @property | |||
| def nick(self): | |||
| """Our nickname on the server, like ``"EarwigBot"``.""" | |||
| return self._nick | |||
| @property | |||
| def ident(self): | |||
| """Our ident on the server, like ``"earwig"``. | |||
| See `http://en.wikipedia.org/wiki/Ident`_. | |||
| """ | |||
| return self._ident | |||
| @property | |||
| def realname(self): | |||
| """Our realname (gecos field) on the server.""" | |||
| return self._realname | |||
| def say(self, target, msg): | |||
| """Send a private message to a target on the server.""" | |||
| msg = "PRIVMSG {0} :{1}".format(target, msg) | |||
| @@ -120,6 +154,11 @@ class IRCConnection(object): | |||
| msg = "MODE {0} {1} {2}".format(target, level, msg) | |||
| self._send(msg) | |||
| def ping(self, target): | |||
| """Ping another entity on the server.""" | |||
| msg = "PING {0} {0}".format(target) | |||
| self._send(msg) | |||
| def pong(self, target): | |||
| """Pong another entity on the server.""" | |||
| msg = "PONG {0}".format(target) | |||
| @@ -136,14 +175,26 @@ class IRCConnection(object): | |||
| self._is_running = False | |||
| break | |||
| self._last_recv = time() | |||
| lines = read_buffer.split("\n") | |||
| read_buffer = lines.pop() | |||
| for line in lines: | |||
| self._process_message(line) | |||
| if self.is_stopped(): | |||
| self._close() | |||
| break | |||
| self._close() | |||
| def keep_alive(self): | |||
| """Ensure that we stay connected, stopping if the connection breaks.""" | |||
| now = time() | |||
| if now - self._last_recv > 60: | |||
| if self._last_ping < self._last_recv: | |||
| self.ping(self.host) | |||
| self._last_ping = now | |||
| elif now - self._last_ping > 60: | |||
| self.stop() | |||
| def stop(self, msg=None): | |||
| """Request the IRC connection to close at earliest convenience.""" | |||
| if self._is_running: | |||
| @@ -22,72 +22,172 @@ | |||
| import re | |||
| from earwigbot.exceptions import KwargParseError | |||
| __all__ = ["Data"] | |||
| class Data(object): | |||
| """Store data from an individual line received on IRC.""" | |||
| def __init__(self, bot, line): | |||
| self.line = line | |||
| self.my_nick = bot.config.irc["frontend"]["nick"].lower() | |||
| self.chan = self.nick = self.ident = self.host = self.msg = "" | |||
| def __init__(self, bot, my_nick, line, msgtype): | |||
| self._bot = bot | |||
| self._my_nick = my_nick | |||
| self._line = line | |||
| def parse_args(self): | |||
| """Parse command arguments from the message. | |||
| self._is_private = self._is_command = False | |||
| self._msg = self._command = self._trigger = None | |||
| self._args = [] | |||
| self._kwargs = {} | |||
| self._parse(msgtype) | |||
| def _parse(self, msgtype): | |||
| """Parse a line from IRC into its components as instance attributes.""" | |||
| sender = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] | |||
| self._nick, self._ident, self._host = sender | |||
| self._chan = self.line[2] | |||
| :py:attr:`self.msg <msg>` is converted into the string | |||
| :py:attr:`self.command <command>` and the argument list | |||
| :py:attr:`self.args <args>` if the message starts with a "trigger" | |||
| (``"!"``, ``"."``, or the bot's name); :py:attr:`self.is_command | |||
| <is_command>` will be set to ``True``, and :py:attr:`self.trigger | |||
| <trigger>` will store the trigger string. Otherwise, | |||
| :py:attr:`is_command` will be set to ``False``.""" | |||
| args = self.msg.strip().split() | |||
| if msgtype == "PRIVMSG": | |||
| if self.chan == self.my_nick: | |||
| # This is a privmsg to us, so set 'chan' as the nick of the | |||
| # sender instead of the 'channel', which is ourselves: | |||
| self._chan = self._nick | |||
| self._is_private = True | |||
| self._msg = " ".join(line[3:])[1:] | |||
| self._parse_args() | |||
| self._parse_kwargs() | |||
| while "" in args: | |||
| args.remove("") | |||
| def _parse_args(self): | |||
| """Parse command arguments from the message. | |||
| # Isolate command arguments: | |||
| self.args = args[1:] | |||
| self.is_command = False # Is this message a command? | |||
| self.trigger = None # What triggered this command? (!, ., or our nick) | |||
| self.msg is converted into the string self.command and the argument | |||
| list self.args if the message starts with a "trigger" ("!", ".", or the | |||
| bot's name); self.is_command will be set to True, and self.trigger will | |||
| store the trigger string. Otherwise, is_command will be set to False. | |||
| """ | |||
| self._args = self.msg.strip().split()[1:] | |||
| try: | |||
| self.command = args[0].lower() | |||
| self._command = args[0].lower() | |||
| except IndexError: | |||
| self.command = None | |||
| return | |||
| if self.command.startswith("!") or self.command.startswith("."): | |||
| # e.g. "!command arg1 arg2" | |||
| self.is_command = True | |||
| self.trigger = self.command[0] | |||
| self.command = self.command[1:] # Strip the "!" or "." | |||
| self._is_command = True | |||
| self._trigger = self.command[0] | |||
| self._command = self.command[1:] # Strip the "!" or "." | |||
| elif self.command.startswith(self.my_nick): | |||
| # e.g. "EarwigBot, command arg1 arg2" | |||
| self.is_command = True | |||
| self.trigger = self.my_nick | |||
| self._is_command = True | |||
| self._trigger = self.my_nick | |||
| try: | |||
| self.command = self.args.pop(0).lower() | |||
| self._command = self.args.pop(0).lower() | |||
| except IndexError: | |||
| self.command = "" | |||
| self._command = "" | |||
| def parse_kwargs(self): | |||
| """Parse keyword arguments embedded in :py:attr:`self.args <args>`. | |||
| def _parse_kwargs(self): | |||
| """Parse keyword arguments embedded in self.args. | |||
| Parse a command given as ``"!command key1=value1 key2=value2..."`` | |||
| into a dict, :py:attr:`self.kwargs <kwargs>`, like | |||
| ``{'key1': 'value2', 'key2': 'value2'...}``. | |||
| Parse a command given as "!command key1=value1 key2=value2..." into a | |||
| dict, self.kwargs, like {'key1': 'value2', 'key2': 'value2'...}. | |||
| """ | |||
| self.kwargs = {} | |||
| for arg in self.args[2:]: | |||
| try: | |||
| key, value = re.findall("^(.*?)\=(.*?)$", arg)[0] | |||
| except IndexError: | |||
| raise KwargParseError(arg) | |||
| pass | |||
| if key and value: | |||
| self.kwargs[key] = value | |||
| else: | |||
| raise KwargParseError(arg) | |||
| @property | |||
| def my_nick(self): | |||
| """Our nickname, *not* the nickname of the sender.""" | |||
| return self._my_nick | |||
| @property | |||
| def line(self): | |||
| """The full message received on IRC, including escape characters.""" | |||
| return self._line | |||
| @property | |||
| def chan(self): | |||
| """Channel the message was sent from. | |||
| This will be equal to :py:attr:`nick` if the message is a private | |||
| message. | |||
| """ | |||
| return self._chan | |||
| @property | |||
| def nick(self): | |||
| """Nickname of the sender.""" | |||
| return self._nick | |||
| @property | |||
| def ident(self): | |||
| """`Ident <http://en.wikipedia.org/wiki/Ident>`_ of the sender.""" | |||
| return self._ident | |||
| @property | |||
| def host(self): | |||
| """Hostname of the sender.""" | |||
| return self._host | |||
| @property | |||
| def msg(self): | |||
| """Text of the sent message, if it is a message, else ``None``.""" | |||
| return self._msg | |||
| @property | |||
| def is_private(self): | |||
| """``True`` if this message was sent to us *only*, else ``False``.""" | |||
| return self._is_private | |||
| @property | |||
| def is_command(self): | |||
| """Boolean telling whether or not this message is a bot command. | |||
| A message is considered a command if and only if it begins with the | |||
| character ``"!"``, ``"."``, or the bot's name followed by optional | |||
| punctuation and a space (so ``EarwigBot: do something``, ``EarwigBot, | |||
| do something``, and ``EarwigBot do something`` are all valid). | |||
| """ | |||
| return self._is_command | |||
| @property | |||
| def command(self): | |||
| """If the message is a command, this is the name of the command used. | |||
| See :py:attr:`is_command <self.is_command>` for when a message is | |||
| considered a command. If it's not a command, this will be set to | |||
| ``None``. | |||
| """ | |||
| return self._command | |||
| @property | |||
| def trigger(self): | |||
| """If this message is a command, this is what triggered it. | |||
| It can be either "!" (``"!help"``), "." (``".help"``), or the bot's | |||
| name (``"EarwigBot: help"``). Otherwise, it will be ``None``.""" | |||
| return self._trigger | |||
| @property | |||
| def args(self): | |||
| """List of all arguments given to this command. | |||
| For example, the message ``"!command arg1 arg2 arg3=val3"`` will | |||
| produce the args ``["arg1", "arg2", "arg3=val3"]``. This is empty if | |||
| the message was not a command or if it doesn't have arguments. | |||
| """ | |||
| return self._args | |||
| @property | |||
| def kwargs(self): | |||
| """Dictionary of keyword arguments given to this command. | |||
| For example, the message ``"!command arg1=val1 arg2=val2"`` will | |||
| produce the kwargs ``{"arg1": "val1", "arg2": "val2"}``. This is empty | |||
| if the message was not a command or if it doesn't have keyword | |||
| arguments. | |||
| """ | |||
| return self._kwargs | |||
| @@ -20,8 +20,6 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| import re | |||
| from earwigbot.irc import IRCConnection, Data | |||
| __all__ = ["Frontend"] | |||
| @@ -32,13 +30,12 @@ class Frontend(IRCConnection): | |||
| The IRC frontend runs on a normal IRC server and expects users to interact | |||
| with it and give it commands. Commands are stored as "command classes", | |||
| subclasses of :py:class:`~earwigbot.commands.BaseCommand`. All command | |||
| classes are automatically imported by :py:meth:`commands.load() | |||
| subclasses of :py:class:`~earwigbot.commands.Command`. All command classes | |||
| are automatically imported by :py:meth:`commands.load() | |||
| <earwigbot.managers._ResourceManager.load>` if they are in | |||
| :py:mod:`earwigbot.commands` or the bot's custom command directory | |||
| (explained in the :doc:`documentation </customizing>`). | |||
| """ | |||
| sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| @@ -53,30 +50,17 @@ class Frontend(IRCConnection): | |||
| def _process_message(self, line): | |||
| """Process a single message from IRC.""" | |||
| line = line.strip().split() | |||
| data = Data(self.bot, line) | |||
| if line[1] == "JOIN": | |||
| data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] | |||
| data.chan = line[2] | |||
| data.parse_args() | |||
| data = Data(self.bot, self.nick, line, msgtype="JOIN") | |||
| self.bot.commands.call("join", data) | |||
| elif line[1] == "PRIVMSG": | |||
| data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] | |||
| data.msg = " ".join(line[3:])[1:] | |||
| data.chan = line[2] | |||
| data.parse_args() | |||
| if data.chan == self.bot.config.irc["frontend"]["nick"]: | |||
| # This is a privmsg to us, so set 'chan' as the nick of the | |||
| # sender, then check for private-only command hooks: | |||
| data.chan = data.nick | |||
| data = Data(self.bot, self.nick, line, msgtype="PRIVMSG") | |||
| if data.is_private: | |||
| self.bot.commands.call("msg_private", data) | |||
| else: | |||
| # Check for public-only command hooks: | |||
| self.bot.commands.call("msg_public", data) | |||
| # Check for command hooks that apply to all messages: | |||
| self.bot.commands.call("msg", data) | |||
| elif line[0] == "PING": # If we are pinged, pong back | |||
| @@ -27,8 +27,8 @@ from re import sub | |||
| from threading import Lock, Thread | |||
| from time import gmtime, strftime | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.commands import Command | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["CommandManager", "TaskManager"] | |||
| @@ -52,32 +52,40 @@ class _ResourceManager(object): | |||
| ``with`` statement) so an attempt at reloading resources in another thread | |||
| won't disrupt your iteration. | |||
| """ | |||
| def __init__(self, bot, name, attribute, base): | |||
| def __init__(self, bot, name, base): | |||
| self.bot = bot | |||
| self.logger = bot.logger.getChild(name) | |||
| self._resources = {} | |||
| self._resource_name = name # e.g. "commands" or "tasks" | |||
| self._resource_attribute = attribute # e.g. "Command" or "Task" | |||
| self._resource_base = base # e.g. BaseCommand or BaseTask | |||
| self._resource_base = base # e.g. Command or Task | |||
| self._resource_access_lock = Lock() | |||
| @property | |||
| def lock(self): | |||
| """The resource access/modify lock.""" | |||
| return self._resource_access_lock | |||
| def __iter__(self): | |||
| for name in self._resources: | |||
| yield name | |||
| def _load_resource(self, name, path): | |||
| def _load_resource(self, name, path, klass): | |||
| """Instantiate a resource class and add it to the dictionary.""" | |||
| res_type = self._resource_name[:-1] # e.g. "command" or "task" | |||
| try: | |||
| resource = klass(self.bot) # Create instance of resource | |||
| except Exception: | |||
| e = "Error instantiating {0} class in {1} (from {2})" | |||
| self.logger.exception(e.format(res_type, name, path)) | |||
| else: | |||
| self._resources[resource.name] = resource | |||
| self.logger.debug("Loaded {0} {1}".format(res_type, resource.name)) | |||
| def _load_module(self, name, path): | |||
| """Load a specific resource from a module, identified by name and path. | |||
| We'll first try to import it using imp magic, and if that works, make | |||
| an instance of the 'Command' class inside (assuming it is an instance | |||
| of BaseCommand), add it to self._commands, and log the addition. Any | |||
| problems along the way will either be ignored or logged. | |||
| instances of any classes inside that are subclasses of the base | |||
| (:py:attr:`self._resource_base <_resource_base>`), add them to the | |||
| resources dictionary with :py:meth:`self._load_resource() | |||
| <_load_resource>`, and finally log the addition. Any problems along | |||
| the way will either be ignored or logged. | |||
| """ | |||
| f, path, desc = imp.find_module(name, [path]) | |||
| try: | |||
| @@ -89,24 +97,13 @@ class _ResourceManager(object): | |||
| finally: | |||
| f.close() | |||
| attr = self._resource_attribute | |||
| if not hasattr(module, attr): | |||
| return # No resources in this module | |||
| resource_class = getattr(module, attr) | |||
| try: | |||
| resource = resource_class(self.bot) # Create instance of resource | |||
| except Exception: | |||
| e = "Error instantiating {0} class in {1} (from {2})" | |||
| self.logger.exception(e.format(attr, name, path)) | |||
| return | |||
| if not isinstance(resource, self._resource_base): | |||
| return | |||
| self._resources[resource.name] = resource | |||
| self.logger.debug("Loaded {0} {1}".format(attr.lower(), resource.name)) | |||
| for obj in vars(module).values(): | |||
| if type(obj) is type and isinstance(obj, self._resource_base): | |||
| self._load_resource(name, path, obj) | |||
| def _load_directory(self, dir): | |||
| """Load all valid resources in a given directory.""" | |||
| self.logger.debug("Loading directory {0}".format(dir)) | |||
| processed = [] | |||
| for name in listdir(dir): | |||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||
| @@ -115,9 +112,14 @@ class _ResourceManager(object): | |||
| continue | |||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||
| if modname not in processed: | |||
| self._load_resource(modname, dir) | |||
| self._load_module(modname, dir) | |||
| processed.append(modname) | |||
| @property | |||
| def lock(self): | |||
| """The resource access/modify lock.""" | |||
| return self._resource_access_lock | |||
| def load(self): | |||
| """Load (or reload) all valid resources into :py:attr:`_resources`.""" | |||
| name = self._resource_name # e.g. "commands" or "tasks" | |||
| @@ -146,8 +148,7 @@ class CommandManager(_ResourceManager): | |||
| Manages (i.e., loads, reloads, and calls) IRC commands. | |||
| """ | |||
| def __init__(self, bot): | |||
| base = super(CommandManager, self) | |||
| base.__init__(bot, "commands", "Command", BaseCommand) | |||
| super(CommandManager, self).__init__(bot, "commands", Command) | |||
| def _wrap_check(self, command, data): | |||
| """Check whether a command should be called, catching errors.""" | |||
| @@ -181,7 +182,7 @@ class TaskManager(_ResourceManager): | |||
| Manages (i.e., loads, reloads, schedules, and runs) wiki bot tasks. | |||
| """ | |||
| def __init__(self, bot): | |||
| super(TaskManager, self).__init__(bot, "tasks", "Task", BaseTask) | |||
| super(TaskManager, self).__init__(bot, "tasks", Task) | |||
| def _wrapper(self, task, **kwargs): | |||
| """Wrapper for task classes: run the task and catch any errors.""" | |||
| @@ -197,9 +198,8 @@ class TaskManager(_ResourceManager): | |||
| def start(self, task_name, **kwargs): | |||
| """Start a given task in a new daemon thread, and return the thread. | |||
| kwargs are passed to :py:meth:`task.run() <earwigbot.tasks.BaseTask>`. | |||
| If the task is not found, ``None`` will be returned an an error is | |||
| logged. | |||
| kwargs are passed to :py:meth:`task.run() <earwigbot.tasks.Task>`. If | |||
| the task is not found, ``None`` will be returned an an error is logged. | |||
| """ | |||
| msg = "Starting task '{0}' in a new thread" | |||
| self.logger.info(msg.format(task_name)) | |||
| @@ -23,16 +23,16 @@ | |||
| from earwigbot import exceptions | |||
| from earwigbot import wiki | |||
| __all__ = ["BaseTask"] | |||
| __all__ = ["Task"] | |||
| class BaseTask(object): | |||
| class Task(object): | |||
| """ | |||
| **EarwigBot: Base Bot Task** | |||
| This package provides built-in wiki bot "tasks" EarwigBot runs. Additional | |||
| tasks can be installed as plugins in the bot's working directory. | |||
| This class (import with ``from earwigbot.tasks import BaseTask``) can be | |||
| This class (import with ``from earwigbot.tasks import Task``) can be | |||
| subclassed to create custom bot tasks. | |||
| To run a task, use :py:meth:`bot.tasks.start(name, **kwargs) | |||
| @@ -20,11 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["AFCCatDelink"] | |||
| class Task(BaseTask): | |||
| class AFCCatDelink(Task): | |||
| """A task to delink mainspace categories in declined [[WP:AFC]] | |||
| submissions.""" | |||
| name = "afc_catdelink" | |||
| @@ -26,11 +26,11 @@ from threading import Lock | |||
| import oursql | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["AFCCopyvios"] | |||
| class Task(BaseTask): | |||
| class AFCCopyvios(Task): | |||
| """A task to check newly-edited [[WP:AFC]] submissions for copyright | |||
| violations.""" | |||
| name = "afc_copyvios" | |||
| @@ -20,11 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["AFCDailyCats"] | |||
| class Task(BaseTask): | |||
| class AFCDailyCats(Task): | |||
| """ A task to create daily categories for [[WP:AFC]].""" | |||
| name = "afc_dailycats" | |||
| number = 3 | |||
| @@ -32,11 +32,11 @@ from numpy import arange | |||
| import oursql | |||
| from earwigbot import wiki | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["AFCHistory"] | |||
| class Task(BaseTask): | |||
| class AFCHistory(Task): | |||
| """A task to generate charts about AfC submissions over time. | |||
| The main function of the task is to work through the "AfC submissions by | |||
| @@ -30,11 +30,11 @@ import oursql | |||
| from earwigbot import exceptions | |||
| from earwigbot import wiki | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["AFCStatistics"] | |||
| class Task(BaseTask): | |||
| class AFCStatistics(Task): | |||
| """A task to generate statistics for WikiProject Articles for Creation. | |||
| Statistics are stored in a MySQL database ("u_earwig_afc_statistics") | |||
| @@ -87,7 +87,9 @@ class Task(BaseTask): | |||
| action = kwargs.get("action") | |||
| if not self.db_access_lock.acquire(False): # Non-blocking | |||
| if action == "sync": | |||
| self.logger.info("A sync is already ongoing; aborting") | |||
| return | |||
| self.logger.info("Waiting for database access lock") | |||
| self.db_access_lock.acquire() | |||
| try: | |||
| @@ -20,11 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["AFCUndated"] | |||
| class Task(BaseTask): | |||
| class AFCUndated(Task): | |||
| """A task to clear [[Category:Undated AfC submissions]].""" | |||
| name = "afc_undated" | |||
| @@ -20,11 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["BLPTag"] | |||
| class Task(BaseTask): | |||
| class BLPTag(Task): | |||
| """A task to add |blp=yes to ``{{WPB}}`` or ``{{WPBS}}`` when it is used | |||
| along with ``{{WP Biography}}``.""" | |||
| name = "blptag" | |||
| @@ -20,11 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["FeedDailyCats"] | |||
| class Task(BaseTask): | |||
| class FeedDailyCats(Task): | |||
| """A task to create daily categories for [[WP:FEED]].""" | |||
| name = "feed_dailycats" | |||
| @@ -20,11 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["WikiProjectTagger"] | |||
| class Task(BaseTask): | |||
| class WikiProjectTagger(Task): | |||
| """A task to tag talk pages with WikiProject Banners.""" | |||
| name = "wikiproject_tagger" | |||
| @@ -20,11 +20,11 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.tasks import BaseTask | |||
| from earwigbot.tasks import Task | |||
| __all__ = ["Task"] | |||
| __all__ = ["WrongMIME"] | |||
| class Task(BaseTask): | |||
| class WrongMIME(Task): | |||
| """A task to tag files whose extensions do not agree with their MIME | |||
| type.""" | |||
| name = "wrongmime" | |||