setting up threading and two IRC sockets setting up watcher to report recent changes to a given list of channels on the main srver code cleanuptags/v0.1
| @@ -1,69 +0,0 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| ## Imports | |||||
| import socket, re, time | |||||
| from config.irc_config import * | |||||
| from config.secure_config import * | |||||
| from irc import triggers | |||||
| from irc.actions import * | |||||
| from irc.data import * | |||||
| def main(): | |||||
| read_buffer = str() | |||||
| while 1: | |||||
| try: | |||||
| read_buffer = read_buffer + actions.get() | |||||
| except RuntimeError: # socket broke | |||||
| print "socket has broken, sleeping for a minute and restarting..." | |||||
| time.sleep(60) # sleep for sixty seconds | |||||
| return # then exit our loop and restart the bot | |||||
| lines = read_buffer.split("\n") | |||||
| read_buffer = lines.pop() | |||||
| for line in lines: | |||||
| line = line.strip().split() | |||||
| data = Data() | |||||
| if line[1] == "JOIN": | |||||
| data.nick, data.ident, data.host = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] | |||||
| data.chan = line[2][1:] | |||||
| triggers.check(actions, data, "join") # check if there's anything we can respond to, and if so, respond | |||||
| if line[1] == "PRIVMSG": | |||||
| data.nick, data.ident, data.host = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] | |||||
| data.msg = ' '.join(line[3:])[1:] | |||||
| data.chan = line[2] | |||||
| if data.chan == NICK: # this is a privmsg to us, so set 'chan' as the nick of the sender | |||||
| data.chan = data.nick | |||||
| triggers.check(actions, data, "msg_private") # only respond if it's a private message | |||||
| else: | |||||
| triggers.check(actions, data, "msg_public") # only respond if it's a public (channel) message | |||||
| triggers.check(actions, data, "msg") # check for general messages | |||||
| if data.msg == "!restart": # hardcode the !restart command (we can't return from within actions.py) | |||||
| if data.host in ADMINS: | |||||
| return True | |||||
| if line[0] == "PING": # If we are pinged, pong back to the server | |||||
| actions.send("PONG %s" % line[1]) | |||||
| if line[1] == "376": | |||||
| if NS_AUTH: # if we're supposed to auth to nickserv, do that | |||||
| actions.say("NickServ", "IDENTIFY %s %s" % (NS_USER, NS_PASS)) | |||||
| for chan in CHANS: # join all of our startup channels | |||||
| actions.join(chan) | |||||
| if __name__ == "__main__": | |||||
| sock = socket.socket() | |||||
| sock.connect((HOST, PORT)) | |||||
| actions = Actions(sock) | |||||
| actions.send("NICK %s" % NICK) | |||||
| actions.send("USER %s %s bla :%s" % (IDENT, HOST, REALNAME)) | |||||
| main() | |||||
| @@ -3,23 +3,24 @@ | |||||
| # EarwigBot Configuration File | # EarwigBot Configuration File | ||||
| # This file contains information that the bot uses to connect to IRC. | # This file contains information that the bot uses to connect to IRC. | ||||
| # our server's hostname | |||||
| # our main (front-end) server's hostname and port | |||||
| HOST = "irc.freenode.net" | HOST = "irc.freenode.net" | ||||
| # our server's port | |||||
| PORT = 6667 | PORT = 6667 | ||||
| # our nick | |||||
| NICK = "EarwigBot" | |||||
| # our watcher server's hostname, port, and RC channel | |||||
| WATCHER_HOST = "irc.wikimedia.org" | |||||
| WATCHER_PORT = 6667 | |||||
| WATCHER_CHAN = "#en.wikipedia" | |||||
| # our ident | |||||
| # our nick, ident, and real name, used on both servers | |||||
| NICK = "EarwigBot" | |||||
| IDENT = "earwigbot" | IDENT = "earwigbot" | ||||
| # our real name | |||||
| REALNAME = "[[w:en:User:EarwigBot]]" | REALNAME = "[[w:en:User:EarwigBot]]" | ||||
| # channel to join on startup | |||||
| # channels to join on main server's startup | |||||
| CHANS = ["##earwigbot", "##earwig", "#wikipedia-en-afc"] | CHANS = ["##earwigbot", "##earwig", "#wikipedia-en-afc"] | ||||
| AFC_CHANS = ["#wikipedia-en-afc"] # report recent AFC changes | |||||
| BOT_CHANS = ["##earwigbot", "#wikipedia-en-afc"] # report edits containing "!earwigbot" | |||||
| # hostnames of users who can update/restart the bot with !update | |||||
| # hardcoded hostnames of users who can use !restart and !git | |||||
| ADMINS = ["wikipedia/The-Earwig"] | ADMINS = ["wikipedia/The-Earwig"] | ||||
| @@ -0,0 +1,50 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| ## EarwigBot's Core | |||||
| ## Basically, this creates threads for our IRC watcher component and Wikipedia component, and then runs the main IRC bot on the main thread. | |||||
| ## The IRC bot component of EarwigBot has two parts: a front-end and a watcher. | |||||
| ## The front-end runs on a normal IRC server and expects users to interact with it/give it commands. | |||||
| ## The watcher runs on a wiki recent-changes server and listens for edits. Users cannot interact with this part of the bot. | |||||
| import threading | |||||
| import time | |||||
| import traceback | |||||
| import sys | |||||
| import os | |||||
| parent_dir = os.path.split(sys.path[0])[0] | |||||
| sys.path.append(parent_dir) # make sure we look in the parent directory for modules | |||||
| from irc import frontend, watcher | |||||
| f_conn = None | |||||
| w_conn = None | |||||
| def irc_watcher(f_conn): | |||||
| global w_conn | |||||
| while 1: # restart the watcher component if (just) it breaks | |||||
| w_conn = watcher.get_connection() | |||||
| try: | |||||
| watcher.main(w_conn, f_conn) | |||||
| except: | |||||
| traceback.print_exc() | |||||
| time.sleep(5) # sleep a bit before restarting watcher | |||||
| print "restarting watcher component..." | |||||
| def run(): | |||||
| global f_conn | |||||
| f_conn = frontend.get_connection() | |||||
| t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | |||||
| t_watcher.daemon = True | |||||
| t_watcher.start() | |||||
| frontend.main(f_conn) | |||||
| if __name__ == "__main__": | |||||
| try: | |||||
| run() | |||||
| finally: | |||||
| f_conn.close() | |||||
| w_conn.close() | |||||
| @@ -0,0 +1,15 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| import time | |||||
| from subprocess import * | |||||
| try: | |||||
| from config import irc_config, secure_config | |||||
| except ImportError: | |||||
| print """Missing a config file! Make sure you have configured the bot. All *.py.default files in config/ | |||||
| should have their .default extension removed, and the info inside should be corrected.""" | |||||
| exit() | |||||
| while 1: | |||||
| call(['python', 'core/main.py']) | |||||
| time.sleep(5) # sleep for five seconds between bot runs | |||||
| @@ -1,40 +0,0 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # Actions/commands to interface with IRC. | |||||
| class Actions: | |||||
| def __init__(self, sock): | |||||
| """actions/commands to interface with IRC""" | |||||
| self.sock = sock | |||||
| def get(self, size = 4096): | |||||
| """receive (get) data from the server""" | |||||
| data = self.sock.recv(4096) | |||||
| if not data: | |||||
| raise RuntimeError("socket is dead") | |||||
| return data | |||||
| def send(self, msg): | |||||
| """send data to the server""" | |||||
| self.sock.send(msg + "\r\n") | |||||
| print " %s" % msg | |||||
| def say(self, target, msg): | |||||
| """send a message""" | |||||
| self.send("PRIVMSG %s :%s" % (target, msg)) | |||||
| def reply(self, target, nick, msg): | |||||
| """send a message as a reply""" | |||||
| self.say(target, "%s%s%s: %s" % (chr(2), 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) | |||||
| @@ -5,18 +5,18 @@ | |||||
| import shlex, subprocess, re | import shlex, subprocess, re | ||||
| from config.irc_config import * | from config.irc_config import * | ||||
| actions, data = None, None | |||||
| connection, data = None, None | |||||
| def call(a, d): | |||||
| global actions, data | |||||
| actions, data = a, d | |||||
| def call(c, d): | |||||
| global connection, data | |||||
| connection, data = c, d | |||||
| if data.host not in ADMINS: | if data.host not in ADMINS: | ||||
| actions.reply(data.chan, data.nick, "you must be a bot admin to use this command.") | |||||
| connection.reply(data.chan, data.nick, "you must be a bot admin to use this command.") | |||||
| return | return | ||||
| if not data.args: | if not data.args: | ||||
| actions.reply(data.chan, data.nick, "no arguments provided.") | |||||
| connection.reply(data.chan, data.nick, "no arguments provided.") | |||||
| return | return | ||||
| if data.args[0] == "help": | if data.args[0] == "help": | ||||
| @@ -41,7 +41,7 @@ def call(a, d): | |||||
| do_status() | do_status() | ||||
| else: # they asked us to do something we don't know | else: # they asked us to do something we don't know | ||||
| actions.reply(data.chan, data.nick, "unknown argument: \x0303%s\x0301." % data.args[0]) | |||||
| connection.reply(data.chan, data.nick, "unknown argument: \x0303%s\x0301." % data.args[0]) | |||||
| def exec_shell(command): | def exec_shell(command): | ||||
| """execute a shell command and get the output""" | """execute a shell command and get the output""" | ||||
| @@ -68,14 +68,14 @@ def do_help(): | |||||
| help += "\x0303%s\x0301 (%s), " % (key, help_dict[key]) | help += "\x0303%s\x0301 (%s), " % (key, help_dict[key]) | ||||
| help = help[:-2] # trim last comma | help = help[:-2] # trim last comma | ||||
| actions.reply(data.chan, data.nick, "sub-commands are: %s." % help) | |||||
| connection.reply(data.chan, data.nick, "sub-commands are: %s." % help) | |||||
| def do_branch(): | def do_branch(): | ||||
| """get our current branch""" | """get our current branch""" | ||||
| branch = exec_shell("git name-rev --name-only HEAD") | branch = exec_shell("git name-rev --name-only HEAD") | ||||
| branch = branch[:-1] # strip newline | branch = branch[:-1] # strip newline | ||||
| actions.reply(data.chan, data.nick, "currently on branch \x0302%s\x0301." % branch) | |||||
| connection.reply(data.chan, data.nick, "currently on branch \x0302%s\x0301." % branch) | |||||
| def do_branches(): | def do_branches(): | ||||
| """get list of branches""" | """get list of branches""" | ||||
| @@ -87,66 +87,66 @@ def do_branches(): | |||||
| branches = branches.replace('\n ', ', ') | branches = branches.replace('\n ', ', ') | ||||
| branches = branches.strip() | branches = branches.strip() | ||||
| actions.reply(data.chan, data.nick, "branches: \x0302%s\x0301." % branches) | |||||
| connection.reply(data.chan, data.nick, "branches: \x0302%s\x0301." % branches) | |||||
| def do_checkout(): | def do_checkout(): | ||||
| """switch branches""" | """switch branches""" | ||||
| try: | try: | ||||
| branch = data.args[1] | branch = data.args[1] | ||||
| except IndexError: # no branch name provided | except IndexError: # no branch name provided | ||||
| actions.reply(data.chan, data.nick, "switch to which branch?") | |||||
| connection.reply(data.chan, data.nick, "switch to which branch?") | |||||
| return | return | ||||
| try: | try: | ||||
| result = exec_shell("git checkout %s" % branch) | result = exec_shell("git checkout %s" % branch) | ||||
| if "Already on" in result: | if "Already on" in result: | ||||
| actions.reply(data.chan, data.nick, "already on \x0302%s\x0301!" % branch) | |||||
| connection.reply(data.chan, data.nick, "already on \x0302%s\x0301!" % branch) | |||||
| else: | else: | ||||
| actions.reply(data.chan, data.nick, "switched to branch \x0302%s\x0301." % branch) | |||||
| connection.reply(data.chan, data.nick, "switched to branch \x0302%s\x0301." % branch) | |||||
| except subprocess.CalledProcessError: # git couldn't switch branches | except subprocess.CalledProcessError: # git couldn't switch branches | ||||
| actions.reply(data.chan, data.nick, "branch \x0302%s\x0301 doesn't exist!" % branch) | |||||
| connection.reply(data.chan, data.nick, "branch \x0302%s\x0301 doesn't exist!" % branch) | |||||
| def do_delete(): | def do_delete(): | ||||
| """delete a branch, while making sure that we are not on it""" | """delete a branch, while making sure that we are not on it""" | ||||
| try: | try: | ||||
| delete_branch = data.args[1] | delete_branch = data.args[1] | ||||
| except IndexError: # no branch name provided | except IndexError: # no branch name provided | ||||
| actions.reply(data.chan, data.nick, "delete which branch?") | |||||
| connection.reply(data.chan, data.nick, "delete which branch?") | |||||
| return | return | ||||
| current_branch = exec_shell("git name-rev --name-only HEAD") | current_branch = exec_shell("git name-rev --name-only HEAD") | ||||
| current_branch = current_branch[:-1] # strip newline | current_branch = current_branch[:-1] # strip newline | ||||
| if current_branch == delete_branch: | if current_branch == delete_branch: | ||||
| actions.reply(data.chan, data.nick, "you're currently on this branch; please checkout to a different branch before deleting.") | |||||
| connection.reply(data.chan, data.nick, "you're currently on this branch; please checkout to a different branch before deleting.") | |||||
| return | return | ||||
| try: | try: | ||||
| exec_shell("git branch -d %s" % delete_branch) | exec_shell("git branch -d %s" % delete_branch) | ||||
| actions.reply(data.chan, data.nick, "branch \x0302%s\x0301 has been deleted locally." % delete_branch) | |||||
| connection.reply(data.chan, data.nick, "branch \x0302%s\x0301 has been deleted locally." % delete_branch) | |||||
| except subprocess.CalledProcessError: # git couldn't delete | except subprocess.CalledProcessError: # git couldn't delete | ||||
| actions.reply(data.chan, data.nick, "branch \x0302%s\x0301 doesn't exist!" % delete_branch) | |||||
| connection.reply(data.chan, data.nick, "branch \x0302%s\x0301 doesn't exist!" % delete_branch) | |||||
| def do_pull(): | def do_pull(): | ||||
| """pull from remote repository""" | """pull from remote repository""" | ||||
| branch = exec_shell("git name-rev --name-only HEAD") | branch = exec_shell("git name-rev --name-only HEAD") | ||||
| branch = branch[:-1] # strip newline | branch = branch[:-1] # strip newline | ||||
| actions.reply(data.chan, data.nick, "pulling from remote (currently on \x0302%s\x0301)..." % branch) | |||||
| connection.reply(data.chan, data.nick, "pulling from remote (currently on \x0302%s\x0301)..." % branch) | |||||
| result = exec_shell("git pull") | result = exec_shell("git pull") | ||||
| if "Already up-to-date." in result: | if "Already up-to-date." in result: | ||||
| actions.reply(data.chan, data.nick, "done; no new changes.") | |||||
| connection.reply(data.chan, data.nick, "done; no new changes.") | |||||
| else: | else: | ||||
| changes = re.findall("\s*((.*?)\sfile(.*?)tions?\(-\))", result)[0][0] # find the changes | changes = re.findall("\s*((.*?)\sfile(.*?)tions?\(-\))", result)[0][0] # find the changes | ||||
| actions.reply(data.chan, data.nick, "done; %s." % changes) | |||||
| connection.reply(data.chan, data.nick, "done; %s." % changes) | |||||
| def do_status(): | def do_status(): | ||||
| """check whether we have anything to pull""" | """check whether we have anything to pull""" | ||||
| actions.reply(data.chan, data.nick, "checking remote for updates...") | |||||
| connection.reply(data.chan, data.nick, "checking remote for updates...") | |||||
| result = exec_shell("git fetch --dry-run") | result = exec_shell("git fetch --dry-run") | ||||
| if not result: | if not result: | ||||
| actions.reply(data.chan, data.nick, "local copy is up-to-date with remote.") | |||||
| connection.reply(data.chan, data.nick, "local copy is up-to-date with remote.") | |||||
| else: | else: | ||||
| actions.reply(data.chan, data.nick, "remote is ahead of local copy.") | |||||
| connection.reply(data.chan, data.nick, "remote is ahead of local copy.") | |||||
| @@ -2,11 +2,11 @@ | |||||
| """Generates help information.""" | """Generates help information.""" | ||||
| actions, data = None, None | |||||
| connection, data = None, None | |||||
| def call(a, d): | |||||
| global actions, data | |||||
| actions, data = a, d | |||||
| def call(c, d): | |||||
| global connection, data | |||||
| connection, data = c, d | |||||
| if not data.args: | if not data.args: | ||||
| do_general_help() | do_general_help() | ||||
| @@ -15,7 +15,7 @@ def call(a, d): | |||||
| do_command_help() | do_command_help() | ||||
| def do_general_help(): | def do_general_help(): | ||||
| actions.reply(data.chan, data.nick, "I am a bot! You can get help for any command by typing '!help <command>'.") | |||||
| connection.reply(data.chan, data.nick, "I am a bot! You can get help for any command by typing '!help <command>'.") | |||||
| def do_command_help(): | def do_command_help(): | ||||
| command = data.args[0] | command = data.args[0] | ||||
| @@ -23,12 +23,12 @@ def do_command_help(): | |||||
| try: | try: | ||||
| exec "from irc.commands import %s as this_command" % command | exec "from irc.commands import %s as this_command" % command | ||||
| except ImportError: | except ImportError: | ||||
| actions.reply(data.chan, data.nick, "command \x0303%s\x0301 not found!" % command) | |||||
| connection.reply(data.chan, data.nick, "command \x0303%s\x0301 not found!" % command) | |||||
| return | return | ||||
| info = this_command.__doc__ | info = this_command.__doc__ | ||||
| if info: | if info: | ||||
| actions.reply(data.chan, data.nick, "info for command \x0303%s\x0301: \"%s\"" % (command, info)) | |||||
| connection.reply(data.chan, data.nick, "info for command \x0303%s\x0301: \"%s\"" % (command, info)) | |||||
| else: | else: | ||||
| actions.reply(data.chan, data.nick, "sorry, no information for \x0303%s\x0301." % command) | |||||
| connection.reply(data.chan, data.nick, "sorry, no information for \x0303%s\x0301." % command) | |||||
| @@ -4,17 +4,17 @@ | |||||
| import random | import random | ||||
| actions, data = None, None | |||||
| connection, data = None, None | |||||
| def call(a, d): | |||||
| global actions, data | |||||
| actions, data = a, d | |||||
| def call(c, d): | |||||
| global connection, data | |||||
| connection, data = c, d | |||||
| choices = ("say_hi()", "say_sup()") | choices = ("say_hi()", "say_sup()") | ||||
| exec random.choice(choices) | exec random.choice(choices) | ||||
| def say_hi(): | def say_hi(): | ||||
| actions.say(data.chan, "Hey \x02%s\x0F!" % data.nick) | |||||
| connection.say(data.chan, "Hey \x02%s\x0F!" % data.nick) | |||||
| def say_sup(): | def say_sup(): | ||||
| actions.say(data.chan, "'sup \x02%s\x0F?" % data.nick) | |||||
| connection.say(data.chan, "'sup \x02%s\x0F?" % data.nick) | |||||
| @@ -0,0 +1,67 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # A class to interface with IRC. | |||||
| import socket | |||||
| import threading | |||||
| class Connection: | |||||
| def __init__(self, host, port, nick, ident, realname): | |||||
| """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 RuntimeError("socket is dead") | |||||
| 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, target, nick, msg): | |||||
| """send a message as a reply""" | |||||
| self.say(target, "%s%s%s: %s" % (chr(2), 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) | |||||
| @@ -22,7 +22,4 @@ class Data: | |||||
| except IndexError: | except IndexError: | ||||
| self.command = None | self.command = None | ||||
| try: | |||||
| self.args = args[1:] # the command arguments | |||||
| except IndexError: | |||||
| self.args = None | |||||
| self.args = args[1:] # the command arguments | |||||
| @@ -0,0 +1,66 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| ## Imports | |||||
| import re | |||||
| from config.irc_config import * | |||||
| from config.secure_config import * | |||||
| from irc import triggers | |||||
| from irc.connection import Connection | |||||
| from irc.data import Data | |||||
| def get_connection(): | |||||
| connection = Connection(HOST, PORT, NICK, IDENT, REALNAME) | |||||
| return connection | |||||
| def main(connection): | |||||
| connection.connect() | |||||
| read_buffer = str() | |||||
| while 1: | |||||
| try: | |||||
| read_buffer = read_buffer + connection.get() | |||||
| except RuntimeError: # socket broke | |||||
| print "socket has broken on front-end; restarting bot..." | |||||
| return | |||||
| lines = read_buffer.split("\n") | |||||
| read_buffer = lines.pop() | |||||
| for line in lines: | |||||
| line = line.strip().split() | |||||
| data = Data() | |||||
| if line[1] == "JOIN": | |||||
| data.nick, data.ident, data.host = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] | |||||
| data.chan = line[2][1:] | |||||
| triggers.check(connection, data, "join") # check if there's anything we can respond to, and if so, respond | |||||
| if line[1] == "PRIVMSG": | |||||
| data.nick, data.ident, data.host = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] | |||||
| data.msg = ' '.join(line[3:])[1:] | |||||
| data.chan = line[2] | |||||
| if data.chan == NICK: # this is a privmsg to us, so set 'chan' as the nick of the sender | |||||
| data.chan = data.nick | |||||
| triggers.check(connection, data, "msg_private") # only respond if it's a private message | |||||
| else: | |||||
| triggers.check(connection, data, "msg_public") # only respond if it's a public (channel) message | |||||
| triggers.check(connection, data, "msg") # check for general messages | |||||
| if data.msg.startswith("!restart"): # hardcode the !restart command (we can't restart from within an ordinary command) | |||||
| if data.host in ADMINS: | |||||
| print "restarting bot per admin 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": | |||||
| if NS_AUTH: # if we're supposed to auth to nickserv, do that | |||||
| connection.say("NickServ", "IDENTIFY %s %s" % (NS_USER, NS_PASS)) | |||||
| for chan in CHANS: # join all of our startup channels | |||||
| connection.join(chan) | |||||
| @@ -0,0 +1,33 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # A class to store data on an individual event received from our IRC watcher. | |||||
| import re | |||||
| class RC: | |||||
| 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 | |||||
| # page name of the modified page | |||||
| # '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/%s" % page | |||||
| flags = flags.strip() # flag tends to have a extraneous whitespace character at the end when it's a log entry | |||||
| self.page, self.flags, self.url, self.user, self.comment = page, flags, url, user, comment | |||||
| def pretty(self): | |||||
| """make a nice, colorful message from self.msg to send to the front-end""" | |||||
| pretty = self.msg | |||||
| return pretty | |||||
| @@ -4,7 +4,7 @@ | |||||
| from irc.commands import test, help, git | from irc.commands import test, help, git | ||||
| def check(actions, data, hook): | |||||
| def check(connection, data, hook): | |||||
| data.parse_args() # parse command arguments into data.command and data.args | data.parse_args() # parse command arguments into data.command and data.args | ||||
| if hook == "join": | if hook == "join": | ||||
| @@ -18,10 +18,10 @@ def check(actions, data, hook): | |||||
| if hook == "msg": | if hook == "msg": | ||||
| if data.command == "!test": | if data.command == "!test": | ||||
| test.call(actions, data) | |||||
| test.call(connection, data) | |||||
| elif data.command == "!help": | elif data.command == "!help": | ||||
| help.call(actions, data) | |||||
| help.call(connection, data) | |||||
| elif data.command == "!git": | elif data.command == "!git": | ||||
| git.call(actions, data) | |||||
| git.call(connection, data) | |||||
| @@ -0,0 +1,71 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| ## Imports | |||||
| import re | |||||
| from config.irc_config import * | |||||
| from irc.connection import Connection | |||||
| from irc.rc import RC | |||||
| global frontend_conn | |||||
| def get_connection(): | |||||
| connection = Connection(WATCHER_HOST, WATCHER_PORT, NICK, IDENT, REALNAME) | |||||
| return connection | |||||
| def main(connection, f_conn): | |||||
| global frontend_conn | |||||
| frontend_conn = f_conn | |||||
| connection.connect() | |||||
| read_buffer = str() | |||||
| while 1: | |||||
| try: | |||||
| read_buffer = read_buffer + connection.get() | |||||
| except RuntimeError: # socket broke | |||||
| print "socket has broken on watcher, restarting component..." | |||||
| 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] | |||||
| if chan != WATCHER_CHAN: # if we're getting a msg from another channel, ignore it | |||||
| continue | |||||
| msg = ' '.join(line[3:])[1:] | |||||
| rc = RC(msg) # create a new RC object to store this change's data | |||||
| rc.parse() | |||||
| check(rc) | |||||
| if line[0] == "PING": # If we are pinged, pong back to the server | |||||
| connection.send("PONG %s" % line[1]) | |||||
| if line[1] == "376": # Join the recent changes channel when we've finished starting up | |||||
| connection.join(WATCHER_CHAN) | |||||
| def report(msg, chans): | |||||
| """send a message to a list of report channels on our front-end server""" | |||||
| for chan in chans: | |||||
| frontend_conn.say(chan, msg) | |||||
| def check(rc): | |||||
| """check to see if """ | |||||
| page_name = rc.page.lower() | |||||
| pretty_msg = rc.pretty() | |||||
| if "!earwigbot" in rc.msg.lower(): | |||||
| report(pretty_msg, chans=BOT_CHANS) | |||||
| if re.match("wikipedia( talk)?:(wikiproject )?articles for creation", page_name): | |||||
| report(pretty_msg, chans=AFC_CHANS) | |||||
| elif re.match("wikipedia( talk)?:files for upload", page_name): | |||||
| report(pretty_msg, chans=AFC_CHANS) | |||||
| elif page_name.startswith("template:afc submission"): | |||||
| report(pretty_msg, chans=AFC_CHANS) | |||||
| if rc.flags == "delete" and re.match("deleted \"\[\[wikipedia( talk)?:(wikiproject )?articles for creation", rc.comment.lower()): | |||||
| report(pretty_msg, chans=AFC_CHANS) | |||||
| @@ -1,14 +0,0 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| from subprocess import * | |||||
| try: | |||||
| from config.secure_config import * | |||||
| except ImportError: | |||||
| print "Can't find a secure_config file!" | |||||
| print "Make sure you have configured the bot by moving 'config/secure_config.py.default' to 'config/secure_config.py' and by filling out the information inside." | |||||
| exit() | |||||
| while 1: | |||||
| cmd = ['python', 'bot.py'] | |||||
| call(cmd) | |||||