# -*- 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())
syntax highlighted by Code2HTML, v. 0.9.1