瀏覽代碼

Merge branch 'feature/tests-framework' into develop

tags/v0.1^2
Ben Kurtovic 13 年之前
父節點
當前提交
4a7f901d71
共有 70 個檔案被更改,包括 1542 行新增1416 行删除
  1. +1
    -1
      .gitignore
  2. +0
    -0
      bot/__init__.py
  3. +0
    -0
      bot/blowfish.py
  4. +1
    -0
      bot/classes/__init__.py
  5. +48
    -0
      bot/classes/base_command.py
  6. +27
    -0
      bot/classes/base_task.py
  7. +88
    -0
      bot/classes/connection.py
  8. +57
    -0
      bot/classes/data.py
  9. +73
    -0
      bot/classes/rc.py
  10. +90
    -0
      bot/commands/__init__.py
  11. +0
    -0
      bot/commands/_old.py
  12. +87
    -0
      bot/commands/afc_report.py
  13. +38
    -28
      bot/commands/afc_status.py
  14. +7
    -15
      bot/commands/calc.py
  15. +30
    -0
      bot/commands/chanops.py
  16. +54
    -0
      bot/commands/crypt.py
  17. +178
    -0
      bot/commands/git.py
  18. +49
    -0
      bot/commands/help.py
  19. +21
    -23
      bot/commands/link.py
  20. +51
    -0
      bot/commands/remind.py
  21. +37
    -0
      bot/commands/rights.py
  22. +16
    -0
      bot/commands/test.py
  23. +146
    -0
      bot/commands/threads.py
  24. +2
    -3
      bot/config.py
  25. +112
    -0
      bot/frontend.py
  26. +7
    -13
      bot/main.py
  27. +11
    -14
      bot/rules.py
  28. +109
    -0
      bot/tasks/__init__.py
  29. +2
    -2
      bot/tasks/afc_catdelink.py
  30. +2
    -2
      bot/tasks/afc_copyvios.py
  31. +2
    -2
      bot/tasks/afc_dailycats.py
  32. +5
    -2
      bot/tasks/afc_statistics.py
  33. +2
    -2
      bot/tasks/afc_undated.py
  34. +2
    -2
      bot/tasks/blptag.py
  35. +2
    -2
      bot/tasks/feed_dailycats.py
  36. +2
    -2
      bot/tasks/wrongmime.py
  37. +91
    -0
      bot/watcher.py
  38. +20
    -0
      bot/wiki/__init__.py
  39. +2
    -2
      bot/wiki/category.py
  40. +1
    -1
      bot/wiki/constants.py
  41. +1
    -1
      bot/wiki/exceptions.py
  42. +15
    -7
      bot/wiki/functions.py
  43. +34
    -8
      bot/wiki/page.py
  44. +9
    -8
      bot/wiki/site.py
  45. +7
    -4
      bot/wiki/user.py
  46. +3
    -3
      earwigbot.py
  47. +0
    -0
     
  48. +0
    -33
      irc/classes/base_command.py
  49. +0
    -75
      irc/classes/connection.py
  50. +0
    -55
      irc/classes/data.py
  51. +0
    -57
      irc/classes/rc.py
  52. +0
    -66
      irc/command_handler.py
  53. +0
    -0
     
  54. +0
    -104
      irc/commands/afc_report.py
  55. +0
    -31
      irc/commands/chanops.py
  56. +0
    -71
      irc/commands/crypt.py
  57. +0
    -163
      irc/commands/git.py
  58. +0
    -54
      irc/commands/help.py
  59. +0
    -50
      irc/commands/remind.py
  60. +0
    -38
      irc/commands/rights.py
  61. +0
    -133
      irc/commands/tasks.py
  62. +0
    -26
      irc/commands/test.py
  63. +0
    -104
      irc/frontend.py
  64. +0
    -78
      irc/watcher.py
  65. +0
    -0
     
  66. +0
    -0
     
  67. +0
    -19
      wiki/base_task.py
  68. +0
    -92
      wiki/task_manager.py
  69. +0
    -0
     
  70. +0
    -20
      wiki/tools/__init__.py

+ 1
- 1
.gitignore 查看文件

@@ -8,7 +8,7 @@ config.json
.cookies

# Ignore OS X's crud:
*.DS_Store
.DS_Store

# Ignore pydev's nonsense:
.project


core/__init__.py → bot/__init__.py 查看文件


lib/blowfish.py → bot/blowfish.py 查看文件


irc/classes/__init__.py → bot/classes/__init__.py 查看文件

@@ -1,4 +1,5 @@
from base_command import *
from base_task import *
from connection import *
from data import *
from rc import *

+ 48
- 0
bot/classes/base_command.py 查看文件

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-

class BaseCommand(object):
"""A base class for commands on IRC.

This docstring is reported to the user when they use !help <command>.
"""
# This is the command's name, as reported to the user when they use !help:
name = "base_command"
# 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:
hooks = ["msg"]

def __init__(self, connection):
"""Constructor for new commands.
This is called once when the command is loaded (from
commands._load_command()). `connection` is a Connection object,
allowing us to do self.connection.say(), self.connection.send(), etc,
from within a method.
"""
self.connection = connection

def check(self, data):
"""Returns whether this command should be called in response to 'data'.

Given a Data() instance, return True if we should respond to this
activity, or False if we should ignore it or it doesn't apply to us.

Most commands return True if data.command == self.name, otherwise they
return False. This is the default behavior of check(); you need only
override it if you wish to change that.
"""
if data.is_command and data.command == self.name:
return True
return False

def process(self, data):
"""Main entry point for doing a command.

Handle an activity (usually a message) on IRC. At this point, thanks
to self.check() which is called automatically by the command handler,
we know this is something we should respond to, so (usually) something
like 'if data.command != "command_name": return' is unnecessary.
"""
pass

+ 27
- 0
bot/classes/base_task.py 查看文件

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-

class BaseTask(object):
"""A base class for bot tasks that edit Wikipedia."""
name = None

def __init__(self):
"""Constructor for new tasks.

This is called once immediately after the task class is loaded by
the task manager (in tasks._load_task()).
"""
pass

def run(self, **kwargs):
"""Main entry point to run a given task.

This is called directly by tasks.start() and is the main way to make a
task do stuff. kwargs will be any keyword arguments passed to start()
which are entirely optional.

The same task instance is preserved between runs, so you can
theoretically store data in self (e.g.
start('mytask', action='store', data='foo')) and then use it later
(e.g. start('mytask', action='save')).
"""
pass

+ 88
- 0
bot/classes/connection.py 查看文件

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-

import socket
import threading

class BrokenSocketException(Exception):
"""A socket has broken, because it is not sending data. Raised by
Connection.get()."""
pass

class Connection(object):
"""A class to interface with IRC."""
def __init__(self, host=None, port=None, nick=None, ident=None,
realname=None):
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.lock = threading.Lock()

def connect(self):
"""Connect to our IRC server."""
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
self.send("NICK %s" % self.nick)
self.send("USER %s %s * :%s" % (self.ident, self.host, self.realname))

def close(self):
"""Close our connection with the IRC server."""
try:
self.sock.shutdown(socket.SHUT_RDWR) # shut down connection first
except socket.error:
pass # ignore if the socket is already down
self.sock.close()

def get(self, size=4096):
"""Receive (i.e. get) data from the server."""
data = self.sock.recv(4096)
if not data:
# Socket isn't giving us any data, so it is dead or broken:
raise BrokenSocketException()
return data

def send(self, msg):
"""Send data to the server."""
# Ensure that we only send one message at a time with a blocking lock:
with self.lock:
self.sock.sendall(msg + "\r\n")
print " %s" % msg

def say(self, target, msg):
"""Send a private message to a target on the server."""
message = "".join(("PRIVMSG ", target, " :", msg))
self.send(message)

def reply(self, data, msg):
"""Send a private message as a reply to a user on the server."""
message = "".join((chr(2), data.nick, chr(0x0f), ": ", msg))
self.say(data.chan, message)

def action(self, target, msg):
"""Send a private message to a target on the server as an action."""
message = "".join((chr(1), "ACTION ", msg, chr(1)))
self.say(target, message)

def notice(self, target, msg):
"""Send a notice to a target on the server."""
message = "".join(("NOTICE ", target, " :", msg))
self.send(message)

def join(self, chan):
"""Join a channel on the server."""
message = " ".join(("JOIN", chan))
self.send(message)

def part(self, chan):
"""Part from a channel on the server."""
message = " ".join(("PART", chan))
self.send(message)

def mode(self, chan, level, msg):
"""Send a mode message to the server."""
message = " ".join(("MODE", chan, level, msg))
self.send(message)

+ 57
- 0
bot/classes/data.py 查看文件

@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-

import re

class KwargParseException(Exception):
"""Couldn't parse a certain keyword argument in self.args, probably because
it was given incorrectly: e.g., no value (abc), just a value (=xyz), just
an equal sign (=), instead of the correct (abc=xyz)."""
pass

class Data(object):
"""Store data from an individual line received on IRC."""
def __init__(self, line):
self.line = line
self.chan = self.nick = self.ident = self.host = self.msg = ""

def parse_args(self):
"""Parse command args from self.msg into self.command and self.args."""
args = self.msg.strip().split(" ")

while "" in args:
args.remove("")

# Isolate command arguments:
self.args = args[1:]
self.is_command = False # is this message a command?

try:
self.command = args[0]
except IndexError:
self.command = None

try:
if self.command.startswith('!') or self.command.startswith('.'):
self.is_command = True
self.command = self.command[1:] # Strip the '!' or '.'
self.command = self.command.lower()
except AttributeError:
pass

def parse_kwargs(self):
"""Parse keyword arguments embedded in self.args.
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 KwargParseException(arg)
if key and value:
self.kwargs[key] = value
else:
raise KwargParseException(arg)

+ 73
- 0
bot/classes/rc.py 查看文件

@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-

import re

class RC(object):
"""A class to store data on an event received from our IRC watcher."""
re_color = re.compile("\x03([0-9]{1,2}(,[0-9]{1,2})?)?")
re_edit = re.compile("\A\[\[(.*?)\]\]\s(.*?)\s(http://.*?)\s\*\s(.*?)\s\*\s(.*?)\Z")
re_log = re.compile("\A\[\[(.*?)\]\]\s(.*?)\s\*\s(.*?)\s\*\s(.*?)\Z")

def __init__(self, msg):
self.msg = msg

def parse(self):
"""Parse a recent change event into some variables."""
# Strip IRC color codes; we don't want or need 'em:
self.msg = self.re_color.sub("", self.msg).strip()
msg = self.msg
self.is_edit = True

# Flags: 'M' for minor edit, 'B' for bot edit, 'create' for a user
# creation log entry, etc:
try:
page, self.flags, url, user, comment = self.re_edit.findall(msg)[0]
except IndexError:
# We're probably missing the http:// part, because it's a log
# entry, which lacks a URL:
page, flags, user, comment = self.re_log.findall(msg)[0]
url = "".join(("http://en.wikipedia.org/wiki/", page))

self.is_edit = False # This is a log entry, not edit

# Flags tends to have extra whitespace at the end when they're
# log entries:
self.flags = flags.strip()

self.page, self.url, self.user, self.comment = page, url, user, comment

def prettify(self):
"""Make a nice, colorful message to send back to the IRC front-end."""
flags = self.flags
# "New <event>:" if we don't know exactly what happened:
event_type = flags
if "N" in flags:
event_type = "page" # "New page:"
elif flags == "delete":
event_type = "deletion" # "New deletion:"
elif flags == "protect":
event_type = "protection" # "New protection:"
elif flags == "create":
event_type = "user" # "New user:"
if self.page == "Special:Log/move":
event_type = "move" # New move:
else:
event_type = "edit" # "New edit:"
if "B" in flags:
# "New bot edit:"
event_type = "bot {}".format(event_type)
if "M" in flags:
# "New minor edit:" OR "New minor bot edit:"
event_type = "minor {}".format(event_type)

# Example formatting:
# New edit: [[Page title]] * User name * http://en... * edit summary
if self.is_edit:
return "".join(("\x02New ", event_type, "\x0F: \x0314[[\x0307",
self.page, "\x0314]]\x0306 *\x0303 ", self.user,
"\x0306 *\x0302 ", self.url, "\x0306 *\x0310 ",
self.comment))

return "".join(("\x02New ", event_type, "\x0F: \x0303", self.user,
"\x0306 *\x0302 ", self.url, "\x0306 *\x0310 ",
self.comment))

+ 90
- 0
bot/commands/__init__.py 查看文件

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's IRC Command Manager

This package provides the IRC "commands" used by the bot's front-end component.
In __init__, you can find some functions used to load and run these commands.
"""

import os
import sys
import traceback

from classes import BaseCommand
import config

__all__ = ["load", "get_all", "check"]

# Base directory when searching for commands:
base_dir = os.path.join(config.root_dir, "bot", "commands")

# Store commands in a dict, where the key is the command's name and the value
# is an instance of the command's class:
_commands = {}

def _load_command(connection, filename):
"""Try to load a specific command from a module, identified by file name.

Given a Connection object and a filename, we'll first try to import it,
and if that works, make an instance of the 'Command' class inside (assuming
it is an instance of BaseCommand), add it to _commands, and report the
addition to the user. Any problems along the way will either be ignored or
reported.
"""
global _commands

# Strip .py from the end of the filename and join with our package name:
name = ".".join(("commands", filename[:-3]))
try:
__import__(name)
except:
print "Couldn't load file {0}:".format(filename)
traceback.print_exc()
return

command = sys.modules[name].Command(connection)
if not isinstance(command, BaseCommand):
return

_commands[command.name] = command
print "Added command {0}...".format(command.name)

def load(connection):
"""Load all valid commands into the _commands global variable.

`connection` is a Connection object that is given to each command's
constructor.
"""
files = os.listdir(base_dir)
files.sort()

for filename in files:
if filename.startswith("_") or not filename.endswith(".py"):
continue
try:
_load_command(connection, filename)
except AttributeError:
pass # The file is doesn't contain a command, so just move on

msg = "Found {0} commands: {1}."
print msg.format(len(_commands), ", ".join(_commands.keys()))

def get_all():
"""Return our dict of all loaded commands."""
return _commands

def check(hook, data):
"""Given an event on IRC, check if there's anything we can respond to."""
# Parse command arguments into data.command and data.args:
data.parse_args()

for command in _commands.values():
if hook in command.hooks:
if command.check(data):
try:
command.process(data)
except:
print "Error executing command '{0}':".format(data.command)
traceback.print_exc()
break

irc/commands/_old.py → bot/commands/_old.py 查看文件


+ 87
- 0
bot/commands/afc_report.py 查看文件

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-

import re

from classes import BaseCommand
import wiki

class Command(BaseCommand):
"""Get information about an AFC submission by name."""
name = "report"

def process(self, data):
self.site = wiki.get_site()
self.data = data

if not data.args:
msg = "what submission do you want me to give information about?"
self.connection.reply(data, msg)
return

title = ' '.join(data.args)
title = title.replace("http://en.wikipedia.org/wiki/", "")
title = title.replace("http://enwp.org/", "").strip()

# Given '!report Foo', first try [[Foo]]:
if self.report(title):
return

# Then try [[Wikipedia:Articles for creation/Foo]]:
title2 = "".join(("Wikipedia:Articles for creation/", title))
if self.report(title2):
return

# Then try [[Wikipedia talk:Articles for creation/Foo]]:
title3 = "".join(("Wikipedia talk:Articles for creation/", title))
if self.report(title3):
return

msg = "submission \x0302{0}\x0301 not found.".format(title)
self.connection.reply(data, msg)

def report(self, title):
data = self.data
page = self.site.get_page(title, follow_redirects=False)
if not page.exists()[0]:
return

url = page.url().replace("en.wikipedia.org/wiki", "enwp.org")
short = re.sub(r"wikipedia( talk)?:articles for creation/", "", title,
re.IGNORECASE)
status = self.get_status(page)
user = self.site.get_user(page.creator())
user_name = user.name()
user_url = user.get_talkpage().url()

msg1 = "AfC submission report for \x0302{0}\x0301 ({1}):"
msg2 = "Status: \x0303{0}\x0301"
msg3 = "Submitted by \x0302{0}\x0301 ({1})"
if status == "accepted":
msg3 = "Reviewed by \x0302{0}\x0301 ({1})"

self.connection.reply(data, msg1.format(short, url))
self.connection.say(data.chan, msg2.format(status))
self.connection.say(data.chan, msg3.format(user_name, user_url))

return True

def get_status(self, page):
content = page.get()

if page.is_redirect():
target = page.get_redirect_target()
if self.site.get_page(target).namespace() == 0:
return "accepted"
return "redirect"
if re.search("\{\{afc submission\|r\|(.*?)\}\}", content, re.I):
return "being reviewed"
if re.search("\{\{afc submission\|\|(.*?)\}\}", content, re.I):
return "pending"
if re.search("\{\{afc submission\|d\|(.*?)\}\}", content, re.I):
regex = "\{\{afc submission\|d\|(.*?)(\||\}\})"
try:
reason = re.findall(regex, content, re.I)[0][0]
except IndexError:
return "declined"
return "declined with reason \"{0}\"".format(reason)
return "unkown"

irc/commands/afc_status.py → bot/commands/afc_status.py 查看文件

@@ -1,24 +1,22 @@
# -*- coding: utf-8 -*-

"""Report the status of AFC submissions, either as an automatic message on join
or a request via !status."""

import re

from core import config
from irc.classes import BaseCommand
from wiki import tools

class AFCStatus(BaseCommand):
def get_hooks(self):
return ["join", "msg"]
from classes import BaseCommand
import config
import wiki

def get_help(self, command):
return "Get the number of pending AfC submissions, open redirect requests, and open file upload requests."
class Command(BaseCommand):
"""Get the number of pending AfC submissions, open redirect requests, and
open file upload requests."""
name = "status"
hooks = ["join", "msg"]

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

try:
if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc":
if data.nick != config.irc["frontend"]["nick"]:
@@ -28,7 +26,7 @@ class AFCStatus(BaseCommand):
return False

def process(self, data):
self.site = tools.get_site()
self.site = wiki.get_site()

if data.line[1] == "JOIN":
notice = self.get_join_notice()
@@ -39,41 +37,48 @@ class AFCStatus(BaseCommand):
action = data.args[0].lower()
if action.startswith("sub") or action == "s":
subs = self.count_submissions()
self.connection.reply(data, "there are currently %s pending AfC submissions." % subs)
msg = "there are currently {0} pending AfC submissions."
self.connection.reply(data, msg.format(subs))

elif action.startswith("redir") or action == "r":
redirs = self.count_redirects()
self.connection.reply(data, "there are currently %s open redirect requests." % redirs)
msg = "there are currently {0} open redirect requests."
self.connection.reply(data, msg.format(redirs))

elif action.startswith("file") or action == "f":
files = self.count_redirects()
self.connection.reply(data, "there are currently %s open file upload requests." % files)
msg = "there are currently {0} open file upload requests."
self.connection.reply(data, msg.format(files))

elif action.startswith("agg") or action == "a":
try:
agg_num = int(data.args[1])
except IndexError:
agg_data = (self.count_submissions(), self.count_redirects(), self.count_files())
agg_data = (self.count_submissions(),
self.count_redirects(), self.count_files())
agg_num = self.get_aggregate_number(agg_data)
except ValueError:
self.connection.reply(data, "\x0303%s\x0301 isn't a number!" % data.args[1])
msg = "\x0303{0}\x0301 isn't a number!"
self.connection.reply(data, msg.format(data.args[1]))
return
aggregate = self.get_aggregate(agg_num)
self.connection.reply(data, "aggregate is currently %s (AfC %s)." % (agg_num, aggregate))
msg = "aggregate is currently {0} (AfC {1})."
self.connection.reply(data, msg.format(agg_num, aggregate))

elif action.startswith("join") or action == "j":
notice = self.get_join_notice()
self.connection.reply(data, notice)

else:
self.connection.reply(data, "unknown argument: \x0303%s\x0301. Valid args are 'subs', 'redirs', 'files', 'agg', and 'join'." % data.args[0])
msg = "unknown argument: \x0303{0}\x0301. Valid args are 'subs', 'redirs', 'files', 'agg', and 'join'."
self.connection.reply(data, msg.format(data.args[0]))

else:
subs = self.count_submissions()
redirs = self.count_redirects()
files = self.count_files()
self.connection.reply(data, "there are currently %s pending submissions, %s open redirect requests, and %s open file upload requests."
% (subs, redirs, files))
msg = "there are currently {0} pending submissions, {1} open redirect requests, and {2} open file upload requests."
self.connection.reply(data, msg.format(subs, redirs, files))

def get_join_notice(self):
subs = self.count_submissions()
@@ -81,20 +86,25 @@ class AFCStatus(BaseCommand):
files = self.count_files()
agg_num = self.get_aggregate_number((subs, redirs, files))
aggregate = self.get_aggregate(agg_num)
return ("\x02Current status:\x0F Articles for Creation %s (\x0302AFC\x0301: \x0305%s\x0301; \x0302AFC/R\x0301: \x0305%s\x0301; \x0302FFU\x0301: \x0305%s\x0301)"
% (aggregate, subs, redirs, files))

msg = "\x02Current status:\x0F Articles for Creation {0} (\x0302AFC\x0301: \x0305{1}\x0301; \x0302AFC/R\x0301: \x0305{2}\x0301; \x0302FFU\x0301: \x0305{3}\x0301)"
return msg.format(aggregate, subs, redirs, files)

def count_submissions(self):
"""Returns the number of open AFC submissions (count of CAT:PEND)."""
cat = self.site.get_category("Pending AfC submissions")
subs = cat.members(limit=500)
subs -= 2 # remove [[Wikipedia:Articles for creation/Redirects]] and [[Wikipedia:Files for upload]], which aren't real submissions
subs = len(cat.members(limit=500))

# Remove [[Wikipedia:Articles for creation/Redirects]] and
# [[Wikipedia:Files for upload]], which aren't real submissions:
subs -= 2
return subs

def count_redirects(self):
"""Returns the number of open redirect submissions. Calculated as the
total number of submissions minus the closed ones."""
content = self.site.get_page("Wikipedia:Articles for creation/Redirects").get()
title = "Wikipedia:Articles for creation/Redirects"
content = self.site.get_page(title).get()
total = len(re.findall("^\s*==(.*?)==\s*$", content, re.MULTILINE))
closed = content.lower().count("{{afc-c|b}}")
redirs = total - closed

irc/commands/calc.py → bot/commands/calc.py 查看文件

@@ -1,23 +1,14 @@
# -*- coding: utf-8 -*-

# A somewhat advanced calculator: http://futureboy.us/fsp/frink.fsp.

import re
import urllib

from irc.classes import BaseCommand

class Calc(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
return "A somewhat advanced calculator: see http://futureboy.us/fsp/frink.fsp for details."
from classes import BaseCommand

def check(self, data):
if data.is_command and data.command == "calc":
return True
return False
class Command(BaseCommand):
"""A somewhat advanced calculator: see http://futureboy.us/fsp/frink.fsp
for details."""
name = "calc"

def process(self, data):
if not data.args:
@@ -27,7 +18,8 @@ class Calc(BaseCommand):
query = ' '.join(data.args)
query = self.cleanup(query)

url = "http://futureboy.us/fsp/frink.fsp?fromVal=%s" % urllib.quote(query)
url = "http://futureboy.us/fsp/frink.fsp?fromVal={0}"
url = url.format(urllib.quote(query))
result = urllib.urlopen(url).read()

r_result = re.compile(r'(?i)<A NAME=results>(.*?)</A>')

+ 30
- 0
bot/commands/chanops.py 查看文件

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-

from classes import BaseCommand
import config

class Command(BaseCommand):
"""Voice, devoice, op, or deop users in the channel."""
name = "chanops"

def check(self, data):
commands = ["voice", "devoice", "op", "deop"]
if data.is_command and data.command in commands:
return True
return False

def process(self, data):
if data.host not in config.irc["permissions"]["admins"]:
msg = "you must be a bot admin to use this command."
self.connection.reply(data, msg)
return

# If it is just !op/!devoice/whatever without arguments, assume they
# want to do this to themselves:
if not data.args:
target = data.nick
else:
target = data.args[0]

msg = " ".join((data.command, data.chan, target))
self.connection.say("ChanServ", msg)

+ 54
- 0
bot/commands/crypt.py 查看文件

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-

import hashlib

from classes import BaseCommand
import blowfish

class Command(BaseCommand):
"""Provides hash functions with !hash (!hash list for supported algorithms)
and blowfish encryption with !encrypt and !decrypt."""
name = "cryptography"

def check(self, data):
if data.is_command and data.command in ["hash", "encrypt", "decrypt"]:
return True
return False

def process(self, data):
if not data.args:
msg = "what do you want me to {0}?".format(data.command)
self.connection.reply(data, msg)
return

if data.command == "hash":
algo = data.args[0]
if algo == "list":
algos = ', '.join(hashlib.algorithms)
msg = algos.join(("supported algorithms: ", "."))
self.connection.reply(data, msg)
elif algo in hashlib.algorithms:
string = ' '.join(data.args[1:])
result = getattr(hashlib, algo)(string).hexdigest()
self.connection.reply(data, result)
else:
msg = "unknown algorithm: '{0}'.".format(algo)
self.connection.reply(data, msg)

else:
key = data.args[0]
text = ' '.join(data.args[1:])

if not text:
msg = "a key was provided, but text to {0} was not."
self.connection.reply(data, msg.format(data.command))
return

try:
if data.command == "encrypt":
self.connection.reply(data, blowfish.encrypt(key, text))
else:
self.connection.reply(data, blowfish.decrypt(key, text))
except blowfish.BlowfishError as error:
msg = "{0}: {1}.".format(error.__class__.__name__, error)
self.connection.reply(data, msg)

+ 178
- 0
bot/commands/git.py 查看文件

@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-

import shlex
import subprocess
import re

from classes import BaseCommand
import config

class Command(BaseCommand):
"""Commands to interface with the bot's git repository; use '!git help' for
a sub-command list."""
name = "git"

def process(self, data):
self.data = data
if data.host not in config.irc["permissions"]["owners"]:
msg = "you must be a bot owner to use this command."
self.connection.reply(data, msg)
return

if not data.args:
msg = "no arguments provided. Maybe you wanted '!git help'?"
self.connection.reply(data, msg)
return

if data.args[0] == "help":
self.do_help()

elif data.args[0] == "branch":
self.do_branch()

elif data.args[0] == "branches":
self.do_branches()

elif data.args[0] == "checkout":
self.do_checkout()

elif data.args[0] == "delete":
self.do_delete()

elif data.args[0] == "pull":
self.do_pull()

elif data.args[0] == "status":
self.do_status()

else: # They asked us to do something we don't know
msg = "unknown argument: \x0303{0}\x0301.".format(data.args[0])
self.connection.reply(data, msg)

def exec_shell(self, command):
"""Execute a shell command and get the output."""
command = shlex.split(command)
result = subprocess.check_output(command, stderr=subprocess.STDOUT)
if result:
result = result[:-1] # Strip newline
return result

def do_help(self):
"""Display all commands."""
help_dict = {
"branch": "get current branch",
"branches": "get all branches",
"checkout": "switch branches",
"delete": "delete an old branch",
"pull": "update everything from the remote server",
"status": "check if we are up-to-date",
}
keys = help_dict.keys()
keys.sort()
help = ""
for key in keys:
help += "\x0303%s\x0301 (%s), " % (key, help_dict[key])
help = help[:-2] # trim last comma and space
self.connection.reply(self.data, "sub-commands are: %s." % help)

def do_branch(self):
"""Get our current branch."""
branch = self.exec_shell("git name-rev --name-only HEAD")
msg = "currently on branch \x0302{0}\x0301.".format(branch)
self.connection.reply(self.data, msg)

def do_branches(self):
"""Get a list of branches."""
branches = self.exec_shell("git branch")
# Remove extraneous characters:
branches = branches.replace('\n* ', ', ')
branches = branches.replace('* ', ' ')
branches = branches.replace('\n ', ', ')
branches = branches.strip()
msg = "branches: \x0302{0}\x0301.".format(branches)
self.connection.reply(self.data, msg)

def do_checkout(self):
"""Switch branches."""
try:
branch = self.data.args[1]
except IndexError: # no branch name provided
self.connection.reply(self.data, "switch to which branch?")
return

current_branch = self.exec_shell("git name-rev --name-only HEAD")

try:
result = self.exec_shell("git checkout %s" % branch)
if "Already on" in result:
msg = "already on \x0302{0}\x0301!".format(branch)
self.connection.reply(self.data, msg)
else:
ms = "switched from branch \x0302{1}\x0301 to \x0302{1}\x0301."
msg = ms.format(current_branch, branch)
self.connection.reply(self.data, msg)

except subprocess.CalledProcessError:
# Git couldn't switch branches; assume the branch doesn't exist:
msg = "branch \x0302{0}\x0301 doesn't exist!".format(branch)
self.connection.reply(self.data, msg)

def do_delete(self):
"""Delete a branch, while making sure that we are not already on it."""
try:
delete_branch = self.data.args[1]
except IndexError: # no branch name provided
self.connection.reply(self.data, "delete which branch?")
return

current_branch = self.exec_shell("git name-rev --name-only HEAD")

if current_branch == delete_branch:
msg = "you're currently on this branch; please checkout to a different branch before deleting."
self.connection.reply(self.data, msg)
return

try:
self.exec_shell("git branch -d %s" % delete_branch)
msg = "branch \x0302{0}\x0301 has been deleted locally."
self.connection.reply(self.data, msg.format(delete_branch))
except subprocess.CalledProcessError:
# Git couldn't switch branches; assume the branch doesn't exist:
msg = "branch \x0302{0}\x0301 doesn't exist!".format(delete_branch)
self.connection.reply(self.data, msg)

def do_pull(self):
"""Pull from our remote repository."""
branch = self.exec_shell("git name-rev --name-only HEAD")
msg = "pulling from remote (currently on \x0302{0}\x0301)..."
self.connection.reply(self.data, msg.format(branch))

result = self.exec_shell("git pull")

if "Already up-to-date." in result:
self.connection.reply(self.data, "done; no new changes.")
else:
regex = "\s*((.*?)\sfile(.*?)tions?\(-\))"
changes = re.findall(regex, result)[0][0]
try:
cmnd_remt = "git config --get branch.{0}.remote".format(branch)
remote = self.exec_shell(cmnd_rmt)
cmnd_url = "git config --get remote.{0}.url".format(remote)
url = self.exec_shell(cmnd_url)
msg = "done; {0} [from {1}].".format(changes, url)
self.connection.reply(self.data, msg)
except subprocess.CalledProcessError:
# Something in .git/config is not specified correctly, so we
# cannot get the remote's URL. However, pull was a success:
self.connection.reply(self.data, "done; %s." % changes)

def do_status(self):
"""Check whether we have anything to pull."""
last = self.exec_shell('git log -n 1 --pretty="%ar"')
result = self.exec_shell("git fetch --dry-run")
if not result: # Nothing was fetched, so remote and local are equal
msg = "last commit was {0}. Local copy is \x02up-to-date\x0F with remote."
self.connection.reply(self.data, msg.format(last))
else:
msg = "last local commit was {0}. Remote is \x02ahead\x0F of local copy."
self.connection.reply(self.data, msg.format(last))

+ 49
- 0
bot/commands/help.py 查看文件

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-

import re

from classes import BaseCommand, Data
import commands

class Command(BaseCommand):
"""Displays help information."""
name = "help"

def process(self, data):
self.cmnds = commands.get_all()
if not data.args:
self.do_main_help(data)
else:
self.do_command_help(data)

def do_main_help(self, data):
"""Give the user a general help message with a list of all commands."""
msg = "I am a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help <command>'."
cmnds = self.cmnds.keys()
cmnds.sort()
msg = msg.format(len(cmnds), ', '.join(cmnds))
self.connection.reply(data, msg)

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

# Create a dummy message to test which commands pick up the user's
# input:
dummy = Data("PRIVMSG #fake-channel :Fake messsage!")
dummy.command = command.lower()
dummy.is_command = True

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

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

irc/commands/link.py → bot/commands/link.py 查看文件

@@ -1,17 +1,13 @@
# -*- coding: utf-8 -*-

# Convert a Wikipedia page name into a URL.

import re
from urllib import quote

from irc.classes import BaseCommand

class Link(BaseCommand):
def get_hooks(self):
return ["msg"]
from classes import BaseCommand

def get_help(self, command):
return "Convert a Wikipedia page name into a URL."
class Command(BaseCommand):
"""Convert a Wikipedia page name into a URL."""
name = "link"

def check(self, data):
if ((data.is_command and data.command == "link") or
@@ -37,29 +33,31 @@ class Link(BaseCommand):
self.connection.reply(data, link)

def parse_line(self, line):
results = list()
results = []

line = re.sub("\{\{\{(.*?)\}\}\}", "", line) # destroy {{{template parameters}}}
# Destroy {{{template parameters}}}:
line = re.sub("\{\{\{(.*?)\}\}\}", "", line)

links = re.findall("(\[\[(.*?)(\||\]\]))", line) # find all [[links]]
# Find all [[links]]:
links = re.findall("(\[\[(.*?)(\||\]\]))", line)
if links:
links = map(lambda x: x[1], links) # re.findall() returns a list of tuples, but we only want the 2nd item in each tuple
results.extend(map(self.parse_link, links))
# re.findall() returns a list of tuples, but we only want the 2nd
# item in each tuple:
links = [i[1] for i in links]
results = map(self.parse_link, links)

templates = re.findall("(\{\{(.*?)(\||\}\}))", line) # find all {{templates}}
# Find all {{templates}}
templates = re.findall("(\{\{(.*?)(\||\}\}))", line)
if templates:
templates = map(lambda x: x[1], templates)
templates = [i[1] for i in templates]
results.extend(map(self.parse_template, templates))

return results

def parse_link(self, pagename):
pagename = pagename.strip()
link = "http://enwp.org/" + pagename
link = link.replace(" ", "_")
return link
link = quote(pagename.replace(" ", "_"), safe="/:")
return "".join(("http://enwp.org/", link))

def parse_template(self, pagename):
pagename = "Template:%s" % pagename # TODO: implement an actual namespace check
link = self.parse_link(pagename)
return link
pagename = "".join(("Template:", pagename))
return self.parse_link(pagename)

+ 51
- 0
bot/commands/remind.py 查看文件

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-

import threading
import time

from classes 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):
if data.is_command and data.command in ["remind", "reminder"]:
return True
return False

def process(self, data):
if not data.args:
msg = "please specify a time (in seconds) and a message in the following format: !remind <time> <msg>."
self.connection.reply(data, msg)
return

try:
wait = int(data.args[0])
except ValueError:
msg = "the time must be given as an integer, in seconds."
self.connection.reply(data, msg)
return
message = ' '.join(data.args[1:])
if not message:
msg = "what message do you want me to give you when time is up?"
self.connection.reply(data, msg)
return

end = time.localtime(time.time() + wait)
end_time = time.strftime("%b %d %H:%M:%S", end)
end_time_with_timezone = time.strftime("%b %d %H:%M:%S %Z", end)

msg = 'Set reminder for "{0}" in {1} seconds (ends {2}).'
msg = msg.format(message, wait, end_time_with_timezone)
self.connection.reply(data, msg)

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

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

+ 37
- 0
bot/commands/rights.py 查看文件

@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-

from classes import BaseCommand
import wiki

class Command(BaseCommand):
"""Retrieve a list of rights for a given username."""
name = "rights"

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

def process(self, data):
if not data.args:
self.connection.reply(data, "who do you want me to look up?")
return

username = ' '.join(data.args)
site = wiki.get_site()
user = site.get_user(username)
try:
rights = user.groups()
except wiki.UserNotFoundError:
msg = "the user \x0302{0}\x0301 does not exist."
self.connection.reply(data, msg.format(username))
return

try:
rights.remove("*") # Remove the '*' group given to everyone
except ValueError:
pass
msg = "the rights for \x0302{0}\x0301 are {1}."
self.connection.reply(data, msg.format(username, ', '.join(rights)))

+ 16
- 0
bot/commands/test.py 查看文件

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-

import random

from classes import BaseCommand

class Command(BaseCommand):
"""Test the bot!"""
name = "test"

def process(self, data):
hey = random.randint(0, 1)
if hey:
self.connection.say(data.chan, "Hey \x02%s\x0F!" % data.nick)
else:
self.connection.say(data.chan, "'sup \x02%s\x0F?" % data.nick)

+ 146
- 0
bot/commands/threads.py 查看文件

@@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-

import threading
import re

from classes import BaseCommand, Data, KwargParseException
import tasks
import config

class Command(BaseCommand):
"""Manage wiki tasks from IRC, and check on thread status."""
name = "threads"

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

def process(self, data):
self.data = data
if data.host not in config.irc["permissions"]["owners"]:
msg = "at this time, you must be a bot owner to use this command."
self.connection.reply(data, msg)
return

if not data.args:
if data.command == "tasklist":
self.do_list()
else:
msg = "no arguments provided. Maybe you wanted '!{0} list', '!{0} start', or '!{0} listall'?"
self.connection.reply(data, msg.format(data.command))
return

if data.args[0] == "list":
self.do_list()

elif data.args[0] == "start":
self.do_start()

elif data.args[0] in ["listall", "all"]:
self.do_listall()

else: # They asked us to do something we don't know
msg = "unknown argument: \x0303{0}\x0301.".format(data.args[0])
self.connection.reply(data, msg)

def do_list(self):
"""With !tasks list (or abbreviation !tasklist), list all running
threads. This includes the main threads, like the irc frontend and the
watcher, and task threads."""
threads = threading.enumerate()

normal_threads = []
task_threads = []

for thread in threads:
tname = thread.name
if tname == "MainThread":
tname = self.get_main_thread_name()
t = "\x0302{0}\x0301 (as main thread, id {1})"
normal_threads.append(t.format(tname, thread.ident))
elif tname in ["irc-frontend", "irc-watcher", "wiki-scheduler"]:
t = "\x0302{0}\x0301 (id {1})"
normal_threads.append(t.format(tname, thread.ident))
elif tname.startswith("reminder"):
tname = tname.replace("reminder ", "")
t = "\x0302reminder\x0301 (until {0})"
normal_threads.append(t.format(tname))
else:
tname, start_time = re.findall("^(.*?) \((.*?)\)$", tname)[0]
t = "\x0302{0}\x0301 (id {1}, since {2})"
task_threads.append(t.format(tname, thread.ident, start_time))

if task_threads:
msg = "\x02{0}\x0F threads active: {1}, and \x02{2}\x0F task threads: {3}."
msg = msg.format(len(threads), ', '.join(normal_threads),
len(task_threads), ', '.join(task_threads))
else:
msg = "\x02{0}\x0F threads active: {1}, and \x020\x0F task threads."
msg = msg.format(len(threads), ', '.join(normal_threads))

self.connection.reply(self.data, msg)

def do_listall(self):
"""With !tasks listall or !tasks all, list all loaded tasks, and report
whether they are currently running or idle."""
all_tasks = tasks.get_all().keys()
threads = threading.enumerate()
tasklist = []

all_tasks.sort()

for task in all_tasks:
threadlist = [t for t in threads if t.name.startswith(task)]
ids = [str(t.ident) for t in threadlist]
if not ids:
tasklist.append("\x0302{0}\x0301 (idle)".format(task))
elif len(ids) == 1:
t = "\x0302{0}\x0301 (\x02active\x0F as id {1})"
tasklist.append(t.format(task, ids[0]))
else:
t = "\x0302{0}\x0301 (\x02active\x0F as ids {1})"
tasklist.append(t.format(task, ', '.join(ids)))

tasklist = ", ".join(tasklist)

msg = "{0} tasks loaded: {1}.".format(len(all_tasks), tasklist)
self.connection.reply(self.data, msg)

def do_start(self):
"""With !tasks start, start any loaded task by name with or without
kwargs."""
data = self.data

try:
task_name = data.args[1]
except IndexError: # No task name given
self.connection.reply(data, "what task do you want me to start?")
return

try:
data.parse_kwargs()
except KwargParseException, arg:
msg = "error parsing argument: \x0303{0}\x0301.".format(arg)
self.connection.reply(data, msg)
return

if task_name not in tasks.get_all().keys():
# This task does not exist or hasn't been loaded:
msg = "task could not be found; either bot/tasks/{0}.py doesn't exist, or it wasn't loaded correctly."
self.connection.reply(data, msg.format(task_name))
return

tasks.start(task_name, **data.kwargs)
msg = "task \x0302{0}\x0301 started.".format(task_name)
self.connection.reply(data, msg)

def get_main_thread_name(self):
"""Return the "proper" name of the MainThread."""
if "irc_frontend" in config.components:
return "irc-frontend"
elif "wiki_schedule" in config.components:
return "wiki-scheduler"
else:
return "irc-watcher"

core/config.py → bot/config.py 查看文件

@@ -18,9 +18,9 @@ from within config's three global variables and one function:
"""

import json
from os import makedirs, path
from os import path

from lib import blowfish
import blowfish

script_dir = path.dirname(path.abspath(__file__))
root_dir = path.split(script_dir)[0]
@@ -149,7 +149,6 @@ def schedule(minute, hour, month_day, month, week_day):

def make_new_config():
"""Make a new config file based on the user's input."""
makedirs(config_dir)

encrypt = raw_input("Would you like to encrypt passwords stored in " +
"config.json? [y/n] ")

+ 112
- 0
bot/frontend.py 查看文件

@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's IRC Frontend Component

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 BaseCommand in irc/base_command.py. All command classes are automatically
imported by irc/command_handler.py if they are in irc/commands.
"""

import re

import config
import commands
from classes import Connection, Data, BrokenSocketException

__all__ = ["get_connection", "startup", "main"]

connection = None
sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z")

def get_connection():
"""Return a new Connection() instance with information about our server
connection, but don't actually connect yet."""
cf = config.irc["frontend"]
connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"],
cf["realname"])
return connection

def startup(conn):
"""Accept a single arg, a Connection() object, and set our global variable
'connection' to it. Load all command classes in irc/commands with
command_handler, and then establish a connection with the IRC server."""
global connection
connection = conn
commands.load(connection)
connection.connect()

def main():
"""Main loop for the frontend component.

get_connection() and startup() should have already been called before this.
"""
read_buffer = str()

while 1:
try:
read_buffer = read_buffer + connection.get()
except BrokenSocketException:
print "Socket has broken on front-end; restarting bot..."
return

lines = read_buffer.split("\n")
read_buffer = lines.pop()
for line in lines:
_process_message(line)

def _process_message(line):
"""Process a single message from IRC."""
line = line.strip().split()
data = Data(line) # new Data instance to store info about this line

if line[1] == "JOIN":
data.nick, data.ident, data.host = sender_regex.findall(line[0])[0]
data.chan = line[2][1:]
# Check for 'join' hooks in our commands:
commands.check("join", data)

elif line[1] == "PRIVMSG":
data.nick, data.ident, data.host = sender_regex.findall(line[0])[0]
data.msg = ' '.join(line[3:])[1:]
data.chan = line[2]

if data.chan == 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
commands.check("msg_private", data)
else:
# Check for public-only command hooks:
commands.check("msg_public", data)

# Check for command hooks that apply to all messages:
commands.check("msg", data)

# Hardcode the !restart command (we can't restart from within an
# ordinary command):
if data.msg in ["!restart", ".restart"]:
if data.host in config.irc["permissions"]["owners"]:
print "Restarting bot per owner request..."
return

# If we are pinged, pong back to the server:
if line[0] == "PING":
connection.send("PONG %s" % line[1])

# On successful connection to the server:
if line[1] == "376":
# If we're supposed to auth to NickServ, do that:
try:
username = config.irc["frontend"]["nickservUsername"]
password = config.irc["frontend"]["nickservPassword"]
except KeyError:
pass
else:
msg = " ".join(("IDENTIFY", username, password))
connection.say("NickServ", msg)

# Join all of our startup channels:
for chan in config.irc["frontend"]["channels"]:
connection.join(chan)

core/main.py → bot/main.py 查看文件

@@ -33,17 +33,11 @@ Else, the bot will stop, as no components are enabled.
import threading
import time
import traceback
import sys
import os

script_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.split(script_dir)[0] # the bot's "root" directory relative
# to its different components
sys.path.append(root_dir) # make sure we look in the root dir for modules

from core import config
from irc import frontend, watcher
from wiki import task_manager
import config
import frontend
import tasks
import watcher

f_conn = None
w_conn = None
@@ -70,7 +64,7 @@ def wiki_scheduler():
time_start = time.time()
now = time.gmtime(time_start)

task_manager.start_tasks(now)
tasks.schedule(now)

time_end = time.time()
time_diff = time_start - time_end
@@ -89,7 +83,7 @@ def irc_frontend():

if "wiki_schedule" in config.components:
print "\nStarting wiki scheduler..."
task_manager.load_tasks()
tasks.load()
t_scheduler = threading.Thread(target=wiki_scheduler)
t_scheduler.name = "wiki-scheduler"
t_scheduler.daemon = True
@@ -123,7 +117,7 @@ def run():

elif "wiki_schedule" in enabled: # run the scheduler on the main
print "Starting wiki scheduler..." # thread, but also run the IRC
task_manager.load_tasks() # watcher on another thread iff it
tasks.load() # watcher on another thread iff it
if "irc_watcher" in enabled: # is enabled
print "\nStarting IRC watcher..."
t_watcher = threading.Thread(target=irc_watcher, args=())

irc/watcher_logic.py → bot/rules.py 查看文件

@@ -1,18 +1,15 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's IRC Watcher Logic
EarwigBot's IRC Watcher Rules

This file contains (configurable!) rules that EarwigBot's watcher uses after it
recieves an event from IRC.

This should, ideally, be in config.json somehow, but Python code makes more
sense for this sort of thing... so...
"""

import re

from wiki import task_manager as tasks
import tasks

afc_prefix = "wikipedia( talk)?:(wikiproject )?articles for creation"

@@ -39,8 +36,8 @@ def process(rc):
chans.update(("##earwigbot", "#wikipedia-en-afc"))
if r_page.search(page_name):
tasks.start_task("afc_statistics", action="process_edit", page=rc.page)
tasks.start_task("afc_copyvios", action="process_edit", page=rc.page)
tasks.start("afc_statistics", action="process_edit", page=rc.page)
tasks.start("afc_copyvios", action="process_edit", page=rc.page)
chans.add("#wikipedia-en-afc")
elif r_ffu.match(page_name):
@@ -50,22 +47,22 @@ def process(rc):
chans.add("#wikipedia-en-afc")
elif rc.flags == "move" and (r_move1.match(comment) or
r_move2.match(comment)):
r_move2.match(comment)):
p = r_moved_pages.findall(rc.comment)[0]
tasks.start_task("afc_statistics", action="process_move", pages=p)
tasks.start_task("afc_copyvios", action="process_move", pages=p)
tasks.start("afc_statistics", action="process_move", pages=p)
tasks.start("afc_copyvios", action="process_move", pages=p)
chans.add("#wikipedia-en-afc")
elif rc.flags == "delete" and r_delete.match(comment):
p = r_deleted_page.findall(rc.comment)[0]
tasks.start_task("afc_statistics", action="process_delete", page=p)
tasks.start_task("afc_copyvios", action="process_delete", page=p)
tasks.start("afc_statistics", action="process_delete", page=p)
tasks.start("afc_copyvios", action="process_delete", page=p)
chans.add("#wikipedia-en-afc")
elif rc.flags == "restore" and r_restore.match(comment):
p = r_restored_page.findall(rc.comment)[0]
tasks.start_task("afc_statistics", action="process_restore", page=p)
tasks.start_task("afc_copyvios", action="process_restore", page=p)
tasks.start("afc_statistics", action="process_restore", page=p)
tasks.start("afc_copyvios", action="process_restore", page=p)
chans.add("#wikipedia-en-afc")
elif rc.flags == "protect" and r_protect.match(comment):

+ 109
- 0
bot/tasks/__init__.py 查看文件

@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's Wiki Task Manager

This package provides the wiki bot "tasks" EarwigBot runs. Here in __init__,
you can find some functions used to load and run these tasks.
"""

import os
import sys
import threading
import time
import traceback

from classes import BaseTask
import config

__all__ = ["load", "schedule", "start", "get_all"]

# Base directory when searching for tasks:
base_dir = os.path.join(config.root_dir, "bot", "tasks")

# Store loaded tasks as a dict where the key is the task name and the value is
# an instance of the task class:
_tasks = {}

def _load_task(filename):
"""Try to load a specific task from a module, identified by file name."""
global _tasks

# Strip .py from the end of the filename and join with our package name:
name = ".".join(("tasks", filename[:-3]))
try:
__import__(name)
except:
print "Couldn't load file {0}:".format(filename)
traceback.print_exc()
return

task = sys.modules[name].Task()
if not isinstance(task, BaseTask):
return

_tasks[task.name] = task
print "Added task {0}...".format(task.name)

def _wrapper(task, **kwargs):
"""Wrapper for task classes: run the task and catch any errors."""
try:
task.run(**kwargs)
except:
error = "Task '{0}' raised an exception and had to stop:"
print error.format(task.name)
traceback.print_exc()
else:
print "Task '{0}' finished without error.".format(task.name)

def load():
"""Load all valid tasks from bot/tasks/, into the _tasks variable."""
files = os.listdir(base_dir)
files.sort()

for filename in files:
if filename.startswith("_") or not filename.endswith(".py"):
continue
try:
_load_task(filename)
except AttributeError:
pass # The file is doesn't contain a task, so just move on

print "Found {0} tasks: {1}.".format(len(_tasks), ', '.join(_tasks.keys()))

def schedule(now=time.gmtime()):
"""Start all tasks that are supposed to be run at a given time."""
# Get list of tasks to run this turn:
tasks = config.schedule(now.tm_min, now.tm_hour, now.tm_mday, now.tm_mon,
now.tm_wday)

for task in tasks:
if isinstance(task, list): # they've specified kwargs
start(task[0], **task[1]) # so pass those to start_task
else: # otherwise, just pass task_name
start(task)

def start(task_name, **kwargs):
"""Start a given task in a new thread. Pass args to the task's run()
function."""
print "Starting task '{0}' in a new thread...".format(task_name)

try:
task = _tasks[task_name]
except KeyError:
error = "Couldn't find task '{0}': bot/tasks/{0}.py does not exist."
print error.format(task_name)
return

task_thread = threading.Thread(target=lambda: _wrapper(task, **kwargs))
start_time = time.strftime("%b %d %H:%M:%S")
task_thread.name = "{0} ({1})".format(task_name, start_time)

# Stop bot task threads automagically if the main bot stops:
task_thread.daemon = True

task_thread.start()

def get_all():
"""Return our dict of all loaded tasks."""
return _tasks

wiki/tasks/afc_catdelink.py → bot/tasks/afc_catdelink.py 查看文件

@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-

from wiki.base_task import BaseTask
from classes import BaseTask

class Task(BaseTask):
"""A task to delink mainspace categories in declined [[WP:AFC]]
submissions."""
task_name = "afc_catdelink"
name = "afc_catdelink"

def __init__(self):
pass

wiki/tasks/afc_copyvios.py → bot/tasks/afc_copyvios.py 查看文件

@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-

from wiki.base_task import BaseTask
from classes import BaseTask

class Task(BaseTask):
"""A task to check newly-edited [[WP:AFC]] submissions for copyright
violations."""
task_name = "afc_copyvios"
name = "afc_copyvios"

def __init__(self):
pass

wiki/tasks/afc_dailycats.py → bot/tasks/afc_dailycats.py 查看文件

@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-

from wiki.base_task import BaseTask
from classes import BaseTask

class Task(BaseTask):
""" A task to create daily categories for [[WP:AFC]]."""
task_name = "afc_dailycats"
name = "afc_dailycats"

def __init__(self):
pass

wiki/tasks/afc_statistics.py → bot/tasks/afc_statistics.py 查看文件

@@ -1,14 +1,17 @@
# -*- coding: utf-8 -*-

from wiki.base_task import BaseTask
import time

from classes import BaseTask

class Task(BaseTask):
"""A task to generate statistics for [[WP:AFC]] and save them to
[[Template:AFC_statistics]]."""
task_name = "afc_statistics"
name = "afc_statistics"

def __init__(self):
pass

def run(self, **kwargs):
time.sleep(5)
print kwargs

wiki/tasks/afc_undated.py → bot/tasks/afc_undated.py 查看文件

@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-

from wiki.base_task import BaseTask
from classes import BaseTask

class Task(BaseTask):
"""A task to clear [[Category:Undated AfC submissions]]."""
task_name = "afc_undated"
name = "afc_undated"

def __init__(self):
pass

wiki/tasks/blptag.py → bot/tasks/blptag.py 查看文件

@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-

from wiki.base_task import BaseTask
from classes import BaseTask

class Task(BaseTask):
"""A task to add |blp=yes to {{WPB}} or {{WPBS}} when it is used along with
{{WP Biography}}."""
task_name = "blptag"
name = "blptag"

def __init__(self):
pass

wiki/tasks/feed_dailycats.py → bot/tasks/feed_dailycats.py 查看文件

@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-

from wiki.base_task import BaseTask
from classes import BaseTask

class Task(BaseTask):
"""A task to create daily categories for [[WP:FEED]]."""
task_name = "feed_dailycats"
name = "feed_dailycats"

def __init__(self):
pass

wiki/tasks/wrongmime.py → bot/tasks/wrongmime.py 查看文件

@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-

from wiki.base_task import BaseTask
from classes import BaseTask

class Task(BaseTask):
"""A task to tag files whose extensions do not agree with their MIME
type."""
task_name = "wrongmime"
name = "wrongmime"

def __init__(self):
pass

+ 91
- 0
bot/watcher.py 查看文件

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's IRC Watcher Component

The IRC watcher runs on a wiki recent-changes server and listens for edits.
Users cannot interact with this part of the bot. When an event occurs, we run
it through rules.py's process() function, which can result in wiki bot tasks
being started (located in tasks/) or messages being sent to channels on the IRC
frontend.
"""

import config
from classes import Connection, RC, BrokenSocketException
import rules

frontend_conn = None

def get_connection():
"""Return a new Connection() instance with connection information.

Don't actually connect yet.
"""
cf = config.irc["watcher"]
connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"],
cf["realname"])
return connection

def main(connection, f_conn=None):
"""Main loop for the Watcher IRC Bot component.
get_connection() should have already been called and the connection should
have been started with connection.connect(). Accept the frontend connection
as well as an optional parameter in order to send messages directly to
frontend IRC channels.
"""
global frontend_conn
frontend_conn = f_conn
read_buffer = str()

while 1:
try:
read_buffer = read_buffer + connection.get()
except BrokenSocketException:
return

lines = read_buffer.split("\n")
read_buffer = lines.pop()

for line in lines:
_process_message(connection, line)

def _process_message(connection, line):
"""Process a single message from IRC."""
line = line.strip().split()

if line[1] == "PRIVMSG":
chan = line[2]

# Ignore messages originating from channels not in our list, to prevent
# someone PMing us false data:
if chan not in config.irc["watcher"]["channels"]:
return

msg = ' '.join(line[3:])[1:]
rc = RC(msg) # new RC object to store this event's data
rc.parse() # parse a message into pagenames, usernames, etc.
process_rc(rc) # report to frontend channels or start tasks

# If we are pinged, pong back to the server:
elif line[0] == "PING":
msg = " ".join(("PONG", line[1]))
connection.send(msg)

# When we've finished starting up, join all watcher channels:
elif line[1] == "376":
for chan in config.irc["watcher"]["channels"]:
connection.join(chan)

def process_rc(rc):
"""Process a recent change event from IRC (or, an RC object).

The actual processing is configurable, so we don't have that hard-coded
here. We simply call rules's process() function and expect a list of
channels back, which we report the event data to.
"""
chans = rules.process(rc)
if chans and frontend_conn:
pretty = rc.prettify()
for chan in chans:
frontend_conn.say(chan, pretty)

+ 20
- 0
bot/wiki/__init__.py 查看文件

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's Wiki Toolset

This is a collection of classes and functions to read from and write to
Wikipedia and other wiki sites. No connection whatsoever to python-wikitools
written by Mr.Z-man, other than a similar purpose. We share no code.

Import the toolset with `import wiki`.
"""

from wiki.constants import *
from wiki.exceptions import *
from wiki.functions import *

from wiki.category import Category
from wiki.page import Page
from wiki.site import Site
from wiki.user import User

wiki/tools/category.py → bot/wiki/category.py 查看文件

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-

from wiki.tools.page import Page
from wiki.page import Page

class Category(Page):
"""
@@ -24,7 +24,7 @@ class Category(Page):
up to 500, and bots can go up to 5,000 on a single API query.
"""
params = {"action": "query", "list": "categorymembers",
"cmlimit": limit, "cmtitle": self.title}
"cmlimit": limit, "cmtitle": self._title}
result = self._site._api_query(params)
members = result['query']['categorymembers']
return [member["title"] for member in members]

wiki/tools/constants.py → bot/wiki/constants.py 查看文件

@@ -6,7 +6,7 @@ EarwigBot's Wiki Toolset: Constants
This module defines some useful constants, such as default namespace IDs for
easy lookup and our user agent.

Import with `from wiki.tools.constants import *`.
Import with `from wiki.constants import *`.
"""

import platform

wiki/tools/exceptions.py → bot/wiki/exceptions.py 查看文件

@@ -3,7 +3,7 @@
"""
EarwigBot's Wiki Toolset: Exceptions

This module contains all exceptions used by the wiki.tools package.
This module contains all exceptions used by the wiki package.
"""

class WikiToolsetError(Exception):

wiki/tools/functions.py → bot/wiki/functions.py 查看文件

@@ -3,11 +3,11 @@
"""
EarwigBot's Wiki Toolset: Misc Functions

This module, a component of the wiki.tools package, contains miscellaneous
functions that are not methods of any class, like get_site().
This module, a component of the wiki package, contains miscellaneous functions
that are not methods of any class, like get_site().

There's no need to import this module explicitly. All functions here are
automatically available from wiki.tools.
automatically available from wiki.
"""

from cookielib import LWPCookieJar, LoadError
@@ -16,9 +16,9 @@ from getpass import getpass
from os import chmod, path
import stat

from core import config
from wiki.tools.exceptions import SiteNotFoundError
from wiki.tools.site import Site
import config
from wiki.exceptions import SiteNotFoundError
from wiki.site import Site

__all__ = ["get_site"]

@@ -84,10 +84,18 @@ def _get_site_object_from_dict(name, d):
article_path = d.get("articlePath")
script_path = d.get("scriptPath")
sql = (d.get("sqlServer"), d.get("sqlDB"))
namespaces = d.get("namespaces")
namespaces = d.get("namespaces", {})
login = (config.wiki.get("username"), config.wiki.get("password"))
cookiejar = _get_cookiejar()

for key, value in namespaces.items(): # Convert string keys to integers
del namespaces[key]
try:
namespaces[int(key)] = value
except ValueError: # Data is broken, ignore it
namespaces = None
break

return Site(name=name, project=project, lang=lang, base_url=base_url,
article_path=article_path, script_path=script_path, sql=sql,
namespaces=namespaces, login=login, cookiejar=cookiejar)

wiki/tools/page.py → bot/wiki/page.py 查看文件

@@ -3,7 +3,7 @@
import re
from urllib import quote

from wiki.tools.exceptions import *
from wiki.exceptions import *

class Page(object):
"""
@@ -20,6 +20,7 @@ class Page(object):
url -- returns the page's URL
namespace -- returns the page's namespace as an integer
protection -- returns the page's current protection status
creator -- returns the page's creator (first user to edit)
is_talkpage -- returns True if the page is a talkpage, else False
is_redirect -- returns True if the page is a redirect, else False
toggle_talk -- returns a content page's talk page, or vice versa
@@ -51,6 +52,7 @@ class Page(object):
self._protection = None
self._fullurl = None
self._content = None
self._creator = None

# Try to determine the page's namespace using our site's namespace
# converter:
@@ -122,15 +124,17 @@ class Page(object):
"""Loads various data from the API in a single query.

Loads self._title, ._exists, ._is_redirect, ._pageid, ._fullurl,
._protection, ._namespace, ._is_talkpage, and ._lastrevid using the
API. It will do a query of its own unless `result` is provided, in
which case we'll pretend `result` is what the query returned.
._protection, ._namespace, ._is_talkpage, ._creator, and ._lastrevid
using the API. It will do a query of its own unless `result` is
provided, in which case we'll pretend `result` is what the query
returned.

Assuming the API is sound, this should not raise any exceptions.
"""
if result is None:
params = {"action": "query", "prop": "info", "titles": self._title,
"inprop": "protection|url"}
params = {"action": "query", "rvprop": "user", "rvdir": "newer",
"prop": "info|revisions", "rvlimit": 1,
"titles": self._title, "inprop": "protection|url"}
result = self._site._api_query(params)

res = result["query"]["pages"].values()[0]
@@ -169,9 +173,10 @@ class Page(object):
self._namespace = res["ns"]
self._is_talkpage = self._namespace % 2 == 1 # talkpages have odd IDs

# This last field will only be specified if the page exists:
# These last two fields will only be specified if the page exists:
self._lastrevid = res.get("lastrevid")
try:
self._lastrevid = res["lastrevid"]
self._creator = res['revisions'][0]['user']
except KeyError:
pass

@@ -287,6 +292,27 @@ class Page(object):
self._force_validity() # invalid pages cannot be protected
return self._protection

def creator(self, force=False):
"""Returns the page's creator (i.e., the first user to edit the page).

Makes an API query if force is True or if we haven't already made one.
Normally, we can get the creator along with everything else (except
content) in self._load_attributes(). However, due to a limitation in
the API (can't get the editor of one revision and the content of
another at both ends of the history), if our other attributes were only
loaded from get(), we'll have to do another API query. This is done
by calling ourselves again with force=True.

Raises InvalidPageError or PageNotFoundError if the page name is
invalid or the page does not exist, respectively.
"""
if self._exists == 0 or force:
self._load_wrapper()
self._force_existence()
if not self._creator and not force:
self.creator(force=True)
return self._creator

def is_talkpage(self, force=False):
"""Returns True if the page is a talkpage, else False.


wiki/tools/site.py → bot/wiki/site.py 查看文件

@@ -9,11 +9,11 @@ from urllib import unquote_plus, urlencode
from urllib2 import build_opener, HTTPCookieProcessor, URLError
from urlparse import urlparse

from wiki.tools.category import Category
from wiki.tools.constants import *
from wiki.tools.exceptions import *
from wiki.tools.page import Page
from wiki.tools.user import User
from wiki.category import Category
from wiki.constants import *
from wiki.exceptions import *
from wiki.page import Page
from wiki.user import User

class Site(object):
"""
@@ -96,8 +96,9 @@ class Site(object):

We'll encode the given params, adding format=json along the way, and
make the request through self._opener, which has built-in cookie
support via self._cookiejar, a User-Agent
(wiki.tools.constants.USER_AGENT), and Accept-Encoding set to "gzip".
support via self._cookiejar, a User-Agent (wiki.constants.USER_AGENT),
and Accept-Encoding set to "gzip".
Assuming everything went well, we'll gunzip the data (if compressed),
load it as a JSON object, and return it.

@@ -153,7 +154,7 @@ class Site(object):

params = {"action": "query", "meta": "siteinfo"}

if self._namespaces is None or force:
if not self._namespaces or force:
params["siprop"] = "general|namespaces|namespacealiases"
result = self._api_query(params)
self._load_namespaces(result)

wiki/tools/user.py → bot/wiki/user.py 查看文件

@@ -2,9 +2,9 @@

from time import strptime

from wiki.tools.constants import *
from wiki.tools.exceptions import UserNotFoundError
from wiki.tools.page import Page
from wiki.constants import *
from wiki.exceptions import UserNotFoundError
from wiki.page import Page

class User(object):
"""
@@ -94,7 +94,10 @@ class User(object):
self._blockinfo = False

self._groups = res["groups"]
self._rights = res["rights"].values()
try:
self._rights = res["rights"].values()
except AttributeError:
self._rights = res["rights"]
self._editcount = res["editcount"]

reg = res["registration"]

+ 3
- 3
earwigbot.py 查看文件

@@ -20,7 +20,7 @@ from os import path
from sys import executable
from time import sleep

from core.config import verify_config
from bot import config

__author__ = "Ben Kurtovic"
__copyright__ = "Copyright (C) 2009, 2010, 2011 by Ben Kurtovic"
@@ -28,12 +28,12 @@ __license__ = "MIT License"
__version__ = "0.1-dev"
__email__ = "ben.kurtovic@verizon.net"

bot_script = path.join(path.dirname(path.abspath(__file__)), "core", "main.py")
bot_script = path.join(path.dirname(path.abspath(__file__)), "bot", "main.py")

def main():
print "EarwigBot v{0}\n".format(__version__)

is_encrypted = verify_config()
is_encrypted = config.verify_config()
if is_encrypted: # passwords in the config file are encrypted
key = getpass("Enter key to unencrypt bot passwords: ")
else:


+ 0
- 0
查看文件


+ 0
- 33
irc/classes/base_command.py 查看文件

@@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-

# A base class for commands on IRC.

class BaseCommand(object):
def __init__(self, connection):
"""A base class for commands on IRC."""
self.connection = connection

def get_hooks(self):
"""Hooks are: 'msg', 'msg_private', 'msg_public', and 'join'. Return
the hooks you want this command to be called on."""
return []

def get_help(self, command):
"""Return help information for the command, used by !help. return None
for no help. If a given class handles multiple commands, the command
variable can be used to return different help for each one."""
return None

def check(self, data):
"""Given a Data() object, return True if we should respond to this
activity, or False if we should ignore it/it doesn't apply to us. Most
commands return True if data.command == 'command_name', otherwise
they return False."""
return False

def process(self, data):
"""Handle an activity (usually a message) on IRC. At this point, thanks
to self.check() which is called automatically by command_handler, we
know this is something we should respond to, so (usually) a
'if data.command != "command_name": return' is unnecessary."""
pass

+ 0
- 75
irc/classes/connection.py 查看文件

@@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-

# A class to interface with IRC.

import socket
import threading

class BrokenSocketException(Exception):
"""A socket has broken, because it is not sending data."""
pass

class Connection(object):
def __init__(self, host=None, port=None, nick=None, ident=None, realname=None):
"""a class to interface with IRC"""
self.host = host
self.port = port
self.nick = nick
self.ident = ident
self.realname = realname

def connect(self):
"""connect to IRC"""
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
self.send("NICK %s" % self.nick)
self.send("USER %s %s * :%s" % (self.ident, self.host, self.realname))

def close(self):
"""close our connection with IRC"""
try:
self.sock.shutdown(socket.SHUT_RDWR) # shut down connection first
except socket.error:
pass # ignore if the socket is already down
self.sock.close()

def get(self, size=4096):
"""receive (get) data from the server"""
data = self.sock.recv(4096)
if not data: # socket giving us no data, so it is dead/broken
raise BrokenSocketException()
return data

def send(self, msg):
"""send data to the server"""
lock = threading.Lock()
lock.acquire() # ensure that we only send one message at a time (blocking)
try:
self.sock.sendall(msg + "\r\n")
print " %s" % msg
finally:
lock.release()

def say(self, target, msg):
"""send a message"""
self.send("PRIVMSG %s :%s" % (target, msg))

def reply(self, data, msg):
"""send a message as a reply"""
self.say(data.chan, "%s%s%s: %s" % (chr(2), data.nick, chr(0x0f), msg))

def action(self, target, msg):
"""send a message as an action"""
self.say(target,"%sACTION %s%s" % (chr(1), msg, chr(1)))

def notice(self, target, msg):
"""send a notice"""
self.send("NOTICE %s :%s" % (target, msg))

def join(self, chan):
"""join a channel"""
self.send("JOIN %s" % chan)

def mode(self, chan, level, msg):
"""send a mode message"""
self.send("MODE %s %s %s" % (chan, level, msg))

+ 0
- 55
irc/classes/data.py 查看文件

@@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-

# A class to store data from an individual line received on IRC.

import re

class KwargParseException(Exception):
"""Couldn't parse a certain keyword argument in self.args, probably because
it was given incorrectly: e.g., no value (abc), just a value (=xyz), just
an equal sign (=), instead of the correct (abc=xyz)."""
pass

class Data(object):
def __init__(self):
"""store data from an individual line received on IRC"""
self.line = str()
self.chan = str()
self.nick = str()
self.ident = str()
self.host = str()
self.msg = str()

def parse_args(self):
"""parse command arguments from self.msg into self.command and self.args"""
args = self.msg.strip().split(' ') # strip out extra whitespace and split the message into a list
while '' in args: # remove any empty arguments
args.remove('')

self.args = args[1:] # the command arguments
self.is_command = False # whether this is a real command or not

try:
self.command = args[0] # the command itself
except IndexError:
self.command = None

try:
if self.command.startswith('!') or self.command.startswith('.'):
self.is_command = True
self.command = self.command[1:] # strip '!' or '.'
self.command = self.command.lower() # lowercase command name
except AttributeError:
pass

def parse_kwargs(self):
"""parse command arguments from self.args, given as !command key1=value1 key2=value2..., into a dict self.kwargs: {'key1': 'value2', 'key2': 'value2'...}"""
self.kwargs = {}
for arg in self.args[2:]:
try:
key, value = re.findall("^(.*?)\=(.*?)$", arg)[0]
except IndexError:
raise KwargParseException(arg)
if not key or not value:
raise KwargParseException(arg)
self.kwargs[key] = value

+ 0
- 57
irc/classes/rc.py 查看文件

@@ -1,57 +0,0 @@
# -*- coding: utf-8 -*-

# A class to store data on an individual event received from our IRC watcher.

import re

class RC(object):
def __init__(self, msg):
"""store data on an individual event received from our IRC watcher"""
self.msg = msg

def parse(self):
"""parse recent changes log into some variables"""
msg = self.msg
msg = re.sub("\x03([0-9]{1,2}(,[0-9]{1,2})?)?", "", msg) # strip IRC color codes; we don't want/need 'em
msg = msg.strip()
self.msg = msg
self.is_edit = True

# flags: 'M' for minor edit, 'B' for bot edit, 'create' for a user creation log entry...
try:
page, flags, url, user, comment = re.findall("\A\[\[(.*?)\]\]\s(.*?)\s(http://.*?)\s\*\s(.*?)\s\*\s(.*?)\Z", msg)[0]
except IndexError: # we're probably missing the http:// part, because it's a log entry, which lacks a url
page, flags, user, comment = re.findall("\A\[\[(.*?)\]\]\s(.*?)\s\*\s(.*?)\s\*\s(.*?)\Z", msg)[0]
url = "http://en.wikipedia.org/wiki/{}".format(page)
flags = flags.strip() # flag tends to have a extraneous whitespace character at the end when it's a log entry
self.is_edit = False # this is a log entry, not edit
self.page, self.flags, self.url, self.user, self.comment = page, flags, url, user, comment

def get_pretty(self):
"""make a nice, colorful message from self.msg to send to the front-end"""
flags = self.flags
event_type = flags # "New <event>:" if we don't know exactly what happened
if "N" in flags:
event_type = "page" # "New page:"
elif flags == "delete":
event_type = "deletion" # "New deletion:"
elif flags == "protect":
event_type = "protection" # "New protection:"
elif flags == "create":
event_type = "user" # "New user:"
if self.page == "Special:Log/move":
event_type = "move" # New move:
else:
event_type = "edit" # "New edit:"
if "B" in flags:
event_type = "bot {}".format(event_type) # "New bot edit:"
if "M" in flags:
event_type = "minor {}".format(event_type) # "New minor edit:" OR "New minor bot edit:"
if self.is_edit:
pretty = "\x02New {0}\x0F: \x0314[[\x0307{1}\x0314]]\x0306 *\x0303 {2}\x0306 *\x0302 {3}\x0306 *\x0310 {4}".format(event_type, self.page, self.user, self.url, self.comment)
else:
pretty = "\x02New {0}\x0F: \x0303{1}\x0306 *\x0302 {2}\x0306 *\x0310 {3}".format(event_type, self.user, self.url, self.comment)
return pretty

+ 0
- 66
irc/command_handler.py 查看文件

@@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-

# A module to manage IRC commands.

import os
import traceback

commands = []

def load_commands(connection):
"""load all valid command classes from irc/commmands/ into the commands variable"""
files = os.listdir(os.path.join("irc", "commands")) # get all files in irc/commands/
files.sort() # alphabetically sort list of files

for f in files:
if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_"
continue
module = f[:-3] # strip .py from end
try:
exec "from irc.commands import %s" % module
except: # importing the file failed for some reason...
print "Couldn't load file %s:" % f
traceback.print_exc()
continue
process_module(connection, eval(module)) # 'module' is a string, so get the actual object for processing by eval-ing it

pretty_cmnds = map(lambda c: c.__class__.__name__, commands)
print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds))

def process_module(connection, module):
"""go through all objects in a module and add valid command classes to the commands variable"""
global commands
objects = dir(module)

for this_obj in objects: # go through everything in the file
obj = eval("module.%s" % this_obj) # this_obj is a string, so get the actual object corresponding to that string

try:
bases = obj.__bases__
except AttributeError: # object isn't a valid class, so ignore it
continue

for base in bases:
if base.__name__ == "BaseCommand": # this inherits BaseCommand, so it must be a command class
command = obj(connection) # initialize a new command object
commands.append(command)
print "Added command class %s from %s..." % (this_obj, module.__name__)
continue

def get_commands():
"""get our commands"""
return commands

def check(hook, data):
"""given an event on IRC, check if there's anything we can respond to by calling each command class"""
data.parse_args() # parse command arguments into data.command and data.args

for command in commands:
if hook in command.get_hooks():
if command.check(data):
try:
command.process(data)
except:
print "Error executing command '{}':".format(data.command)
traceback.print_exc() # catch exceptions and print them
break

+ 0
- 0
查看文件


+ 0
- 104
irc/commands/afc_report.py 查看文件

@@ -1,104 +0,0 @@
# -*- coding: utf-8 -*-

"""
Get information about an AFC submission by name.
"""

import json
import re
import urllib

from irc.classes import BaseCommand

class AFCReport(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
return "Get information about an AFC submission by name."

def check(self, data):
if data.is_command and data.command in ["report", "afc_report"]:
return True
return False

def process(self, data):
self.data = data
if not data.args:
self.connection.reply(data, "what submission do you want me to give information about?")
return

pagename = ' '.join(data.args)
pagename = pagename.replace("http://en.wikipedia.org/wiki/", "").replace("http://enwp.org/", "").replace("_", " ")
pagename = pagename.strip()

if self.page_exists(pagename): # given '!report Foo', first try [[Foo]]
self.report(pagename)
else: # if that doesn't work, try [[Wikipedia:Articles for creation/Foo]]
if self.page_exists("Wikipedia:Articles for creation/" + pagename):
self.report("Wikipedia:Articles for creation/" + pagename)
else: # if that doesn't work, try [[Wikipedia talk:Articles for creation/Foo]]
if self.page_exists("Wikipedia talk:Articles for creation/" + pagename):
self.report("Wikipedia talk:Articles for creation/" + pagename)
else:
self.connection.reply(data, "submission \x0302{0}\x0301 not found.".format(pagename))

def report(self, pagename):
data = self.data
shortname = pagename.replace("Wikipedia:Articles for creation/", "").replace("Wikipedia talk:Articles for creation/", "")
url = "http://enwp.org/" + urllib.quote(pagename.replace(" ", "_"))
status = self.get_status(pagename)
user, user_url = self.get_creator(pagename)

self.connection.reply(data, "AfC submission report for \x0302{0}\x0301 ({1}):".format(shortname, url))
self.connection.say(data.chan, "Status: \x0303{0}\x0301".format(status))
if status == "accepted": # the first edit will be the redirect [[WT:AFC/Foo]] -> [[Foo]], NOT the creation of the submission
self.connection.say(data.chan, "Reviewed by \x0302{0}\x0301 ({1})".format(user, user_url))
else:
self.connection.say(data.chan, "Submitted by \x0302{0}\x0301 ({1})".format(user, user_url))

def page_exists(self, pagename):
params = {'action': 'query', 'format': 'json', 'titles': pagename}
data = urllib.urlencode(params)
raw = urllib.urlopen("http://en.wikipedia.org/w/api.php", data).read()
res = json.loads(raw)
try:
res['query']['pages'].values()[0]['missing'] # this key will appear if the page does not exist
return False
except KeyError: # if it's not there, the page exists
return True

def get_status(self, pagename):
params = {'action': 'query', 'prop': 'revisions', 'rvprop':'content', 'rvlimit':'1', 'format': 'json'}
params['titles'] = pagename
data = urllib.urlencode(params)
raw = urllib.urlopen("http://en.wikipedia.org/w/api.php", data).read()
res = json.loads(raw)
pageid = res['query']['pages'].keys()[0]
content = res['query']['pages'][pageid]['revisions'][0]['*']
lcontent = content.lower()
if re.search("\{\{afc submission\|r\|(.*?)\}\}", lcontent):
return "being reviewed"
elif re.search("\{\{afc submission\|\|(.*?)\}\}", lcontent):
return "pending"
elif re.search("\{\{afc submission\|d\|(.*?)\}\}", lcontent):
try:
reason = re.findall("\{\{afc submission\|d\|(.*?)(\||\}\})", lcontent)[0][0]
return "declined with reason \"{0}\"".format(reason)
except IndexError:
return "declined"
else:
if "#redirect" in content:
return "accepted"
else:
return "unkown"

def get_creator(self, pagename):
params = {'action': 'query', 'prop': 'revisions', 'rvprop': 'user', 'rvdir': 'newer', 'rvlimit': '1', 'format': 'json'}
params['titles'] = pagename
data = urllib.urlencode(params)
raw = urllib.urlopen("http://en.wikipedia.org/w/api.php", data).read()
res = json.loads(raw)
user = res['query']['pages'].values()[0]['revisions'][0]['user']
user_url = "http://enwp.org/User_talk:" + urllib.quote(user.replace(" ", "_"))
return user, user_url

+ 0
- 31
irc/commands/chanops.py 查看文件

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-

# Voice/devoice/op/deop users in the channel.

from irc.classes import BaseCommand
from core import config

class ChanOps(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
action = command.capitalize()
return "%s users in the channel." % action

def check(self, data):
if data.is_command and data.command in ["voice", "devoice", "op", "deop"]:
return True
return False

def process(self, data):
if data.host not in config.irc["permissions"]["admins"]:
self.connection.reply(data, "you must be a bot admin to use this command.")
return

if not data.args: # if it is just !op/!devoice/whatever without arguments, assume they want to do this to themselves
target = data.nick
else:
target = data.args[0]

self.connection.say("ChanServ", "%s %s %s" % (data.command, data.chan, target))

+ 0
- 71
irc/commands/crypt.py 查看文件

@@ -1,71 +0,0 @@
# -*- coding: utf-8 -*-

"""
Cryptography functions (hashing and cyphers) for EarwigBot IRC.
"""

import hashlib

from irc.classes import BaseCommand
from lib import blowfish

class Cryptography(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
if command == "hash":
return ("Return the hash of a string using a given algorithm, " +
"e.g. '!hash sha512 Hello world!'. Use '!hash list' for " +
"a list of supported algorithms.")
elif command == "encrypt":
return ("Encrypt any string with a given key using an " +
"implementation of Blowfish, e.g. '!encrypt some_key " +
"Hello!'.")
else:
return ("Decrypt any string with a given key using an " +
"implementation of Blowfish, e.g. '!decrypt some_key " +
"762cee8a5239548af18275d6c1184f16'.")

def check(self, data):
if data.is_command and data.command in ["hash", "encrypt", "decrypt"]:
return True
return False

def process(self, data):
if not data.args:
self.connection.reply(data, "what do you want me to {0}?".format(
data.command))
return

if data.command == "hash":
algo = data.args[0]
if algo == "list":
algos = ', '.join(hashlib.algorithms)
self.connection.reply(data, "supported algorithms: " + algos +
".")
elif algo in hashlib.algorithms:
string = ' '.join(data.args[1:])
result = eval("hashlib.{0}(string)".format(algo)).hexdigest()
self.connection.reply(data, result)
else:
self.connection.reply(data, "unknown algorithm: '{0}'.".format(
algo))

else:
key = data.args[0]
text = ' '.join(data.args[1:])

if not text:
self.connection.reply(data, ("a key was provided, but text " +
"to {0} was not.").format(data.command))
return

try:
if data.command == "encrypt":
self.connection.reply(data, blowfish.encrypt(key, text))
else:
self.connection.reply(data, blowfish.decrypt(key, text))
except blowfish.BlowfishError as error:
self.connection.reply(data, "{0}: {1}.".format(
error.__class__.__name__, error))

+ 0
- 163
irc/commands/git.py 查看文件

@@ -1,163 +0,0 @@
# -*- coding: utf-8 -*-

# Commands to interface with the bot's git repository; use '!git help' for sub-command list.

import shlex
import subprocess
import re

from irc.classes import BaseCommand
from core import config

class Git(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
return "Commands to interface with the bot's git repository; use '!git help' for sub-command list."

def check(self, data):
if data.is_command and data.command == "git":
return True
return False

def process(self, data):
self.data = data
if data.host not in config.irc["permissions"]["owners"]:
self.connection.reply(data, "you must be a bot owner to use this command.")
return

if not data.args:
self.connection.reply(data, "no arguments provided. Maybe you wanted '!git help'?")
return

if data.args[0] == "help":
self.do_help()

elif data.args[0] == "branch":
self.do_branch()

elif data.args[0] == "branches":
self.do_branches()

elif data.args[0] == "checkout":
self.do_checkout()

elif data.args[0] == "delete":
self.do_delete()

elif data.args[0] == "pull":
self.do_pull()

elif data.args[0] == "status":
self.do_status()

else: # they asked us to do something we don't know
self.connection.reply(data, "unknown argument: \x0303%s\x0301." % data.args[0])

def exec_shell(self, command):
"""execute a shell command and get the output"""
command = shlex.split(command)
result = subprocess.check_output(command, stderr=subprocess.STDOUT)
if result:
result = result[:-1] # strip newline
return result

def do_help(self):
"""display all commands"""
help_dict = {
"branch": "get current branch",
"branches": "get all branches",
"checkout": "switch branches",
"delete": "delete an old branch",
"pull": "update everything from the remote server",
"status": "check if we are up-to-date",
}
keys = help_dict.keys()
keys.sort()
help = ""
for key in keys:
help += "\x0303%s\x0301 (%s), " % (key, help_dict[key])
help = help[:-2] # trim last comma and space
self.connection.reply(self.data, "sub-commands are: %s." % help)

def do_branch(self):
"""get our current branch"""
branch = self.exec_shell("git name-rev --name-only HEAD")
self.connection.reply(self.data, "currently on branch \x0302%s\x0301." % branch)

def do_branches(self):
"""get list of branches"""
branches = self.exec_shell("git branch")
branches = branches.replace('\n* ', ', ') # cleanup extraneous characters
branches = branches.replace('* ', ' ')
branches = branches.replace('\n ', ', ')
branches = branches.strip()
self.connection.reply(self.data, "branches: \x0302%s\x0301." % branches)

def do_checkout(self):
"""switch branches"""
try:
branch = self.data.args[1]
except IndexError: # no branch name provided
self.connection.reply(self.data, "switch to which branch?")
return

current_branch = self.exec_shell("git name-rev --name-only HEAD")

try:
result = self.exec_shell("git checkout %s" % branch)
if "Already on" in result:
self.connection.reply(self.data, "already on \x0302%s\x0301!" % branch)
else:
self.connection.reply(self.data, "switched from branch \x0302%s\x0301 to \x0302%s\x0301." % (current_branch, branch))

except subprocess.CalledProcessError: # git couldn't switch branches
self.connection.reply(self.data, "branch \x0302%s\x0301 doesn't exist!" % branch)

def do_delete(self):
"""delete a branch, while making sure that we are not on it"""
try:
delete_branch = self.data.args[1]
except IndexError: # no branch name provided
self.connection.reply(self.data, "delete which branch?")
return

current_branch = self.exec_shell("git name-rev --name-only HEAD")

if current_branch == delete_branch:
self.connection.reply(self.data, "you're currently on this branch; please checkout to a different branch before deleting.")
return

try:
self.exec_shell("git branch -d %s" % delete_branch)
self.connection.reply(self.data, "branch \x0302%s\x0301 has been deleted locally." % delete_branch)
except subprocess.CalledProcessError: # git couldn't delete
self.connection.reply(self.data, "branch \x0302%s\x0301 doesn't exist!" % delete_branch)

def do_pull(self):
"""pull from remote repository"""
branch = self.exec_shell("git name-rev --name-only HEAD")
self.connection.reply(self.data, "pulling from remote (currently on \x0302%s\x0301)..." % branch)

result = self.exec_shell("git pull")

if "Already up-to-date." in result:
self.connection.reply(self.data, "done; no new changes.")
else:
changes = re.findall("\s*((.*?)\sfile(.*?)tions?\(-\))", result)[0][0] # find the changes
try:
remote = self.exec_shell("git config --get branch.%s.remote" % branch)
url = self.exec_shell("git config --get remote.%s.url" % remote)
self.connection.reply(self.data, "done; %s [from %s]." % (changes, url))
except subprocess.CalledProcessError: # something in .git/config is not specified correctly, so we cannot get the remote's url
self.connection.reply(self.data, "done; %s." % changes)

def do_status(self):
"""check whether we have anything to pull"""
last = self.exec_shell("git log -n 1 --pretty=\"%ar\"")
result = self.exec_shell("git fetch --dry-run")
if not result: # nothing was fetched, so remote and local are equal
self.connection.reply(self.data, "last commit was %s. Local copy is \x02up-to-date\x0F with remote." % last)
else:
self.connection.reply(self.data, "last local commit was %s. Remote is \x02ahead\x0F of local copy." % last)

+ 0
- 54
irc/commands/help.py 查看文件

@@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-

# Generates help information.

from irc.classes import BaseCommand, Data
from irc import command_handler

class Help(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
return "Generates help information."

def check(self, data):
if data.is_command and data.command == "help":
return True
return False

def process(self, data):
if not data.args:
self.do_general_help(data)
else:
if data.args[0] == "list":
self.do_list_help(data)
else:
self.do_command_help(data)

def do_general_help(self, data):
self.connection.reply(data, "I am a bot! You can get help for any command with '!help <command>', or a list of all loaded modules with '!help list'.")

def do_list_help(self, data):
commands = command_handler.get_commands()
cmnds = map(lambda c: c.__class__.__name__, commands)
pretty_cmnds = ', '.join(cmnds)
self.connection.reply(data, "%s command classes loaded: %s." % (len(cmnds), pretty_cmnds))

def do_command_help(self, data):
command = data.args[0]
commands = command_handler.get_commands()

dummy = Data() # dummy message to test which command classes pick up this command
dummy.command = command.lower() # lowercase command name
dummy.is_command = True

for cmnd in commands:
if cmnd.check(dummy):
help = cmnd.get_help(command)
break

try:
self.connection.reply(data, "info for command \x0303%s\x0301: \"%s\"" % (command, help))
except UnboundLocalError:
self.connection.reply(data, "sorry, no help for \x0303%s\x0301." % command)

+ 0
- 50
irc/commands/remind.py 查看文件

@@ -1,50 +0,0 @@
# -*- coding: utf-8 -*-

"""
Set a message to be repeated to you in a certain amount of time.
"""

import threading
import time

from irc.classes import BaseCommand

class Remind(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
return "Set a message to be repeated to you in a certain amount of time."

def check(self, data):
if data.is_command and data.command in ["remind", "reminder"]:
return True
return False

def process(self, data):
if not data.args:
self.connection.reply(data, "please specify a time (in seconds) and a message in the following format: !remind <time> <msg>.")
return

try:
wait = int(data.args[0])
except ValueError:
self.connection.reply(data, "the time must be given as an integer, in seconds.")
return
message = ' '.join(data.args[1:])
if not message:
self.connection.reply(data, "what message do you want me to give you when time is up?")
return

end_time = time.strftime("%b %d %H:%M:%S", time.localtime(time.time() + wait))
end_time_with_timezone = time.strftime("%b %d %H:%M:%S %Z", time.localtime(time.time() + wait))
self.connection.reply(data, 'Set reminder for "{0}" in {1} seconds (ends {2}).'.format(message, wait, end_time_with_timezone))

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

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

+ 0
- 38
irc/commands/rights.py 查看文件

@@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-

"""
Retrieve a list of user rights for a given username via the API.
"""

from irc.classes import BaseCommand
from wiki import tools

class Rights(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
return "Retrieve a list of rights for a given username."

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

def process(self, data):
if not data.args:
self.connection.reply(data, "what user do you want me to look up?")
return

username = ' '.join(data.args)
site = tools.get_site()
user = site.get_user(username)
rights = user.groups()
if rights:
try:
rights.remove("*") # remove the implicit '*' group given to everyone
except ValueError:
pass
self.connection.reply(data, "the rights for \x0302{0}\x0301 are {1}.".format(username, ', '.join(rights)))
else:
self.connection.reply(data, "the user \x0302{0}\x0301 has no rights, or does not exist.".format(username))

+ 0
- 133
irc/commands/tasks.py 查看文件

@@ -1,133 +0,0 @@
# -*- coding: utf-8 -*-

# Manage wiki tasks from IRC, and check on thread status.

import threading
import re

from irc.classes import BaseCommand, Data, KwargParseException
from wiki import task_manager
from core import config

class Tasks(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
return "Manage wiki tasks from IRC, and check on thread status."

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

def process(self, data):
self.data = data
if data.host not in config.irc["permissions"]["owners"]:
self.connection.reply(data, "at this time, you must be a bot owner to use this command.")
return

if not data.args:
if data.command == "tasklist":
self.do_list()
else:
self.connection.reply(data, "no arguments provided. Maybe you wanted '!{cmnd} list', '!{cmnd} start', or '!{cmnd} listall'?".format(cmnd=data.command))
return

if data.args[0] == "list":
self.do_list()

elif data.args[0] == "start":
self.do_start()

elif data.args[0] in ["listall", "all"]:
self.do_listall()

else: # they asked us to do something we don't know
self.connection.reply(data, "unknown argument: \x0303{0}\x0301.".format(data.args[0]))

def do_list(self):
"""With !tasks list (or abbreviation !tasklist), list all running
threads. This includes the main threads, like the irc frontend and the
watcher, and task threads."""
threads = threading.enumerate()

normal_threads = []
task_threads = []

for thread in threads:
tname = thread.name
if tname == "MainThread":
tname = self.get_main_thread_name()
normal_threads.append("\x0302{0}\x0301 (as main thread, id {1})".format(tname, thread.ident))
elif tname in ["irc-frontend", "irc-watcher", "wiki-scheduler"]:
normal_threads.append("\x0302{0}\x0301 (id {1})".format(tname, thread.ident))
elif tname.startswith("reminder"):
normal_threads.append("\x0302reminder\x0301 (until {0})".format(tname.replace("reminder ", "")))
else:
tname, start_time = re.findall("^(.*?) \((.*?)\)$", tname)[0]
task_threads.append("\x0302{0}\x0301 (id {1}, since {2})".format(tname, thread.ident, start_time))

if task_threads:
msg = "\x02{0}\x0F threads active: {1}, and \x02{2}\x0F task threads: {3}.".format(len(threads), ', '.join(normal_threads), len(task_threads), ', '.join(task_threads))
else:
msg = "\x02{0}\x0F threads active: {1}, and \x020\x0F task threads.".format(len(threads), ', '.join(normal_threads))
self.connection.reply(self.data, msg)

def do_listall(self):
"""With !tasks listall or !tasks all, list all loaded tasks, and report
whether they are currently running or idle."""
tasks = task_manager.task_list.keys()
threads = threading.enumerate()
tasklist = []

tasks.sort()

for task in tasks:
threads_running_task = [t for t in threads if t.name.startswith(task)]
ids = map(lambda t: str(t.ident), threads_running_task)
if not ids:
tasklist.append("\x0302{0}\x0301 (idle)".format(task))
elif len(ids) == 1:
tasklist.append("\x0302{0}\x0301 (\x02active\x0F as id {1})".format(task, ids[0]))
else:
tasklist.append("\x0302{0}\x0301 (\x02active\x0F as ids {1})".format(task, ', '.join(ids)))

tasklist = ", ".join(tasklist)

msg = "{0} tasks loaded: {1}.".format(len(tasks), tasklist)
self.connection.reply(self.data, msg)

def do_start(self):
"""With !tasks start, start any loaded task by name with or without
kwargs."""
data = self.data

try:
task_name = data.args[1]
except IndexError: # no task name given
self.connection.reply(data, "what task do you want me to start?")
return

try:
data.parse_kwargs()
except KwargParseException, arg:
self.connection.reply(data, "error parsing argument: \x0303{0}\x0301.".format(arg))
return

if task_name not in task_manager.task_list.keys(): # this task does not exist or hasn't been loaded
self.connection.reply(data, "task could not be found; either wiki/tasks/{0}.py doesn't exist, or it wasn't loaded correctly.".format(task_name))
return

task_manager.start_task(task_name, **data.kwargs)
self.connection.reply(data, "task \x0302{0}\x0301 started.".format(task_name))

def get_main_thread_name(self):
"""Return the "proper" name of the MainThread; e.g. "irc-frontend" or
"irc-watcher"."""
if "irc_frontend" in config.components:
return "irc-frontend"
elif "wiki_schedule" in config.components:
return "wiki-scheduler"
else:
return "irc-watcher"

+ 0
- 26
irc/commands/test.py 查看文件

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-

# A very simple command to test the bot.

import random

from irc.classes import BaseCommand

class Test(BaseCommand):
def get_hooks(self):
return ["msg"]

def get_help(self, command):
return "Test the bot!"

def check(self, data):
if data.is_command and data.command == "test":
return True
return False

def process(self, data):
hey = random.randint(0, 1)
if hey:
self.connection.say(data.chan, "Hey \x02%s\x0F!" % data.nick)
else:
self.connection.say(data.chan, "'sup \x02%s\x0F?" % data.nick)

+ 0
- 104
irc/frontend.py 查看文件

@@ -1,104 +0,0 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's IRC Front-end Component

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 BaseCommand in irc/base_command.py. All command classes are automatically
imported by irc/command_handler.py if they are in irc/commands.
"""

from re import findall

from core import config
from irc import command_handler
from irc.classes import Connection, Data, BrokenSocketException

connection = None

def get_connection():
"""Return a new Connection() instance with information about our server
connection, but don't actually connect yet."""
cf = config.irc["frontend"]
connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"],
cf["realname"])
return connection

def startup(conn):
"""Accept a single arg, a Connection() object, and set our global variable
'connection' to it. Load all command classes in irc/commands with
command_handler, and then establish a connection with the IRC server."""
global connection
connection = conn
command_handler.load_commands(connection)
connection.connect()

def main():
"""Main loop for the Frontend IRC Bot component. get_connection() and
startup() should have already been called."""
read_buffer = str()

while 1:
try:
read_buffer = read_buffer + connection.get()
except BrokenSocketException:
print "Socket has broken on front-end; restarting bot..."
return

lines = read_buffer.split("\n")
read_buffer = lines.pop()

for line in lines: # handle a single message from IRC
line = line.strip().split()
data = Data() # new Data() instance to store info about this line
data.line = line

if line[1] == "JOIN":
data.nick, data.ident, data.host = findall(
":(.*?)!(.*?)@(.*?)\Z", line[0])[0]
data.chan = line[2][1:]
command_handler.check("join", data) # check for 'join' hooks in
# our commands

if line[1] == "PRIVMSG":
data.nick, data.ident, data.host = findall(
":(.*?)!(.*?)@(.*?)\Z", line[0])[0]
data.msg = ' '.join(line[3:])[1:]
data.chan = line[2]

if data.chan == 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
command_handler.check("msg_private", data)
else:
# check for public-only command hooks
command_handler.check("msg_public", data)

# check for command hooks that apply to all messages
command_handler.check("msg", data)

# hardcode the !restart command (we can't restart from within
# an ordinary command)
if data.msg in ["!restart", ".restart"]:
if data.host in config.irc["permissions"]["owners"]:
print "Restarting bot per owner request..."
return

if line[0] == "PING": # if we are pinged, pong back to the server
connection.send("PONG %s" % line[1])

if line[1] == "376": # we've successfully connected to the network
try: # if we're supposed to auth to nickserv, do that
ns_username = config.irc["frontend"]["nickservUsername"]
ns_password = config.irc["frontend"]["nickservPassword"]
except KeyError:
pass
else:
connection.say("NickServ", "IDENTIFY {0} {1}".format(
ns_username, ns_password))
# join all of our startup channels
for chan in config.irc["frontend"]["channels"]:
connection.join(chan)

+ 0
- 78
irc/watcher.py 查看文件

@@ -1,78 +0,0 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's IRC Watcher Component

The IRC watcher runs on a wiki recent-changes server and listens for edits.
Users cannot interact with this part of the bot. When an event occurs, run it
through irc/watcher_logic.py's process() function, which can result in either
wiki bot tasks being started (listed in wiki/tasks/) or messages being sent to
channels in the IRC frontend.
"""

from core import config
from irc.classes import Connection, RC, BrokenSocketException
from irc import watcher_logic

frontend_conn = None

def get_connection():
"""Return a new Connection() instance with information about our server
connection, but don't actually connect yet."""
cf = config.irc["watcher"]
connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"],
cf["realname"])
return connection

def main(connection, f_conn=None):
"""Main loop for the Watcher IRC Bot component. get_connection() should
have already been called and the connection should have been started with
connection.connect(). Accept the frontend connection as well as an optional
parameter in order to send messages directly to frontend IRC channels."""
global frontend_conn
frontend_conn = f_conn
read_buffer = str()

while 1:
try:
read_buffer = read_buffer + connection.get()
except BrokenSocketException:
return

lines = read_buffer.split("\n")
read_buffer = lines.pop()

for line in lines:
line = line.strip().split()

if line[1] == "PRIVMSG":
chan = line[2]

# ignore messages originating from channels not in our list, to
# prevent someone PMing us false data
if chan not in config.irc["watcher"]["channels"]:
continue

msg = ' '.join(line[3:])[1:]
rc = RC(msg) # new RC object to store this event's data
rc.parse() # parse a message into pagenames, usernames, etc.
process(rc) # report to frontend channels or start tasks

if line[0] == "PING": # if we are pinged, pong back to the server
connection.send("PONG %s" % line[1])

# when we've finished starting up, join all watcher channels
if line[1] == "376":
for chan in config.irc["watcher"]["channels"]:
connection.join(chan)

def process(rc):
"""Process a message from IRC (technically, an RC object). The actual
processing is configurable, so we don't have that hard-coded here. We
simply call irc/watcher_logic.py's process() function and expect a list of
channels back, which we report the event data to."""
chans = watcher_logic.process(rc)
if chans and frontend_conn:
pretty = rc.get_pretty()
for chan in chans:
frontend_conn.say(chan, pretty)

+ 0
- 0
查看文件


+ 0
- 0
查看文件


+ 0
- 19
wiki/base_task.py 查看文件

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-

class BaseTask(object):
"""A base class for bot tasks that edit Wikipedia."""
task_name = None

def __init__(self):
"""This is called once immediately after the task class is loaded by
the task manager (in wiki.task_manager.load_class_from_file())."""
pass

def run(self, **kwargs):
"""This is called directly by task_manager.start_task() and is the main
way to make a task do stuff. kwargs will be any keyword arguments
passed to start_task(), which are (of course) optional. The same task
instance is preserved between runs, so you can theoretically store data
in self (e.g. start_task('mytask', action='store', data='foo')) and
then use it later (e.g. start_task('mytask', action='save'))."""
pass

+ 0
- 92
wiki/task_manager.py 查看文件

@@ -1,92 +0,0 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's Wiki Bot Task Manager

This module provides some functions to run and load bot tasks from wiki/tasks/.
"""

import time
import traceback
import threading
import os

from core import config

# store loaded tasks as a dict where the key is the task name and the value is
# an instance of the task class (wiki.tasks.task_file.Task())
task_list = dict()

def load_tasks():
"""Load all valid task classes from wiki/tasks/, and add them to the
task_list variable."""
files = os.listdir(os.path.join("wiki", "tasks"))
files.sort() # alphabetically sort all files in wiki/tasks/
for f in files:
if not os.path.isfile(os.path.join("wiki", "tasks", f)):
continue # ignore non-files
if f.startswith("_") or not f.endswith(".py"):
continue # ignore non-python files or files beginning with an _
load_class_from_file(f)
print "Found %s tasks: %s." % (len(task_list), ', '.join(task_list.keys()))

def load_class_from_file(f):
"""Look in a given file for the task class."""
global task_list

module = f[:-3] # strip .py from end
try:
exec "from wiki.tasks import %s as m" % module
except: # importing the file failed for some reason...
print "Couldn't load task file %s:" % f
traceback.print_exc()
return
try:
task_class = m.Task
except:
print "Couldn't find or get task class in file %s:" % f
traceback.print_exc()
return
task_name = task_class.task_name
task_list[task_name] = task_class()
print "Added task %s from wiki/tasks/%s..." % (task_name, f)

def start_tasks(now=time.gmtime()):
"""Start all tasks that are supposed to be run at a given time."""
tasks = config.schedule(now.tm_min, now.tm_hour, now.tm_mday, now.tm_mon,
now.tm_wday) # get list of tasks to run this turn

for task in tasks:
if isinstance(task, list): # they've specified kwargs
start_task(task[0], **task[1]) # so pass those to start_task
else: # otherwise, just pass task_name
start_task(task)

def start_task(task_name, **kwargs):
"""Start a given task in a new thread. Pass args to the task's run()
function."""
print "Starting task '{0}' in a new thread...".format(task_name)

try:
task = task_list[task_name]
except KeyError:
print ("Couldn't find task '{0}': wiki/tasks/{0}.py does not exist.").format(task_name)
return

task_thread = threading.Thread(target=lambda: task_wrapper(task, **kwargs))
task_thread.name = "{0} ({1})".format(task_name, time.strftime("%b %d %H:%M:%S"))

# stop bot task threads automagically if the main bot stops
task_thread.daemon = True

task_thread.start()

def task_wrapper(task, **kwargs):
"""Wrapper for task classes: run the task and catch any errors."""
try:
task.run(**kwargs)
except:
print "Task '{0}' raised an exception and had to stop:".format(task.task_name)
traceback.print_exc()
else:
print "Task '{0}' finished without error.".format(task.task_name)

+ 0
- 0
查看文件


+ 0
- 20
wiki/tools/__init__.py 查看文件

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-

"""
EarwigBot's Wiki Toolset

This is a collection of classes and functions to read from and write to
Wikipedia and other wiki sites. No connection whatsoever to python-wikitools
written by Mr.Z-man, other than a similar purpose. We share no code.

Import the toolset with `from wiki import tools`.
"""

from wiki.tools.constants import *
from wiki.tools.exceptions import *
from wiki.tools.functions import *

from wiki.tools.category import Category
from wiki.tools.page import Page
from wiki.tools.site import Site
from wiki.tools.user import User

Loading…
取消
儲存