pvl/irk.py
author Tero Marttila <terom@paivola.fi>
Sun, 13 Jan 2013 03:37:30 +0200
changeset 130 4f8c465706be
parent 128 42d4bd708373
child 134 99a8987fc424
permissions -rw-r--r--
pvl.irk: typofix
"""
    Irker client.
"""

import pvl.syslog.file # for stdin
import pvl.socket # for tcp

import optparse, sys

import logging; log = logging.getLogger('pvl.irk')

import json

def parser (parser, connect='tcp://localhost/', target=None) :
    """
        Optparse option group.
    """

    irker = optparse.OptionGroup(parser, 'Irker output')
    
    irker.add_option('--irker', metavar='URL',  default=connect,
            help="Irker daemon URL")

    irker.add_option('--irker-notice',          action='store_true',
            help="Use irker NOTICE")

    irker.add_option('--irker-part',            action='store_true',
            help="Use irker PART")

    return irker

def apply (options) :
    """
        Return Irker (XXX: target) from options.
    """
    
    # None -> stdout
    return Irker(options.irker, options) # options.irker_*

class Irk (object) :
    """
        Irker JSON connection speaks JSON over a stream.

        TODO: timeouts?
    """

    PORT = 6659

    @classmethod
    def connect (cls, url) :
        """
            Connect to given URL string, or None -> stdout
        """

        if not url :
            # no read
            return cls(pvl.syslog.file.File(sys.stdout), recv=False)

        else :
            sock = pvl.socket.connect(url, port=cls.PORT)

            # just to make things a bit more exciting... and we really don't want to be blocking on our output..
            sock.setblocking(False)
            
            return cls(
                    pvl.socket.WriteStream(sock, buffer=None),
                    pvl.socket.ReadStream(sock)
            )

    def __init__ (self, send, recv=None) :
        """
            Use given file-like object (write, flush, fileno) for output.
        """

        self.send = send
        self.recv = recv
        
        log.debug("%s <-> %s", send, recv)

    def fileno (self) :
        """
            Return fd. Useful for detecting error conditions (connection lost).

            Only valid if self.recv is True.
        """

        return self.recv.fileno()

    def __call__ (self, **opts) :
        """
            Raises IOError on write errors.
        """

        log.debug("%s", opts)
        
        # write line + flush
        self.send(json.dumps(opts))
        
        # XXX: self.send.flush()

    def __iter__ (self) :
        """
            Yield JSON inputs from source.
        """

        if not self.recv :
            # never going to be anything
            return

        for line in self.recv :
            # XXX: error handling?
            yield json.loads(line)

class IrkerTarget (object) :
    """
        A channel on an Irk connection.
    """

    def __init__ (self, irker, target, notice=None, part=None) :
        self.irker = irker
        self.target = target

        self._notice = notice
        self._part = part
        
    def join (self) :
        log.info("%s", self)
        self.irker(to=str(self), privmsg='')

    def privmsg (self, *args) :
        for arg in args :
            log.info("%s: %s", self, arg)
            self.irker(to=str(self), privmsg=arg)

    def notice (self, *args) :
        for arg in args :
            log.info("%s: %s", self, arg)
            self.irker(to=str(self), notice=arg)

    def part (self, msg='') :
        log.info("%s: %s", self, msg)

        if self._part :
            self.irker(to=str(self), part=msg)
        else :
            log.warn("%s: no --irker-part", self)

    def __call__ (self, *args) :
        # default msg policy
        if self._notice :
            return self.notice(*args)
        else :
            return self.privmsg(*args)

    def __str__ (self) :
        return self.target

class Irker (object) :
    """
        Reconnecting Irk.
    """

    def __init__ (self, url=None, options=None) :
        """
            url         - irker to connect to
            options     - irker_* configs
        """

        self.url = url
        self.targets = {}
        self.options = options
        
        self.connect()
    
    def connect (self) :
        """
            Connect, and fix up our targets.
        """

        self.irk = Irk.connect(self.url)

        # rejoin
        for target in self.targets.itervalues() :
            target.join()
    
    def __call__ (self, **opts) :
        """
            Send on current irker connection.

            TODO: handle errors and reconnect?
        """

        self.irk(**opts)

    def __getitem__ (self, target) :
        """
            Bind to given target URL, returning an IrkerTarget for sending messages.
        """

        if target not in self.targets :
            self.targets[target] = IrkerTarget(self, target, 
                    notice  = self.options and self.options.irker_notice,
                    part    = self.options and self.options.irker_part,
            )
            self.targets[target].join()
            
        return self.targets[target]

    def __delitem__ (self, target) :
        """
            Unbind given target URL.
        """

        target = self.targets.pop(target)
        target.part()

    def __iter__ (self) :
        return iter(self.targets)