* _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" | |||