Skip to content
Snippets Groups Projects
irker.py 5.72 KiB
Newer Older
Eric S. Raymond's avatar
Eric S. Raymond committed
#!/usr/bin/env python
"""
irker - a simple IRC multiplexer daemon

Takes JSON objects of the form {'channel':<channel-url>, 'privmsg':<text>}
Eric S. Raymond's avatar
Eric S. Raymond committed
and relays messages to IRC channels.

Eric S. Raymond's avatar
Eric S. Raymond committed
Run this as a daemon in order to maintain stateful connections to IRC
Eric S. Raymond's avatar
Eric S. Raymond committed
servers; this will allow it to respond to server pings and minimize
join/leave traffic.
Eric S. Raymond's avatar
Eric S. Raymond committed

Requires Python 2.6.

Eric S. Raymond's avatar
Eric S. Raymond committed
TO-DO: some servers have a limit of 20 channels per server connection.
Eric S. Raymond's avatar
Eric S. Raymond committed
TO-DO: Register the port?
TO-DO: Multiple irkers could try to use the same nick
Eric S. Raymond's avatar
Eric S. Raymond committed
TO-DO: share connections among multiple sessions.
TO-DO: time out session instances as well as sockets.
Eric S. Raymond's avatar
Eric S. Raymond committed
"""
# These things might need tuning

HOST = "localhost"
PORT = 4747

TTL = (3 * 60 * 60)	# Connection time to live in seconds

# No user-serviceable parts below this line

import os, sys, json, irclib, exceptions, getopt, urlparse, time
Eric S. Raymond's avatar
Eric S. Raymond committed
import threading, Queue, SocketServer
Eric S. Raymond's avatar
Eric S. Raymond committed

class SessionException(exceptions.Exception):
    def __init__(self, message):
        exceptions.Exception.__init__(self)
        self.message = message

class Session():
    "IRC session and message queue processing."
    count = 0
Eric S. Raymond's avatar
Eric S. Raymond committed
    def __init__(self, irker, url):
        self.irker = irker
        self.url = url
        # Server connection setup
        parsed = urlparse.urlparse(url)
        host, sep, port = parsed.netloc.partition(':')
        if not port:
            port = 6667
        self.servername = host
        self.channel = parsed.path.lstrip('/')
        self.port = int(port)
Eric S. Raymond's avatar
Eric S. Raymond committed
        Session.count += 1
        # The consumer thread
        self.queue = Queue.Queue()
        self.thread = threading.Thread(target=self.dequeue)
        self.thread.daemon = True
        self.thread.start()
    def enqueue(self, message):
        "Enque a message for transmission."
        self.queue.put(message)
    def dequeue(self):
        "Try to ship pending messages from the queue."
        while True:
            # We want to be kind to the IRC servers and not hold unused
            # sockets open forever, so they have a time-to-live.  The
            # loop is coded this particular way so that we can drop
            # the actual server connection when its time-to-live
            # expires, then reconnect and resume transmission if the
            # queue fills up again.
            if not self.server:
                self.server = self.irker.allocate_server(self.servername,
                                                         self.port,
                                                         self.name())
                self.irker.debug(1, "TTL bump (connection) at %s" % time.asctime())
                self.last_active = time.time()
            elif self.queue.empty():
                if time.time() > self.last_active + TTL:
                    self.irker.debug(1, "timing out inactive connection at %s" % time.asctime())
                    self.server.part("#" + self.channel)
                    self.server = None
                    break
            else:
                message = self.queue.get()
                self.server.join("#" + self.channel)
                self.server.privmsg("#" + self.channel, message)
                self.last_active = time.time()
                self.irker.debug(1, "TTL bump (transmission) at %s" % time.asctime())
    def name(self):
        "Generate a unique name for this session."
Eric S. Raymond's avatar
Eric S. Raymond committed
        return "irker%03d" % Session.count
    def await(self):
Eric S. Raymond's avatar
Eric S. Raymond committed
        "Block until processing of all queued messages is done."
        self.queue.join()
Eric S. Raymond's avatar
Eric S. Raymond committed
class Irker:
    "Persistent IRC multiplexer."
Eric S. Raymond's avatar
Eric S. Raymond committed
    def __init__(self, debuglevel=0):
        self.debuglevel = debuglevel
        self.irc = irclib.IRC(debuglevel=self.debuglevel-1)
Eric S. Raymond's avatar
Eric S. Raymond committed
        thread = threading.Thread(target=self.irc.process_forever)
        self.irc._thread = thread
        thread.daemon = True
        thread.start()
        self.sessions = {}
Eric S. Raymond's avatar
Eric S. Raymond committed
    def logerr(self, errmsg):
        "Log a processing error."
        sys.stderr.write("irker: " + errmsg + "\n")
Eric S. Raymond's avatar
Eric S. Raymond committed
    def debug(self, level, errmsg):
        "Debugging information."
        if self.debuglevel >= level:
            sys.stderr.write("irker[%d]: %s\n" % (self.debuglevel, errmsg))
    def allocate_server(self, servername, port, nick):
        "Allocate a new server instance."
        newserver = self.irc.server()
        newserver.connect(servername, port, nick)
        return newserver
Eric S. Raymond's avatar
Eric S. Raymond committed
    def handle(self, line):
        "Perform a JSON relay request."
        try:
            request = json.loads(line.strip())
            if "channel" not in request or "privmsg" not in request:
Eric S. Raymond's avatar
Eric S. Raymond committed
                self.logerr("ill-formed reqest")
            else:
                channel = request['channel']
                message = request['privmsg']
Eric S. Raymond's avatar
Eric S. Raymond committed
                if channel not in self.sessions:
                    self.sessions[channel] = Session(self, channel)
                self.sessions[channel].enqueue(message)
        except ValueError:
            self.logerr("can't recognize JSON on input.")
    def terminate(self):
        "Ship all pending messages before terminating."
        for session in self.sessions.values():
            session.await()

class MyTCPHandler(SocketServer.StreamRequestHandler):
Eric S. Raymond's avatar
Eric S. Raymond committed
    def handle(self):
Eric S. Raymond's avatar
Eric S. Raymond committed
        while True:
            irker.handle(self.rfile.readline().strip())
Eric S. Raymond's avatar
Eric S. Raymond committed

if __name__ == '__main__':
Eric S. Raymond's avatar
Eric S. Raymond committed
    debuglevel = 0
Eric S. Raymond's avatar
Eric S. Raymond committed
    (options, arguments) = getopt.getopt(sys.argv[1:], "d:p:")
Eric S. Raymond's avatar
Eric S. Raymond committed
    for (opt, val) in options:
        if opt == '-d':
Eric S. Raymond's avatar
Eric S. Raymond committed
            debuglevel = int(val)
Eric S. Raymond's avatar
Eric S. Raymond committed
        elif opt == '-p':
            port = int(val)
Eric S. Raymond's avatar
Eric S. Raymond committed
    irker = Irker(debuglevel=debuglevel)
Eric S. Raymond's avatar
Eric S. Raymond committed
    server = SocketServer.TCPServer((host, port), MyTCPHandler)
    except KeyboardInterrupt: