Преглед на файлове

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.
tags/v0.1^2
Ben Kurtovic преди 12 години
родител
ревизия
78ac1b8a80
променени са 37 файла, в които са добавени 440 реда и са изтрити 312 реда
  1. +63
    -93
      docs/customizing.rst
  2. +20
    -15
      earwigbot/bot.py
  3. +4
    -4
      earwigbot/commands/__init__.py
  4. +4
    -2
      earwigbot/commands/afc_report.py
  5. +4
    -2
      earwigbot/commands/afc_status.py
  6. +4
    -2
      earwigbot/commands/calc.py
  7. +4
    -2
      earwigbot/commands/chanops.py
  8. +4
    -2
      earwigbot/commands/crypt.py
  9. +4
    -2
      earwigbot/commands/ctcp.py
  10. +4
    -2
      earwigbot/commands/editcount.py
  11. +4
    -2
      earwigbot/commands/git.py
  12. +4
    -2
      earwigbot/commands/help.py
  13. +4
    -2
      earwigbot/commands/link.py
  14. +4
    -2
      earwigbot/commands/praise.py
  15. +4
    -2
      earwigbot/commands/quit.py
  16. +4
    -2
      earwigbot/commands/registration.py
  17. +4
    -2
      earwigbot/commands/remind.py
  18. +4
    -2
      earwigbot/commands/replag.py
  19. +4
    -2
      earwigbot/commands/rights.py
  20. +5
    -3
      earwigbot/commands/test.py
  21. +4
    -10
      earwigbot/commands/threads.py
  22. +0
    -12
      earwigbot/exceptions.py
  23. +64
    -13
      earwigbot/irc/connection.py
  24. +140
    -40
      earwigbot/irc/data.py
  25. +5
    -21
      earwigbot/irc/frontend.py
  26. +36
    -36
      earwigbot/managers.py
  27. +3
    -3
      earwigbot/tasks/__init__.py
  28. +3
    -3
      earwigbot/tasks/afc_catdelink.py
  29. +3
    -3
      earwigbot/tasks/afc_copyvios.py
  30. +3
    -3
      earwigbot/tasks/afc_dailycats.py
  31. +3
    -3
      earwigbot/tasks/afc_history.py
  32. +5
    -3
      earwigbot/tasks/afc_statistics.py
  33. +3
    -3
      earwigbot/tasks/afc_undated.py
  34. +3
    -3
      earwigbot/tasks/blptag.py
  35. +3
    -3
      earwigbot/tasks/feed_dailycats.py
  36. +3
    -3
      earwigbot/tasks/wikiproject_tagger.py
  37. +3
    -3
      earwigbot/tasks/wrongmime.py

+ 63
- 93
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)
<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

+ 20
- 15
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):


+ 4
- 4
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
<command>"``.


+ 4
- 2
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"



+ 4
- 2
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"


+ 4
- 2
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"


+ 4
- 2
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"


+ 4
- 2
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"


+ 4
- 2
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"


+ 4
- 2
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"



+ 4
- 2
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"


+ 4
- 2
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"



+ 4
- 2
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"



+ 4
- 2
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"



+ 4
- 2
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"


+ 4
- 2
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"



+ 4
- 2
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"



+ 4
- 2
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"



+ 4
- 2
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"



+ 5
- 3
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))


+ 4
- 10
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."


+ 0
- 12
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):
<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."""



+ 64
- 13
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:


+ 140
- 40
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 <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

+ 5
- 21
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()
<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


+ 36
- 36
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() <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))


+ 3
- 3
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)


+ 3
- 3
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"


+ 3
- 3
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"


+ 3
- 3
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


+ 3
- 3
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


+ 5
- 3
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:


+ 3
- 3
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"



+ 3
- 3
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"


+ 3
- 3
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"



+ 3
- 3
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"



+ 3
- 3
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"


Зареждане…
Отказ
Запис