Переглянути джерело

Command.commands; small change in managers; !time

tags/v0.1^2
Ben Kurtovic 12 роки тому
джерело
коміт
b34dd94f0d
20 змінених файлів з 161 додано та 122 видалено
  1. +15
    -5
      README.rst
  2. +20
    -7
      docs/customizing.rst
  3. +12
    -4
      earwigbot/commands/__init__.py
  4. +0
    -5
      earwigbot/commands/_old.py
  5. +1
    -4
      earwigbot/commands/afc_pending.py
  6. +2
    -3
      earwigbot/commands/afc_status.py
  7. +1
    -4
      earwigbot/commands/chanops.py
  8. +1
    -4
      earwigbot/commands/crypt.py
  9. +1
    -4
      earwigbot/commands/editcount.py
  10. +1
    -4
      earwigbot/commands/geolocate.py
  11. +11
    -22
      earwigbot/commands/help.py
  12. +1
    -4
      earwigbot/commands/langcode.py
  13. +2
    -5
      earwigbot/commands/praise.py
  14. +1
    -4
      earwigbot/commands/quit.py
  15. +1
    -4
      earwigbot/commands/registration.py
  16. +3
    -10
      earwigbot/commands/remind.py
  17. +1
    -4
      earwigbot/commands/rights.py
  18. +3
    -6
      earwigbot/commands/threads.py
  19. +70
    -0
      earwigbot/commands/time.py
  20. +14
    -19
      earwigbot/managers.py

+ 15
- 5
README.rst Переглянути файл

@@ -166,6 +166,13 @@ for and what they should be overridden with, but these are the basics:

- Class attribute ``name`` is the name of the command. This must be specified.

- Class attribute ``commands`` is a list of names that will trigger this
command. It defaults to the command's ``name``, but you can override it with
multiple names to serve as aliases. This is handled by the default
``check()`` implementation (see below), so if ``check()`` is overridden, this
is ignored by everything except the help_ command (so ``!help alias`` will
trigger help for the actual command).

- Class attribute ``hooks`` is a list of the "IRC events" that this command
might respond to. It defaults to ``["msg"]``, but options include
``"msg_private"`` (for private messages only), ``"msg_public"`` (for channel
@@ -181,10 +188,12 @@ for and what they should be overridden with, but these are the basics:
- Method ``check()`` is passed a ``Data`` [2]_ object, and should return
``True`` if you want to respond to this message, or ``False`` otherwise. The
default behavior is to return ``True`` only if ``data.is_command`` is
``True`` and ``data.command == self.name``, which is suitable for most cases.
A common, straightforward reason for overriding is if a command has aliases
(see chanops_ for an example). Note that by returning ``True``, you prevent
any other commands from responding to this message.
``True`` and ``data.command`` ``==`` ``self.name`` (or ``data.command`` is in
``self.commands`` if that list is overriden; see above), which is suitable
for most cases. A possible reason for overriding is if you want to do
something in response to events from a specific channel only. Note that by
returning ``True``, you prevent any other commands from responding to this
message.

- Method ``process()`` is passed the same ``Data`` object as ``check()``, but
only if ``check()`` returned ``True``. This is where the bulk of your command
@@ -230,7 +239,7 @@ and what they should be overridden with, but these are the basics:
``"([[WP:BOT|Bot]]; [[User:EarwigBot#Task $1|Task $1]]): $2"``, which the
task class's ``make_summary(comment)`` method will take and replace ``$1``
with the task number and ``$2`` with the details of the edit.
Additionally, ``shutoff_enabled()`` (which checks whether the bot has been
told to stop on-wiki by checking the content of a particular page) can check
a different page for each task using similar variables. EarwigBot's
@@ -510,6 +519,7 @@ Footnotes
.. _earwigbot.bot.Bot: https://github.com/earwig/earwigbot/blob/develop/earwigbot/bot.py
.. _earwigbot.config.BotConfig: https://github.com/earwig/earwigbot/blob/develop/earwigbot/config.py
.. _earwigbot.commands.BaseCommand: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/__init__.py
.. _help: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/help.py
.. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py
.. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py
.. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py


+ 20
- 7
docs/customizing.rst Переглянути файл

@@ -96,6 +96,15 @@ 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.commands` is a list
of names that will trigger this command. It defaults to the command's
:py:attr:`~earwigbot.commands.BaseCommand.name`, but you can override it with
multiple names to serve as aliases. This is handled by the default
:py:meth:`~earwigbot.commands.BaseCommand.check` implementation (see below),
so if :py:meth:`~earwigbot.commands.BaseCommand.check` is overridden, this is
ignored by everything except the help_ command (so ``!help alias`` will
trigger help for the actual command).

- Class attribute :py:attr:`~earwigbot.commands.BaseCommand.hooks` is a list of
the "IRC events" that this command might respond to. It defaults to
``["msg"]``, but options include ``"msg_private"`` (for private messages
@@ -113,12 +122,15 @@ these are the basics:
- Method :py:meth:`~earwigbot.commands.BaseCommand.check` is passed a
: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.
behavior is to return ``True`` only if :py:attr:`data.is_command` is ``True``
and :py:attr:`data.command` ``==``
:py:attr:`~earwigbot.commands.BaseCommand.name` (or :py:attr:`data.command
<earwigbot.irc.data.Data.command>` is in
:py:attr:`~earwigbot.commands.BaseCommand.commands` if that list is
overriden; see above), which is suitable for most cases. A possible reason
for overriding is if you want to do something in response to events from a
specific channel only. Note that by returning ``True``, you prevent any other
commands from responding to this message.

- Method :py:meth:`~earwigbot.commands.BaseCommand.process` is passed the same
:py:class:`~earwigbot.irc.data.Data` object as
@@ -179,7 +191,7 @@ are the basics:
task class's :py:meth:`make_summary(comment)
<earwigbot.tasks.BaseTask.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
@@ -249,6 +261,7 @@ task, or the afc_statistics_ plugin for a more complicated one.
:py:attr:`~earwigbot.irc.data.Data.ident`,
and :py:attr:`~earwigbot.irc.data.Data.host`.

.. _help: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/help.py
.. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py
.. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py
.. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py


+ 12
- 4
earwigbot/commands/__init__.py Переглянути файл

@@ -36,9 +36,13 @@ class BaseCommand(object):
This docstring is reported to the user when they type ``"!help
<command>"``.
"""
# This is the command's name, as reported to the user when they use !help:
# The command's name, as reported to the user when they use !help:
name = None

# A list of names that will trigger this command. If left empty, it will
# be triggered by the command's name and its name only:
commands = []

# Hooks are "msg", "msg_private", "msg_public", and "join". "msg" is the
# default behavior; if you wish to override that, change the value in your
# command subclass:
@@ -86,11 +90,15 @@ class BaseCommand(object):
sent on IRC, it should be cheap to execute and unlikely to throw
exceptions.

Most commands return ``True`` if :py:attr:`data.command
Most commands return ``True`` only if :py:attr:`data.command
<earwigbot.irc.data.Data.command>` ``==`` :py:attr:`self.name <name>`,
otherwise they return ``False``. This is the default behavior of
:py:meth:`check`; you need only override it if you wish to change that.
or :py:attr:`data.command <earwigbot.irc.data.Data.command>` is in
:py:attr:`self.commands <commands>` if that list is overriden. This is
the default behavior; you should only override it if you wish to change
that.
"""
if self.commands:
return data.is_command and data.command in self.commands
return data.is_command and data.command == self.name

def process(self, data):


+ 0
- 5
earwigbot/commands/_old.py Переглянути файл

@@ -21,11 +21,6 @@ def parse(command, line, line2, nick, chan, host, auth, notice, say, reply, s):
u.close()
say('"' + info['Date'] + '" - tycho.usno.navy.mil', chan)
return
if command == "beats":
beats = ((time.time() + 3600) % 86400) / 86.4
beats = int(math.floor(beats))
say('@%03i' % beats, chan)
return
if command == "dict" or command == "dictionary":
def trim(thing):
if thing.endswith('&nbsp;'):


+ 1
- 4
earwigbot/commands/afc_pending.py Переглянути файл

@@ -25,10 +25,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand):
"""Link the user to the pending AFC submissions page and category."""
name = "pending"

def check(self, data):
commands = ["pending", "pend"]
return data.is_command and data.command in commands
commands = ["pending", "pend"]

def process(self, data):
msg1 = "pending submissions status page: http://enwp.org/WP:AFC/ST"


+ 2
- 3
earwigbot/commands/afc_status.py Переглянути файл

@@ -28,13 +28,12 @@ class Command(BaseCommand):
"""Get the number of pending AfC submissions, open redirect requests, and
open file upload requests."""
name = "status"
commands = ["status", "count", "num", "number"]
hooks = ["join", "msg"]

def check(self, data):
commands = ["status", "count", "num", "number"]
if data.is_command and data.command in commands:
if data.is_command and data.command in self.commands:
return True

try:
if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc":
if data.nick != self.config.irc["frontend"]["nick"]:


+ 1
- 4
earwigbot/commands/chanops.py Переглянути файл

@@ -26,10 +26,7 @@ class Command(BaseCommand):
"""Voice, devoice, op, or deop users in the channel, or join or part from
other channels."""
name = "chanops"

def check(self, data):
cmnds = ["chanops", "voice", "devoice", "op", "deop", "join", "part"]
return data.is_command and data.command in cmnds
commands = ["chanops", "voice", "devoice", "op", "deop", "join", "part"]

def process(self, data):
if data.command == "chanops":


+ 1
- 4
earwigbot/commands/crypt.py Переглянути файл

@@ -30,10 +30,7 @@ class Command(BaseCommand):
"""Provides hash functions with !hash (!hash list for supported algorithms)
and blowfish encryption with !encrypt and !decrypt."""
name = "crypt"

def check(self, data):
commands = ["crypt", "hash", "encrypt", "decrypt"]
return data.is_command and data.command in commands
commands = ["crypt", "hash", "encrypt", "decrypt"]

def process(self, data):
if data.command == "crypt":


+ 1
- 4
earwigbot/commands/editcount.py Переглянути файл

@@ -28,10 +28,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand):
"""Return a user's edit count."""
name = "editcount"

def check(self, data):
commands = ["ec", "editcount"]
return data.is_command and data.command in commands
commands = ["ec", "editcount"]

def process(self, data):
if not data.args:


+ 1
- 4
earwigbot/commands/geolocate.py Переглянути файл

@@ -28,6 +28,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand):
"""Geolocate an IP address (via http://ipinfodb.com/)."""
name = "geolocate"
commands = ["geolocate", "locate", "geo", "ip"]

def setup(self):
self.config.decrypt(self.config.commands, (self.name, "apiKey"))
@@ -38,10 +39,6 @@ class Command(BaseCommand):
log = 'Cannot use without an API key for http://ipinfodb.com/ stored as config.commands["{0}"]["apiKey"]'
self.logger.warn(log.format(self.name))

def check(self, data):
commands = ["geolocate", "locate", "geo", "ip"]
return data.is_command and data.command in commands

def process(self, data):
if not data.args:
self.reply(data, "please specify an IP to lookup.")


+ 11
- 22
earwigbot/commands/help.py Переглянути файл

@@ -23,7 +23,6 @@
import re

from earwigbot.commands import BaseCommand
from earwigbot.irc import Data

class Command(BaseCommand):
"""Displays help information."""
@@ -48,34 +47,24 @@ class Command(BaseCommand):
def do_main_help(self, data):
"""Give the user a general help message with a list of all commands."""
msg = "Hi, I'm a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help <command>'."
cmnds = sorted(self.bot.commands)
cmnds = sorted([cmnd.name for cmnd in self.bot.commands])
msg = msg.format(len(cmnds), ', '.join(cmnds))
self.reply(data, msg)

def do_command_help(self, data):
"""Give the user help for a specific command."""
command = data.args[0]
target = data.args[0]

# Create a dummy message to test which commands pick up the user's
# input:
msg = ":foo!bar@example.com PRIVMSG #channel :msg".split()
dummy = Data(self.bot, msg)
dummy.command = command.lower()
dummy.is_command = True
for command in self.bot.commands:
if command.name == target or target in command.commands:
if command.__doc__:
doc = command.__doc__.replace("\n", "")
doc = re.sub("\s\s+", " ", doc)
msg = "help for command \x0303{0}\x0301: \"{1}\""
self.reply(data, msg.format(target, doc))
return

for cmnd_name in self.bot.commands:
cmnd = self.bot.commands.get(cmnd_name)
if not cmnd.check(dummy):
continue
if cmnd.__doc__:
doc = cmnd.__doc__.replace("\n", "")
doc = re.sub("\s\s+", " ", doc)
msg = "help for command \x0303{0}\x0301: \"{1}\""
self.reply(data, msg.format(command, doc))
return
break

msg = "sorry, no help for \x0303{0}\x0301.".format(command)
msg = "sorry, no help for \x0303{0}\x0301.".format(target)
self.reply(data, msg)

def do_hello(self, data):


+ 1
- 4
earwigbot/commands/langcode.py Переглянути файл

@@ -26,10 +26,7 @@ class Command(BaseCommand):
"""Convert a language code into its name and a list of WMF sites in that
language."""
name = "langcode"

def check(self, data):
commands = ["langcode", "lang", "language"]
return data.is_command and data.command in commands
commands = ["langcode", "lang", "language"]

def process(self, data):
if not data.args:


+ 2
- 5
earwigbot/commands/praise.py Переглянути файл

@@ -25,11 +25,8 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand):
"""Praise people!"""
name = "praise"

def check(self, data):
commands = ["praise", "earwig", "leonard", "leonard^bloom", "groove",
"groovedog"]
return data.is_command and data.command in commands
commands = ["praise", "earwig", "leonard", "leonard^bloom", "groove",
"groovedog"]

def process(self, data):
if data.command == "earwig":


+ 1
- 4
earwigbot/commands/quit.py Переглянути файл

@@ -26,10 +26,7 @@ class Command(BaseCommand):
"""Quit, restart, or reload components from the bot. Only the owners can
run this command."""
name = "quit"

def check(self, data):
commands = ["quit", "restart", "reload"]
return data.is_command and data.command in commands
commands = ["quit", "restart", "reload"]

def process(self, data):
if data.host not in self.config.irc["permissions"]["owners"]:


+ 1
- 4
earwigbot/commands/registration.py Переглянути файл

@@ -28,10 +28,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand):
"""Return when a user registered."""
name = "registration"

def check(self, data):
commands = ["registration", "reg", "age"]
return data.is_command and data.command in commands
commands = ["registration", "reg", "age"]

def process(self, data):
if not data.args:


+ 3
- 10
earwigbot/commands/remind.py Переглянути файл

@@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import threading
from threading import Timer
import time

from earwigbot.commands import BaseCommand
@@ -28,9 +28,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand):
"""Set a message to be repeated to you in a certain amount of time."""
name = "remind"

def check(self, data):
return data.is_command and data.command in ["remind", "reminder"]
commands = ["remind", "reminder"]

def process(self, data):
if not data.args:
@@ -58,12 +56,7 @@ class Command(BaseCommand):
msg = msg.format(message, wait, end_time_with_timezone)
self.reply(data, msg)

t_reminder = threading.Thread(target=self.reminder,
args=(data, message, wait))
t_reminder = Timer(wait, self.reply, args=(data, message))
t_reminder.name = "reminder " + end_time
t_reminder.daemon = True
t_reminder.start()

def reminder(self, data, message, wait):
time.sleep(wait)
self.reply(data, message)

+ 1
- 4
earwigbot/commands/rights.py Переглянути файл

@@ -26,10 +26,7 @@ from earwigbot.commands import BaseCommand
class Command(BaseCommand):
"""Retrieve a list of rights for a given username."""
name = "rights"

def check(self, data):
commands = ["rights", "groups", "permissions", "privileges"]
return data.is_command and data.command in commands
commands = ["rights", "groups", "permissions", "privileges"]

def process(self, data):
if not data.args:


+ 3
- 6
earwigbot/commands/threads.py Переглянути файл

@@ -29,10 +29,7 @@ from earwigbot.exceptions import KwargParseError
class Command(BaseCommand):
"""Manage wiki tasks from IRC, and check on thread status."""
name = "threads"

def check(self, data):
commands = ["tasks", "task", "threads", "tasklist"]
return data.is_command and data.command in commands
commands = ["tasks", "task", "threads", "tasklist"]

def process(self, data):
self.data = data
@@ -103,7 +100,7 @@ class Command(BaseCommand):
whether they are currently running or idle."""
threads = threading.enumerate()
tasklist = []
for task in sorted(self.bot.tasks):
for task in sorted([task.name for task in self.bot.tasks]):
threadlist = [t for t in threads if t.name.startswith(task)]
ids = [str(t.ident) for t in threadlist]
if not ids:
@@ -138,7 +135,7 @@ class Command(BaseCommand):
self.reply(data, msg)
return

if task_name not in self.bot.tasks:
if task_name not in [task.name for task in self.bot.tasks]:
# This task does not exist or hasn't been loaded:
msg = "task could not be found; either it doesn't exist, or it wasn't loaded correctly."
self.reply(data, msg.format(task_name))


+ 70
- 0
earwigbot/commands/time.py Переглянути файл

@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from datetime import datetime, timedelta
from math import floor
from time import time

from earwigbot.commands import BaseCommand

class Command(BaseCommand):
"""Report the current time in any timezone (UTC default), or in beats."""
name = "time"
commands = ["time", "beats", "swatch"]
timezones = [
"UTC": 0,
"EST": -5,
"EDT": -4,
"CST": -6,
"CDT": -5,
"MST": -7,
"MDT": -6,
"PST": -8,
"PDT": -7,
]

def process(self, data):
if data.command in ["beats", "swatch"]:
self.do_beats(data)
return
if data.args:
timezone = data.args[0]
else:
timezone = "UTC"
if timezone in ["beats", "swatch"]:
self.do_beats(data)
else:
self.do_time(data, timezone)

def do_beats(self, data):
beats = ((time() + 3600) % 86400) / 86.4
beats = int(floor(beats))
self.reply(data, "@{0:0>3}".format(beats))

def do_time(self, data, timezone):
now = datetime.utcnow()
try:
now += timedelta(hours=self.timezones[timezone]) # Timezone offset
except KeyError:
self.reply(data, "unknown timezone: {0}.".format(timezone))
return
self.reply(data, now.strftime("%Y-%m-%d %H:%M:%S") + " " + timezone)

+ 14
- 19
earwigbot/managers.py Переглянути файл

@@ -24,7 +24,7 @@
import imp
from os import listdir, path
from re import sub
from threading import Lock, Thread
from threading import RLock, Thread
from time import gmtime, strftime

from earwigbot.commands import BaseCommand
@@ -46,11 +46,7 @@ class _ResourceManager(object):

This class handles the low-level tasks of (re)loading resources via
:py:meth:`load`, retrieving specific resources via :py:meth:`get`, and
iterating over all resources via :py:meth:`__iter__`. If iterating over
resources, it is recommended to acquire :py:attr:`self.lock <lock>`
beforehand and release it afterwards (alternatively, wrap your code in a
``with`` statement) so an attempt at reloading resources in another thread
won't disrupt your iteration.
iterating over all resources via :py:meth:`__iter__`.
"""
def __init__(self, bot, name, attribute, base):
self.bot = bot
@@ -60,16 +56,12 @@ class _ResourceManager(object):
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_access_lock = Lock()

@property
def lock(self):
"""The resource access/modify lock."""
return self._resource_access_lock
self._resource_access_lock = RLock()

def __iter__(self):
for name in self._resources:
yield name
with self.lock:
for resource in self._resources.itervalues():
yield resource

def _load_resource(self, name, path):
"""Load a specific resource from a module, identified by name and path.
@@ -118,6 +110,11 @@ class _ResourceManager(object):
self._load_resource(modname, dir)
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"
@@ -138,7 +135,8 @@ class _ResourceManager(object):
Will raise :py:exc:`KeyError` if the resource (a command or task) is
not found.
"""
return self._resources[key]
with self.lock:
return self._resources[key]


class CommandManager(_ResourceManager):
@@ -167,13 +165,10 @@ class CommandManager(_ResourceManager):

def call(self, hook, data):
"""Respond to a hook type and a :py:class:`Data` object."""
self.lock.acquire()
for command in self._resources.itervalues():
for command in self:
if hook in command.hooks and self._wrap_check(command, data):
self.lock.release()
self._wrap_process(command, data)
return
self.lock.release()


class TaskManager(_ResourceManager):


Завантаження…
Відмінити
Зберегти