irc2.py   [plain text]


# -*- coding: Latin-1 -*-

from twisted.protocols import irc
from twisted.internet import defer
from twisted.python.log import msg as logmsg

class IRCError(Exception):
    pass

class NoSuchChannel(IRCError):
    pass

class NoSuchNickname(IRCError):
    pass

class NicknameError(IRCError):
    pass

class ErroneousNickname(NicknameError):
    pass

class NicknameMissing(NicknameError):
    pass

class NicknameCollision(NicknameError):
    """A nickname is already in use on some server on the network
    """

class NicknameInUse(NicknameError):
    """A nickname is already in use on this server
    """

def log(msg, protocol="irc", channel="info", level="protocol"):
    logmsg(msg, protocol=protocol, channel=channel, level=level)

class AdvancedClient(irc.IRCClient):
    _pendingQuit = None
    
    def __init__(self):
        self._pending = []
    
    def _makeCommand(self, name, additional=None, unknownHandler=None):
        self._pending.append((name, defer.Deferred(), additional, unknownHandler))
        return self._pending[-1][1]

    def _getHandler(self):
        if not self._pending:
            raise ValueError("No current command")
        f = self._pending[0][3]
        if f is None:
            raise ValueError("Current unknown-handler is None")
        return f

    def _getAdd(self, command=None, possible=()):
        if not self._pending:
            raise ValueError("No current command")
        if command is not None:
            if self._pending[0][0] == command:
                return self._pending[0][2]
            else:
                raise ValueError("Current command not %r" % (command,))
        else:
            if self._pending[0][0] in possible:
                return self._pending[0][2]
            else:
                raise ValueError("Current command not in %r" % (command,))

    def _getDeferred(self, command=None, possible=()):
        # We can prevent offset errors induced by the receipt of unexpected
        # messages in addition to those we do expect, but I see no way to
        # prevent those induced by the omission of expected messages or the
        # receipt of expected messages in an unexpected order (that is, any
        # order other than that in which we send the associated commands). 
        # Therefore, if a server fails to respond in the manner which we
        # expected, client code using this class will likely become hung and
        # remain so forever.
        if not self._pending:
            raise ValueError("No commands pending")
        if command is not None:
            if self._pending[0][0] == command:
                return self._pending.pop(0)[1]
            else:
                raise ValueError("Next command is not %r" % (command,))
        else:
            if self._pending[0][0] in possible:
                return self._pending.pop(0)[1]
            else:
                raise ValueError("Next command not in %r" % (command,))

    def _callback(self, command=None, possible=(), args=None, additional=False):
        if additional:
            try:
                args = self._getAdd(command)
            except ValueError:
                log("Unexpected callback for %r" % (command,))
                return
        try:
            d = self._getDeferred(command=command, possible=possible)
        except ValueError, e:
            log("Unexpected callback: %s" % (e,))
        else:
            d.callback(args)

    def _errback(self, command=None, possible=(), failure=None):
        try:
            d = self._getDeferred(command=command, possible=possible)
        except ValueError, e:
            log("Unexpected errback: %s" % (e,))
        else:
            d.errback(failure)

    def connectionLost(self, reason):
        self._callback("QUIT", args=self)

    def irc_unknown(self, prefix, command, params):
        try:
            f = self._getHandler()
        except ValueError:
            irc.IRCClient.irc_unknown(self, prefix, command, params)
        else:
            f(prefix, command, params)

    def quit(self, message=''):
        """Quit the network
        
        @rtype: C{Deferred}
        @return: A deferred whose callback is invoked when the quit succeeds
        (the connection is lost).
        """
        irc.IRCClient.quit(self, message)
        return self._makeCommand("QUIT")
    
    def join(self, channel, key=None):
        """Join a channel
        
        @rtype: C{Deferred}
        @return: A deferred whose callback is invoked when the join succeeds or
        whose errback is invoked if it fails.
        """
        irc.IRCClient.join(self, channel, key)
        return self._makeCommand("JOIN")
    
    def joined(self, channel):
        self._callback("JOIN", args=self)

    def leave(self, channel, reason=None):
        """Leave a channel
        
        @return: A deferred whose callback is invoked when the part succeeds or
        whose errback is invoked if it fails.
        """
        irc.IRCClient.leave(self, channel, reason)
        return self._makeCommand("PART")
    part = leave
    
    def left(self, channel):
        self._callback("PART", args=self)

    def irc_ERR_NOSUCHCHANNEL(self, prefix, params):
        channel = params[1]
        try:
            # I can do better than this.  Give me time.
            d = self._getDeferred(possible=("JOIN", "PART", "TOPIC"))
        except ValueError:
            log("Unexpected no-such-channel")
        else:
            d.errback(NoSuchChannel(channel))

    def names(self, *channels):
        """List names of users visible to this user.
        
        @type channels: C{str}
        @param channels: The channels for which to request names.
        
        @rtype: C{Deferred} of C{dict} mapping C{str} to C{list} of C{str}
        @return: A mapping of channel names to lists of users in those
        channels.
        """
        self.sendLine("NAMES " + " ".join(channels))
        return self._makeCommand("NAMES", {})
    
    def irc_RPL_NAMREPLY(self, prefix, params):
        channel = params[2]
        names = params[3].split()
        self._getAdd("NAMES").setdefault(channel, []).extend(names)
    
    def irc_RPL_ENDOFNAMES(self, prefix, params):
        self._callback("NAMES", additional=True)

    def who(self, name, op=False):
        """List names of users who match a particular pattern.
        
        @type name: C{str}
        @param name: The pattern against which to match.
        
        @type op: C{bool}
        @param op: If true, only request operators who match the given
        pattern.
        
        @rtype: C{Deferred} of C{list} of C{tuples}
        @return: A list of 8-tuples consisting of
        
            channel, user, host, server, nick, flags, hopcount, real name
        
        all of which are strings, except hopcount, which is an integer.
        """
        if op:
            self.sendLine("WHO " + name)
        else:
            self.sendLine("WHO " + name + " o")
        return self._makeCommand("WHO", [])
    
    def irc_RPL_WHOREPLY(self, prefix, params):
        params = params[2:]
        params[-1:] = params[-1].split(None, 1)
        params[-2] = int(params[-2])
        self._getAdd("WHO").append(tuple(params))
    
    def irc_RPL_ENDOFWHO(self, prefix, params):
        self._callback("WHO", additional=True)

    def whois(self, user):
        """Retrieve information about the specified user.
        
        @type user: C{str}
        @param user: The user about whom to retrieve information.
        
        @rtype: C{Deferred} of C{dict} mapping information tags to
        received information.  The items of this dict are as follows:
        
            'user':      (ident, hostmask, real name)
            'server':    (server, server info)
            'operator':  True
            'idle':      idle time, in seconds, as an integer
            'channels':  list of strings indicating the channels to which
                            the user belongs
        
        Other keys may exist if the server responds with non-standard
        information.
        """
        self.sendLine("WHOIS " + user)
        return self._makeCommand("WHOIS", {}, unknownHandler=self._whoisHandler)

    def _whoisHandler(self, prefix, command, params):
        try:
            command = int(command)
        except ValueError:
            pass
        self._getAdd("WHOIS").setdefault(command, []).append(params)
    
    def irc_RPL_WHOISUSER(self, prefix, params):
        self._getAdd("WHOIS")['user'] = params[2], params[3], params[5]
    
    def irc_RPL_WHOISSERVER(self, prefix, params):
        self._getAdd("WHOIS")['server'] = params[2], params[3]
    
    def irc_RPL_WHOISOPERATOR(self, prefix, params):
        self._getAdd("WHOIS")['operator'] = True
    
    def irc_RPL_WHOISIDLE(self, prefix, params):
        self._getAdd("WHOIS")['idle'] = int(params[2])
    
    def irc_RPL_WHOISCHANNELS(self, prefix, params):
        self._getAdd("WHOIS").setdefault('channels', []).append(params[1])
    
    def irc_RPL_ENDOFWHOIS(self, prefix, params):
        self._callback("WHOIS", additional=True)

    def irc_ERR_NOSUCHNICK(self, prefix, params):
        self._errback(possible=("WHOIS",), failure=NoSuchNickname())

    def topic(self, channel, topic=None):
        """Retrieve the topic for the given channel.
        
        @type channel C{str}
        @param channel: The channel for which to retrieve the topic.
        
        @type topic: C{str}
        @param topic: The topic to set, or None to just retrieve the
        current topic.
        
        @rtype: C{Deferred} of C{str} or C{None}
        @return: A Deferred of the topic, or of None if no topic is set.
        """
        irc.IRCClient.topic(self, channel, topic)
        return self._makeCommand("TOPIC")
    
    def irc_RPL_TOPIC(self, prefix, params):
        channel, topic = params[1:]
        self._callback("TOPIC", args=topic)
    
    def irc_RPL_NOTOPIC(self, prefix, params):
        channel = params[1]
        self._callback("TOPIC", args=None)

    def nick(self, newnick):
        """Change this user's nickname.
        
        @type newnick: C{str}

        @return: C{Deferred} that fires with the nick when it has been
        changed, or whose errback is called if the nick name cannot be
        changed.

        @raise NicknameError: Raised asynchronously if the server reports
        an error in changing the nickname.  Any one of the four
        NicknameError subclasses may be raised: NicknameMissing,
        NicknameInUse, ErroneousNickname, or NicknameCollision.
        """
        self.sendLine("NICK " + newnick)
        return self._makeCommand("NICK")

    def nickChanged(self, nick):
        irc.IRCClient.nickChanged(self, nick)
        self._callback("NICK", args=nick)

    def irc_ERR_NONICKNAMEGIVEN(self, prefix, params):
        self._errback("NICK", failure=NicknameMissing())
    
    def irc_ERR_NICKNAMEINUSE(self, prefix, params):
        self._errback("NICK", failure=NicknameInUse())
    
    def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
        self._errback("NICK", failure=ErroneousNickname())
    
    def irc_ERR_NICKCOLLISION(self, prefix, params):
        self._errback("NICK", failure=NicknameCollision())