From 78ac1b8a807d867fcbc7867cd69721d9e4128c1c Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Tue, 3 Jul 2012 16:53:10 -0400 Subject: [PATCH] Tons of refactoring, miscellaneous cleanup, and improvements. * _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. --- docs/customizing.rst | 156 ++++++++++++----------------- earwigbot/bot.py | 35 ++++--- earwigbot/commands/__init__.py | 8 +- earwigbot/commands/afc_report.py | 6 +- earwigbot/commands/afc_status.py | 6 +- earwigbot/commands/calc.py | 6 +- earwigbot/commands/chanops.py | 6 +- earwigbot/commands/crypt.py | 6 +- earwigbot/commands/ctcp.py | 6 +- earwigbot/commands/editcount.py | 6 +- earwigbot/commands/git.py | 6 +- earwigbot/commands/help.py | 6 +- earwigbot/commands/link.py | 6 +- earwigbot/commands/praise.py | 6 +- earwigbot/commands/quit.py | 6 +- earwigbot/commands/registration.py | 6 +- earwigbot/commands/remind.py | 6 +- earwigbot/commands/replag.py | 6 +- earwigbot/commands/rights.py | 6 +- earwigbot/commands/test.py | 8 +- earwigbot/commands/threads.py | 14 +-- earwigbot/exceptions.py | 12 --- earwigbot/irc/connection.py | 77 ++++++++++++--- earwigbot/irc/data.py | 180 ++++++++++++++++++++++++++-------- earwigbot/irc/frontend.py | 26 +---- earwigbot/managers.py | 72 +++++++------- earwigbot/tasks/__init__.py | 6 +- earwigbot/tasks/afc_catdelink.py | 6 +- earwigbot/tasks/afc_copyvios.py | 6 +- earwigbot/tasks/afc_dailycats.py | 6 +- earwigbot/tasks/afc_history.py | 6 +- earwigbot/tasks/afc_statistics.py | 8 +- earwigbot/tasks/afc_undated.py | 6 +- earwigbot/tasks/blptag.py | 6 +- earwigbot/tasks/feed_dailycats.py | 6 +- earwigbot/tasks/wikiproject_tagger.py | 6 +- earwigbot/tasks/wrongmime.py | 6 +- 37 files changed, 440 insertions(+), 312 deletions(-) diff --git a/docs/customizing.rst b/docs/customizing.rst index fa4be59..3c8f2bc 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -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) `, :py:meth:`reply(data, msg) @@ -128,10 +128,10 @@ these are the basics: `, and :py:meth:`part(chan) `. -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) - ` method will take and replace + ` 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) `, 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 ` 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 diff --git a/earwigbot/bot.py b/earwigbot/bot.py index d352532..c1e8074 100644 --- a/earwigbot/bot.py +++ b/earwigbot/bot.py @@ -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): diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index 7a70244..7a89fb0 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -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 "``. diff --git a/earwigbot/commands/afc_report.py b/earwigbot/commands/afc_report.py index 6129e11..a6f201e 100644 --- a/earwigbot/commands/afc_report.py +++ b/earwigbot/commands/afc_report.py @@ -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" diff --git a/earwigbot/commands/afc_status.py b/earwigbot/commands/afc_status.py index 635d6cc..d4261cb 100644 --- a/earwigbot/commands/afc_status.py +++ b/earwigbot/commands/afc_status.py @@ -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" diff --git a/earwigbot/commands/calc.py b/earwigbot/commands/calc.py index 776ea57..17aee65 100644 --- a/earwigbot/commands/calc.py +++ b/earwigbot/commands/calc.py @@ -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" diff --git a/earwigbot/commands/chanops.py b/earwigbot/commands/chanops.py index 1083a45..fa20b2e 100644 --- a/earwigbot/commands/chanops.py +++ b/earwigbot/commands/chanops.py @@ -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" diff --git a/earwigbot/commands/crypt.py b/earwigbot/commands/crypt.py index 6a57c8c..0d37eb0 100644 --- a/earwigbot/commands/crypt.py +++ b/earwigbot/commands/crypt.py @@ -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" diff --git a/earwigbot/commands/ctcp.py b/earwigbot/commands/ctcp.py index 1e89dd5..13f35d7 100644 --- a/earwigbot/commands/ctcp.py +++ b/earwigbot/commands/ctcp.py @@ -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" diff --git a/earwigbot/commands/editcount.py b/earwigbot/commands/editcount.py index 9182877..8223004 100644 --- a/earwigbot/commands/editcount.py +++ b/earwigbot/commands/editcount.py @@ -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" diff --git a/earwigbot/commands/git.py b/earwigbot/commands/git.py index 8ee3f22..23a5daf 100644 --- a/earwigbot/commands/git.py +++ b/earwigbot/commands/git.py @@ -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" diff --git a/earwigbot/commands/help.py b/earwigbot/commands/help.py index 35f97fc..5fd2d59 100644 --- a/earwigbot/commands/help.py +++ b/earwigbot/commands/help.py @@ -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" diff --git a/earwigbot/commands/link.py b/earwigbot/commands/link.py index cb2e154..08734d9 100644 --- a/earwigbot/commands/link.py +++ b/earwigbot/commands/link.py @@ -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" diff --git a/earwigbot/commands/praise.py b/earwigbot/commands/praise.py index e04fd3e..693b58d 100644 --- a/earwigbot/commands/praise.py +++ b/earwigbot/commands/praise.py @@ -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" diff --git a/earwigbot/commands/quit.py b/earwigbot/commands/quit.py index 044f7ff..9ccede0 100644 --- a/earwigbot/commands/quit.py +++ b/earwigbot/commands/quit.py @@ -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" diff --git a/earwigbot/commands/registration.py b/earwigbot/commands/registration.py index 44592ef..f6fce3c 100644 --- a/earwigbot/commands/registration.py +++ b/earwigbot/commands/registration.py @@ -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" diff --git a/earwigbot/commands/remind.py b/earwigbot/commands/remind.py index 05360e6..19c5b32 100644 --- a/earwigbot/commands/remind.py +++ b/earwigbot/commands/remind.py @@ -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" diff --git a/earwigbot/commands/replag.py b/earwigbot/commands/replag.py index fce0240..8a12dde 100644 --- a/earwigbot/commands/replag.py +++ b/earwigbot/commands/replag.py @@ -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" diff --git a/earwigbot/commands/rights.py b/earwigbot/commands/rights.py index a2ad76d..cbdb765 100644 --- a/earwigbot/commands/rights.py +++ b/earwigbot/commands/rights.py @@ -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" diff --git a/earwigbot/commands/test.py b/earwigbot/commands/test.py index 478d6e9..7ea9545 100644 --- a/earwigbot/commands/test.py +++ b/earwigbot/commands/test.py @@ -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)) diff --git a/earwigbot/commands/threads.py b/earwigbot/commands/threads.py index 8d1ed17..bd2fed9 100644 --- a/earwigbot/commands/threads.py +++ b/earwigbot/commands/threads.py @@ -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." diff --git a/earwigbot/exceptions.py b/earwigbot/exceptions.py index 69b9ca3..335f6ff 100644 --- a/earwigbot/exceptions.py +++ b/earwigbot/exceptions.py @@ -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): `. """ -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 - `. - """ - class WikiToolsetError(EarwigBotError): """Base exception class for errors in the Wiki Toolset.""" diff --git a/earwigbot/irc/connection.py b/earwigbot/irc/connection.py index 965a10d..ed8e099 100644 --- a/earwigbot/irc/connection.py +++ b/earwigbot/irc/connection.py @@ -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: diff --git a/earwigbot/irc/data.py b/earwigbot/irc/data.py index 38cbc4d..e5e5186 100644 --- a/earwigbot/irc/data.py +++ b/earwigbot/irc/data.py @@ -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 ` is converted into the string - :py:attr:`self.command ` and the argument list - :py:attr:`self.args ` if the message starts with a "trigger" - (``"!"``, ``"."``, or the bot's name); :py:attr:`self.is_command - ` will be set to ``True``, and :py:attr:`self.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 `. + 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 `, 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 `_ 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 ` 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 diff --git a/earwigbot/irc/frontend.py b/earwigbot/irc/frontend.py index 75c1c20..a301c18 100644 --- a/earwigbot/irc/frontend.py +++ b/earwigbot/irc/frontend.py @@ -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() ` if they are in :py:mod:`earwigbot.commands` or the bot's custom command directory (explained in the :doc:`documentation `). """ - 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 diff --git a/earwigbot/managers.py b/earwigbot/managers.py index e7882f3..0cb34c2 100644 --- a/earwigbot/managers.py +++ b/earwigbot/managers.py @@ -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() `. - If the task is not found, ``None`` will be returned an an error is - logged. + kwargs are passed to :py:meth:`task.run() `. 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)) diff --git a/earwigbot/tasks/__init__.py b/earwigbot/tasks/__init__.py index f9d084e..35c0046 100644 --- a/earwigbot/tasks/__init__.py +++ b/earwigbot/tasks/__init__.py @@ -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) diff --git a/earwigbot/tasks/afc_catdelink.py b/earwigbot/tasks/afc_catdelink.py index fe33c31..21e23a3 100644 --- a/earwigbot/tasks/afc_catdelink.py +++ b/earwigbot/tasks/afc_catdelink.py @@ -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" diff --git a/earwigbot/tasks/afc_copyvios.py b/earwigbot/tasks/afc_copyvios.py index b8b1cb3..5f72ea8 100644 --- a/earwigbot/tasks/afc_copyvios.py +++ b/earwigbot/tasks/afc_copyvios.py @@ -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" diff --git a/earwigbot/tasks/afc_dailycats.py b/earwigbot/tasks/afc_dailycats.py index 70ece8f..200e5c7 100644 --- a/earwigbot/tasks/afc_dailycats.py +++ b/earwigbot/tasks/afc_dailycats.py @@ -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 diff --git a/earwigbot/tasks/afc_history.py b/earwigbot/tasks/afc_history.py index 767653d..87c8636 100644 --- a/earwigbot/tasks/afc_history.py +++ b/earwigbot/tasks/afc_history.py @@ -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 diff --git a/earwigbot/tasks/afc_statistics.py b/earwigbot/tasks/afc_statistics.py index 017c924..5b200db 100644 --- a/earwigbot/tasks/afc_statistics.py +++ b/earwigbot/tasks/afc_statistics.py @@ -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: diff --git a/earwigbot/tasks/afc_undated.py b/earwigbot/tasks/afc_undated.py index d4955dc..a483766 100644 --- a/earwigbot/tasks/afc_undated.py +++ b/earwigbot/tasks/afc_undated.py @@ -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" diff --git a/earwigbot/tasks/blptag.py b/earwigbot/tasks/blptag.py index 2d9c7e4..80f8bab 100644 --- a/earwigbot/tasks/blptag.py +++ b/earwigbot/tasks/blptag.py @@ -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" diff --git a/earwigbot/tasks/feed_dailycats.py b/earwigbot/tasks/feed_dailycats.py index 2d08540..83563dc 100644 --- a/earwigbot/tasks/feed_dailycats.py +++ b/earwigbot/tasks/feed_dailycats.py @@ -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" diff --git a/earwigbot/tasks/wikiproject_tagger.py b/earwigbot/tasks/wikiproject_tagger.py index 71e8d5c..7c3939f 100644 --- a/earwigbot/tasks/wikiproject_tagger.py +++ b/earwigbot/tasks/wikiproject_tagger.py @@ -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" diff --git a/earwigbot/tasks/wrongmime.py b/earwigbot/tasks/wrongmime.py index 2c8813e..353dad2 100644 --- a/earwigbot/tasks/wrongmime.py +++ b/earwigbot/tasks/wrongmime.py @@ -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"