Initial checkin
authorStefan Hajnoczi <stefanha@gmail.com>
Sun, 7 Jun 2009 09:41:45 +0000 (10:41 +0100)
committerStefan Hajnoczi <stefanha@gmail.com>
Sun, 7 Jun 2009 09:41:45 +0000 (10:41 +0100)
cmds.py [new file with mode: 0644]
config.py [new file with mode: 0644]
gpxebot.py [new file with mode: 0755]
utils.py [new file with mode: 0644]

diff --git a/cmds.py b/cmds.py
new file mode 100644 (file)
index 0000000..2914442
--- /dev/null
+++ b/cmds.py
@@ -0,0 +1,138 @@
+import re
+import time
+
+import config
+import utils
+import errcode
+
+logs = {}
+commands = {}
+op_commands = {}
+op_members = [config.NICK]
+aliases = {}
+
+def readrc_command(target=None, replyto=None, words=None):
+    f = open(config.RCFILE).readlines()
+    cmd = []
+    for line in f:
+        response = do_command(config.NICK, config.NICK, config.NICK, line.split())
+        if isinstance(response, list):
+            cmd += response
+        elif response:
+            cmd.append(response)
+    return cmd
+op_commands['readrc'] = readrc_command
+
+def alias_command(_, replyto, words):
+    # alias stefanha stefanha@gmail.com
+    if len(words) == 2:
+        if words[1].lower() == 'list':
+            return [('PRIVMSG', (replyto, '%s    %s' % (user, email))) for (user, email) in aliases.items()]
+    if len(words) < 3:
+        return
+    user = words[1]
+    email = words[2]
+    aliases[user] = email
+op_commands['alias'] = alias_command
+
+def op_command(target, replyto, words):
+    # op stefanha
+    if len(words) < 2:
+        return
+    nick = words[1]
+    if not nick in op_members:
+        op_members.append(nick)
+op_commands['op'] = op_command
+
+def deop_command(target, replyto, words):
+    # deop stefanha
+    if len(words) < 2:
+        return
+    nick = words[1]
+    if nick in op_members:
+        op_members.remove(nick)
+op_commands['deop'] = deop_command
+
+ERRCODE_RE = re.compile(r'((?:0x)?[0-9a-fA-F]{8})')
+
+def errcode_command(_, replyto, words):
+    # errcode 0x12345678
+    msg = ' '.join(words)
+    m = ERRCODE_RE.search(msg)
+    if m:
+        try:
+            return 'PRIVMSG', (replyto, str(errcode.Errcode(int(m.group(1), 16))))
+        except ValueError:
+            pass
+commands['errcode'] = errcode_command
+commands['error'] = errcode_command
+
+def help_command(_, replyto, msg):
+    cmd = []
+    cmd.append(('PRIVMSG', (replyto, 'I look up gPXE error codes.  Message me like this:')))
+    cmd.append(('PRIVMSG', (replyto, 'errcode 0x12345678  OR  Error 0x12345678')))
+    return cmd
+commands['help'] = help_command
+
+def log_command(target, replyto, words):
+    if len(words) < 2:
+        return
+    channel = target
+    if len(words) >= 3:
+        channel = words[2].startswith('#') and words[2] or target
+    if words[1] == 'start':
+        if channel in logs:
+            return 'PRIVMSG', (replyto, 'Logging %s already started' % channel)
+        cmd = []
+        if channel.startswith('#') and channel != target:
+            cmd.append(('CMD', ('JOIN %s' % channel,)))
+        name = utils.make_log(channel)
+        f = open(name, 'w')
+        logs[channel] = (name, f)
+        cmd.append(('PRIVMSG', (replyto, 'Start logging %s...' % channel)))
+        return cmd
+
+    elif words[1] == 'stop':
+        if not channel in logs:
+            return 'PRIVMSG', (replyto, 'Logging %s not started yet' % channel)
+        (name, f) = logs.pop(channel)
+        f.close()
+
+        to_address = None
+        if len(words) >= 3 and words[-1] != channel:
+            to_address = words[-1]
+        if to_address:
+            unrecognized = utils.email_log(to_address, channel, name, aliases)
+            cmd = [('PRIVMSG', (replyto, 'Unrecognized user %s' % user)) for user in unrecognized]
+        return [('PRIVMSG', (replyto, 'Stop logging %s.  Saved log file (%s)' % (channel, name)))] + cmd
+
+    elif words[1] == 'list':
+        return 'PRIVMSG', (replyto, 'Logging: %s' % ','.join(logs.keys()))
+op_commands['log'] = log_command
+
+def join_command(target, replyto, words):
+    index = words.index('join') + 1
+    if index < len(words):
+        channel = words[index]
+        return 'CMD', ('JOIN %s' % (channel.startswith('#') and channel or '#' + channel),)
+op_commands['join'] = join_command
+
+def privmsg_command(target, replyto, words):
+    if len(words) < 3:
+        return
+    return ('PRIVMSG', (words[1], ' '.join(words[2:])))
+op_commands['privmsg'] = privmsg_command
+
+def restart_command(target, replyto, words):
+    return ('RESTART', ())
+op_commands['restart'] = restart_command
+
+def do_command(who, target, replyto, words):
+    if not words:
+        return
+    command = words[0].lower()
+
+    if command in commands:
+        return commands[command](target, replyto, words)
+    if utils.nick_from_mask(who) in op_members and command in op_commands:
+        return op_commands[command](target, replyto, words)
diff --git a/config.py b/config.py
new file mode 100644 (file)
index 0000000..5c20682
--- /dev/null
+++ b/config.py
@@ -0,0 +1,11 @@
+HOST = 'irc.freenode.net'
+PORT = 6667
+NICK = 'gpxebot'
+IDENT = 'gpxebot'
+REALNAME = 'gPXE bot'
+LOG_DIR = '/home/stefanha/logs'
+MTA = 'localhost'
+FROM_ADDRESS = 'gpxebot <stefanha@gmail.com>'
+RCFILE = 'gpxebotrc'
+FIFO = 'cmdfifo'
+DEBUG = False
diff --git a/gpxebot.py b/gpxebot.py
new file mode 100755 (executable)
index 0000000..20098c3
--- /dev/null
@@ -0,0 +1,165 @@
+#!/usr/bin/env python
+# Copyright (C) 2008 Stefan Hajnoczi <stefanha@gmail.com>.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+import select
+import socket
+import sys
+import time
+import os
+
+import config
+import cmds
+import utils
+
+NO_ARGS = -1
+
+handlers = {}
+restart  = False
+
+def autojoin():
+    del handlers['376']
+    do_response(cmds.readrc_command())
+
+def ping(_, arg):
+    cmd('PONG %s' % arg)
+
+def do_response(response):
+    global restart
+
+    # it should be in the format (command, args) 
+    # or [(command, args), (command, args)...]
+    # args is a tuple
+    if not response:
+        return
+    if isinstance(response, list):
+        [do_response(r) for r in response]
+        return
+    (command, args) = response
+    if command == 'PRIVMSG':
+        pmsg(*args)
+    elif command == 'CMD':
+        cmd(*args)
+    elif command == 'RESTART':
+        restart = True
+
+def privmsg(_, target, msg):
+    utils.do_log(cmds.logs, target, who, msg)
+
+    words = msg.split()
+    if target.startswith('#'):
+        replyto = target
+        if not config.NICK in words[0]:
+            return
+        words.pop(0)
+    elif target == config.NICK:
+        replyto = utils.nick_from_mask(who)
+
+    do_response(cmds.do_command(who, target, replyto, words))
+
+def add_handler(command, handler, nargs):
+    handlers[command] = (handler, nargs)
+
+def cmd(msg):
+    utils.dbg('WRITE ' + msg)
+    sock.sendall('%s\r\n' % msg)
+
+def pmsg(target, msg):
+    utils.do_log(cmds.logs, target, config.NICK, msg)
+    cmd('PRIVMSG %s :%s' % (target, msg))
+
+def dispatch(args):
+    command = args[0]
+    if command in handlers:
+        h = handlers[command]
+        if h[1] == NO_ARGS:
+            h[0]()
+        elif len(args) == h[1]:
+            h[0](*args)
+
+def parse(line):
+    if line[0] == ':':
+        who, line = line.split(None, 1)
+        who = who[1:]
+    else:
+        who = None
+    args = []
+    while line and line[0] != ':':
+        fields = line.split(None, 1)
+        if len(fields) == 1:
+            fields.append(None)
+        arg, line = fields
+        args.append(arg)
+    if line:
+        if line[0] == ':':
+            args.append(line[1:])
+        else:
+            args.append(line)
+    return who, args
+
+add_handler('376', autojoin, NO_ARGS)
+add_handler('PING', ping, 2)
+add_handler('PRIVMSG', privmsg, 3)
+
+if len(sys.argv) == 3 and sys.argv[1] == '--socket-fd':
+    # Restart with existing IRC session
+    fd = int(sys.argv[2])
+    sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
+    os.close(fd)
+    autojoin()
+else:
+    # Connect to server and start new IRC session
+    sock = socket.socket()
+    sock.connect((config.HOST, config.PORT))
+    cmd('NICK %s' % config.NICK)
+    cmd('USER %s none none :%s' % (config.IDENT, config.REALNAME))
+
+# Try to create command FIFO
+try:
+    os.mkfifo(config.FIFO)
+except OSError:
+    pass
+
+# Opening a fifo for reading with no writers normally blocks, but opening
+# read/write will not block on Linux.
+cmdfifo = open(config.FIFO, 'r+')
+
+sockbuf = ''
+while not restart:
+    rlist, _, xlist = select.select([sock, cmdfifo], [], [sock])
+
+    if sock in rlist or sock in xlist:
+        r = sock.recv(4096)
+        if not r:
+            break
+        sockbuf += r
+
+        while sockbuf.find('\r\n') != -1:
+            line, sockbuf = sockbuf.split('\r\n', 1)
+            if not line:
+                continue
+            utils.dbg('READ ' + line)
+            who, args = parse(line)
+            dispatch(args)
+
+    if cmdfifo in rlist:
+        words = cmdfifo.readline().strip().split()
+        do_response(cmds.do_command(config.NICK, config.NICK, config.NICK, words))
+
+cmdfifo.close()
+
+# Restart with existing IRC session
+fd = sock.fileno()
+os.execl(sys.argv[0], sys.argv[0], '--socket-fd', str(fd))
diff --git a/utils.py b/utils.py
new file mode 100644 (file)
index 0000000..2f83975
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,52 @@
+import smtplib
+import tempfile
+import time
+import sys
+
+import config
+
+def dbg(msg):
+    if config.DEBUG:
+        sys.stderr.write(msg + '\n')
+
+def nick_from_mask(mask):
+    return (mask.find('!') > -1 and mask.split('!', 1)[0]) or mask
+
+def email_log(to_address, channel, logfile, aliases):
+    email_addresses = []
+    unrecognized = []
+    for a in to_address.split(','):
+        if '@' in a:
+            email_addresses.append(a)
+        elif a in aliases:
+            email_addresses.append(aliases[a])
+        else:
+            unrecognized.append(a)
+    if email_addresses:
+        header = '''From: %(from_address)s
+To: %(to_address)s
+Subject: irc %(channel)s log
+
+''' % {'from_address': config.FROM_ADDRESS,
+            'to_address': ','.join(email_addresses),
+            'channel': channel,
+            }
+        msg = header + open(logfile).read()
+        s = smtplib.SMTP(config.MTA)
+        s.sendmail(config.FROM_ADDRESS, email_addresses, msg)
+    return unrecognized
+
+def get_time():
+    return '%s-%02d-%02d %02d:%02d' % time.gmtime()[:5]
+
+def make_log(channel):
+    (year, month, day, hour, minute) = time.gmtime()[:5]
+    return tempfile.mkstemp(
+            suffix='.log',
+            prefix='%s-%s%02d%02d.%02d%02d-' % (channel, year, month, day, hour, minute),
+            dir=config.LOG_DIR)[1]
+
+def do_log(logs, target, nick, msg):
+    if target in logs:
+        (name, f) = logs[target]
+        f.write('%s <%s> %s\n' % (get_time(), nick_from_mask(nick), msg))