Bläddra i källkod

all IRC commands should work now; renamed tasks.py to threads.py to avoid conflicting with bot/tasks/

tags/v0.1^2
Ben Kurtovic 12 år sedan
förälder
incheckning
9237a7ed9e
20 ändrade filer med 306 tillägg och 283 borttagningar
  1. +3
    -11
      bot/commands/afc_report.py
  2. +34
    -24
      bot/commands/afc_status.py
  3. +6
    -14
      bot/commands/calc.py
  4. +12
    -13
      bot/commands/chanops.py
  5. +15
    -32
      bot/commands/crypt.py
  6. +62
    -47
      bot/commands/git.py
  7. +15
    -9
      bot/commands/help.py
  8. +20
    -22
      bot/commands/link.py
  9. +18
    -17
      bot/commands/remind.py
  10. +20
    -21
      bot/commands/rights.py
  11. +47
    -34
      bot/commands/threads.py
  12. +14
    -7
      bot/tasks/__init__.py
  13. +8
    -8
      bot/wiki/__init__.py
  14. +2
    -2
      bot/wiki/category.py
  15. +1
    -1
      bot/wiki/constants.py
  16. +1
    -1
      bot/wiki/exceptions.py
  17. +11
    -7
      bot/wiki/functions.py
  18. +1
    -1
      bot/wiki/page.py
  19. +9
    -8
      bot/wiki/site.py
  20. +7
    -4
      bot/wiki/user.py

+ 3
- 11
bot/commands/afc_report.py Visa fil

@@ -6,17 +6,9 @@ import urllib

from 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
class Command(BaseCommand):
"""Get information about an AFC submission by name."""
name = "report"

def process(self, data):
self.data = data


+ 34
- 24
bot/commands/afc_status.py Visa fil

@@ -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 classes import BaseCommand
import config
import wiki

class AFCStatus(BaseCommand):
def get_hooks(self):
return ["join", "msg"]

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"]:
@@ -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


+ 6
- 14
bot/commands/calc.py Visa fil

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

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

import re
import urllib

from 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."

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>')


+ 12
- 13
bot/commands/chanops.py Visa fil

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

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

from classes import BaseCommand
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
class Command(BaseCommand):
"""Voice, devoice, op, or deop users in the channel."""
name = "chanops"

def check(self, data):
if data.is_command and data.command in ["voice", "devoice", "op", "deop"]:
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"]:
self.connection.reply(data, "you must be a bot admin to use this command.")
msg = "you must be a bot admin to use this command."
self.connection.reply(data, msg)
return

if not data.args: # if it is just !op/!devoice/whatever without arguments, assume they want to do this to themselves
# 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]

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

+ 15
- 32
bot/commands/crypt.py Visa fil

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

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

import hashlib

from classes import BaseCommand
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'.")
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"]:
@@ -34,31 +17,31 @@ class Cryptography(BaseCommand):

def process(self, data):
if not data.args:
self.connection.reply(data, "what do you want me to {0}?".format(
data.command))
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)
self.connection.reply(data, "supported algorithms: " + algos +
".")
msg = algos.join(("supported algorithms: ", "."))
self.connection.reply(data, msg)
elif algo in hashlib.algorithms:
string = ' '.join(data.args[1:])
result = eval("hashlib.{0}(string)".format(algo)).hexdigest()
result = getattr(hashlib, algo)(string).hexdigest()
self.connection.reply(data, result)
else:
self.connection.reply(data, "unknown algorithm: '{0}'.".format(
algo))
msg = "unknown algorithm: '{0}'.".format(algo)
self.connection.reply(data, msg)

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))
msg = "a key was provided, but text to {0} was not."
self.connection.reply(data, msg.format(data.command))
return

try:
@@ -67,5 +50,5 @@ class Cryptography(BaseCommand):
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))
msg = "{0}: {1}.".format(error.__class__.__name__, error)
self.connection.reply(data, msg)

+ 62
- 47
bot/commands/git.py Visa fil

@@ -1,7 +1,5 @@
# -*- 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
@@ -9,26 +7,21 @@ import re
from classes import BaseCommand
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
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"]:
self.connection.reply(data, "you must be a bot owner to use this command.")
msg = "you must be a bot owner to use this command."
self.connection.reply(data, msg)
return

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

if data.args[0] == "help":
@@ -52,19 +45,20 @@ class Git(BaseCommand):
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])
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"""
"""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
result = result[:-1] # Strip newline
return result

def do_help(self):
"""display all commands"""
"""Display all commands."""
help_dict = {
"branch": "get current branch",
"branches": "get all branches",
@@ -82,21 +76,24 @@ class Git(BaseCommand):
self.connection.reply(self.data, "sub-commands are: %s." % help)

def do_branch(self):
"""get our current branch"""
"""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)
msg = "currently on branch \x0302{0}\x0301.".format(branch)
self.connection.reply(self.data, msg)

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

def do_checkout(self):
"""switch branches"""
"""Switch branches."""
try:
branch = self.data.args[1]
except IndexError: # no branch name provided
@@ -108,15 +105,20 @@ class Git(BaseCommand):
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)
msg = "already on \x0302{0}\x0301!".format(branch)
self.connection.reply(self.data, msg)
else:
self.connection.reply(self.data, "switched from branch \x0302%s\x0301 to \x0302%s\x0301." % (current_branch, branch))
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
self.connection.reply(self.data, "branch \x0302%s\x0301 doesn't exist!" % branch)
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 on it"""
"""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
@@ -126,38 +128,51 @@ class Git(BaseCommand):
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.")
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)
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)
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 remote repository"""
"""Pull from our 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)
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:
changes = re.findall("\s*((.*?)\sfile(.*?)tions?\(-\))", result)[0][0] # find the changes
regex = "\s*((.*?)\sfile(.*?)tions?\(-\))"
changes = re.findall(regex, result)[0][0]
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
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\"")
"""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)
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:
self.connection.reply(self.data, "last local commit was %s. Remote is \x02ahead\x0F of local copy." % last)
msg = "last local commit was {0}. Remote is \x02ahead\x0F of local copy."
self.connection.reply(self.data, msg.format(last))

+ 15
- 9
bot/commands/help.py Visa fil

@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-

import re

from classes import BaseCommand, Data
import commands

@@ -17,7 +19,9 @@ class Command(BaseCommand):
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>'."
msg = msg.format(len(self.cmnds.keys()), ', '.join(self.cmnds))
cmnds = self.cmnds.keys()
cmnds.sort()
msg = msg.format(len(cmnds), ', '.join(cmnds))
self.connection.reply(data, msg)

def do_command_help(self, data):
@@ -26,18 +30,20 @@ class Command(BaseCommand):

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

for cmnd in self.cmnds.values():
if cmnd.check(dummy):
doc = cmnd.__doc__
if doc:
msg = "info for command \x0303{0}\x0301: \"{1}\""
self.connection.reply(data, msg.format(command, doc))
return
break
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)

+ 20
- 22
bot/commands/link.py Visa fil

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

# Convert a Wikipedia page name into a URL.

import re
from urllib import quote

from classes import BaseCommand

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

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)

+ 18
- 17
bot/commands/remind.py Visa fil

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

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

import threading
import time

from 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."
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"]:
@@ -23,24 +16,32 @@ class Remind(BaseCommand):

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>.")
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:
self.connection.reply(data, "the time must be given as an integer, in seconds.")
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:
self.connection.reply(data, "what message do you want me to give you when time is up?")
msg = "what message do you want me to give you when time is up?"
self.connection.reply(data, msg)
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))
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 = threading.Thread(target=self.reminder,
args=(data, message, wait))
t_reminder.name = "reminder " + end_time
t_reminder.daemon = True
t_reminder.start()


+ 20
- 21
bot/commands/rights.py Visa fil

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

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

from classes import BaseCommand
import wiki

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

def get_help(self, command):
return "Retrieve a list of rights for a given username."
class Command(BaseCommand):
"""Retrieve a list of rights for a given username."""
name = "rights"

def check(self, data):
if data.is_command and data.command in ["rights", "groups", "permissions", "privileges"]:
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, "what user do you want me to look up?")
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)
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))
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)))

bot/commands/tasks.py → bot/commands/threads.py Visa fil

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-

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

import threading
import re

@@ -9,29 +7,29 @@ from classes import BaseCommand, Data, KwargParseException
import tasks
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."
class Command(BaseCommand):
"""Manage wiki tasks from IRC, and check on thread status."""
name = "threads"

def check(self, data):
if data.is_command and data.command in ["tasks", "task", "threads", "tasklist"]:
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"]:
self.connection.reply(data, "at this time, you must be a bot owner to use this command.")
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:
self.connection.reply(data, "no arguments provided. Maybe you wanted '!{cmnd} list', '!{cmnd} start', or '!{cmnd} listall'?".format(cmnd=data.command))
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":
@@ -43,8 +41,9 @@ class Tasks(BaseCommand):
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]))
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
@@ -59,43 +58,54 @@ class Tasks(BaseCommand):
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))
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"]:
normal_threads.append("\x0302{0}\x0301 (id {1})".format(tname, thread.ident))
t = "\x0302{0}\x0301 (id {1})"
normal_threads.append(t.format(tname, thread.ident))
elif tname.startswith("reminder"):
normal_threads.append("\x0302reminder\x0301 (until {0})".format(tname.replace("reminder ", "")))
tname = tname.replace("reminder ", "")
t = "\x0302reminder\x0301 (until {0})"
normal_threads.append(t.format(tname))
else:
tname, start_time = re.findall("^(.*?) \((.*?)\)$", tname)[0]
task_threads.append("\x0302{0}\x0301 (id {1}, since {2})".format(tname, thread.ident, start_time))
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}.".format(len(threads), ', '.join(normal_threads), len(task_threads), ', '.join(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.".format(len(threads), ', '.join(normal_threads))
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."""
tasks = tasks._tasks.keys()
all_tasks = tasks.get_all().keys()
threads = threading.enumerate()
tasklist = []

tasks.sort()
all_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)
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:
tasklist.append("\x0302{0}\x0301 (\x02active\x0F as id {1})".format(task, ids[0]))
t = "\x0302{0}\x0301 (\x02active\x0F as id {1})"
tasklist.append(t.format(task, ids[0]))
else:
tasklist.append("\x0302{0}\x0301 (\x02active\x0F as ids {1})".format(task, ', '.join(ids)))
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(tasks), tasklist)
msg = "{0} tasks loaded: {1}.".format(len(all_tasks), tasklist)
self.connection.reply(self.data, msg)

def do_start(self):
@@ -105,26 +115,29 @@ class Tasks(BaseCommand):

try:
task_name = data.args[1]
except IndexError: # no task name given
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))
msg = "error parsing argument: \x0303{0}\x0301.".format(arg)
self.connection.reply(data, msg)
return

if task_name not in tasks._tasks.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))
# This task does not exist or hasn't been loaded:
if task_name not in tasks._tasks.keys():
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)
self.connection.reply(data, "task \x0302{0}\x0301 started.".format(task_name))
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; e.g. "irc-frontend" or
"irc-watcher"."""
"""Return the "proper" name of the MainThread."""
if "irc_frontend" in config.components:
return "irc-frontend"
elif "wiki_schedule" in config.components:

+ 14
- 7
bot/tasks/__init__.py Visa fil

@@ -14,7 +14,7 @@ import os

import config

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

# Store loaded tasks as a dict where the key is the task name and the value is
# an instance of the task class:
@@ -46,14 +46,15 @@ def _wrapper(task, **kwargs):
try:
task.run(**kwargs)
except:
print "Task '{0}' raised an exception and had to stop:".format(task.task_name)
error = "Task '{0}' raised an exception and had to stop:"
print error.format(task.task_name)
traceback.print_exc()
else:
print "Task '{0}' finished without error.".format(task.task_name)

def load():
"""Load all valid task classes from bot/tasks/, and add them to the
_tasks variable."""
"""Load all valid task classes from bot/tasks/, and add them to the _tasks
variable."""
files = os.listdir(os.path.join("bot", "tasks"))
files.sort() # alphabetically sort all files in wiki/tasks/
for f in files:
@@ -83,13 +84,19 @@ def start(task_name, **kwargs):
try:
task = _tasks[task_name]
except KeyError:
print ("Couldn't find task '{0}': bot/tasks/{0}.py does not exist.").format(task_name)
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))
task_thread.name = "{0} ({1})".format(task_name, time.strftime("%b %d %H:%M:%S"))
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
# 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

+ 8
- 8
bot/wiki/__init__.py Visa fil

@@ -7,14 +7,14 @@ 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`.
Import the toolset with `import wiki`.
"""

from wiki.tools.constants import *
from wiki.tools.exceptions import *
from wiki.tools.functions import *
from wiki.constants import *
from wiki.exceptions import *
from wiki.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
from wiki.category import Category
from wiki.page import Page
from wiki.site import Site
from wiki.user import User

+ 2
- 2
bot/wiki/category.py Visa fil

@@ -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]

+ 1
- 1
bot/wiki/constants.py Visa fil

@@ -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


+ 1
- 1
bot/wiki/exceptions.py Visa fil

@@ -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):


+ 11
- 7
bot/wiki/functions.py Visa fil

@@ -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,14 @@ 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]
namespaces[int(key)] = value

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)


+ 1
- 1
bot/wiki/page.py Visa fil

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

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

class Page(object):
"""


+ 9
- 8
bot/wiki/site.py Visa fil

@@ -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)


+ 7
- 4
bot/wiki/user.py Visa fil

@@ -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"]


Laddar…
Avbryt
Spara