diff --git a/bot/commands/afc_report.py b/bot/commands/afc_report.py index c32220e..402b959 100644 --- a/bot/commands/afc_report.py +++ b/bot/commands/afc_report.py @@ -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 diff --git a/bot/commands/afc_status.py b/bot/commands/afc_status.py index 1258eae..d476daf 100644 --- a/bot/commands/afc_status.py +++ b/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 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 diff --git a/bot/commands/calc.py b/bot/commands/calc.py index 2a138a0..0fc0fbd 100644 --- a/bot/commands/calc.py +++ b/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 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)(.*?)') diff --git a/bot/commands/chanops.py b/bot/commands/chanops.py index fec8399..727ebbf 100644 --- a/bot/commands/chanops.py +++ b/bot/commands/chanops.py @@ -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) diff --git a/bot/commands/crypt.py b/bot/commands/crypt.py index 8741342..8727c6d 100644 --- a/bot/commands/crypt.py +++ b/bot/commands/crypt.py @@ -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) diff --git a/bot/commands/git.py b/bot/commands/git.py index d6b67c7..90d7bdc 100644 --- a/bot/commands/git.py +++ b/bot/commands/git.py @@ -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)) diff --git a/bot/commands/help.py b/bot/commands/help.py index 6f57381..484dcb4 100644 --- a/bot/commands/help.py +++ b/bot/commands/help.py @@ -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 '." - 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) diff --git a/bot/commands/link.py b/bot/commands/link.py index 6c972af..749da14 100644 --- a/bot/commands/link.py +++ b/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 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) diff --git a/bot/commands/remind.py b/bot/commands/remind.py index 631f2a1..fd0234d 100644 --- a/bot/commands/remind.py +++ b/bot/commands/remind.py @@ -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