--- a/pvl/irker/irc.py Tue Feb 19 19:28:40 2013 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,462 +0,0 @@
-"""
- IRC client, dispatching irker messages.
-"""
-
-from twisted.internet import reactor, interfaces, protocol, defer, error
-from twisted.words.protocols import irc
-
-from twisted.internet import endpoints
-
-from twisted.python import log
-
-PORT = 6667
-
-def url2endpoint (reactor, url) :
- """
- Turn given urlparse URL into an endpoint.
-
- Raises KeyError on unknown scheme.
- """
-
- SCHEMES = {
- 'irc': lambda : endpoints.TCP4ClientEndpoint(reactor, url.hostname, url.port or PORT),
- }
-
- return SCHEMES[url.scheme]()
-
-def normalize (name) :
- """
- Normalize a channel/nickname for comparisons in IRC.
- """
-
- return name.lower()
-
-class IRCError (Exception) :
- """
- A handled protocol error.
- """
-
- pass
-
-class IRCChannel (object) :
- """
- A joined channel on an IRC server.
- """
-
- ENCODING = 'utf-8'
-
- def __init__ (self, client, channel, encoding=ENCODING) :
- self.client = client
- self.channel = channel
-
- self.encoding = encoding
-
- # TODO: separate join/part state
- #self.joining = self.parting = None
-
- def encode (self, unicode) :
- if unicode :
- return unicode.encode(self.encoding)
- else :
- return None
-
- def privmsg (self, *msgs) :
- for msg in msgs :
- # XXX: encode
- self.client.msg(self.channel, self.encode(msg))
-
- def notice (self, *msgs) :
- for msg in msgs :
- self.client.notice(self.channel, self.encode(msg))
-
- def part (self, msg=None) :
- """
- Remove channel from our list of channels.
- """
-
- # send the PART
- self.client.leave(self.channel, self.encode(msg))
-
- # ...and then immediately forget the channel, in case we rejoin before getting the part back
- # TODO: self.joining/parting
- self.client._close_channel(self.channel)
-
- def errback (self, failure) :
- """
- Fail any pending requests.
- """
-
- log.msg('IRCChannel.errback', self, failure)
-
- def __str__ (self) :
- return self.client.url(self.channel)
-
-class IRCClient (irc.IRCClient) :
- """
- A connection to an IRC server with a specific, requested nickname.
-
- Joins to channels.
- """
-
- performLogin = False
-
- def __init__ (self, factory) :
- self.factory = factory
-
- self.nickname = None
- self.hostname = None
-
- self._registering = None
- self._channels = { }
-
- # TODO: smarter/configurable queueing?
- self.lineRate = 1.0
-
- def connectionMade (self) :
- self.hostname = self.transport.getPeer().host
- self.transport.logPrefix = self.logPrefix
-
- log.msg("connectionMade", self, self.transport)
- irc.IRCClient.connectionMade(self)
-
- def sendLine (self, line) :
- irc.IRCClient.sendLine(self, line)
-
- log.msg(">>>", line)
-
- def lineReceived (self, line) :
- log.msg("<<<", line)
-
- irc.IRCClient.lineReceived(self, line)
-
- ## Register
- def register (self, nickname, username=None, password=None) :
- """
- Register to the server, choosing a nickname based on the given nickname.
-
- Returns a Deferred that callbacks with our actual nickname once we have registered, or errbacks with an IRCError.
- """
-
- if self._registering :
- raise Exception("register: already registering")
-
- self.username = username
- self.password = password
-
- log.msg("register", nickname)
- irc.IRCClient.register(self, nickname)
-
- # defer
- d = self._registering = defer.Deferred()
-
- return d
-
- # irc_ERR_NICKNAMEINUSE
- # alterCollidedNick
- # irc_ERR_ERRONEUSNICKNAME
-
- def irc_ERR_PASSWDMISMATCH (self, prefix, params) :
- err = IRCError('ERR_PASSWDMISMATCH')
- log.err(err)
- self._registering.errback(err)
-
- def irc_RPL_WELCOME (self, prefix, params) :
- self.hostname = prefix
- irc.IRCClient.irc_RPL_WELCOME(self, prefix, params)
-
- def signedOn (self) :
- log.msg("signedOn", self.nickname)
- irc.IRCClient.signedOn(self)
-
- # defer
- d = self._registering
-
- if not d :
- raise Exception("signedOn: not registering?")
-
- self._registering = None
-
- d.callback(self.nickname)
-
- ## Channels
- def join (self, channel, key=None) :
- """
- Join the given channel.
-
- Returns a deferred that callbacks with the IRCChannel once joined, or errbacks.
- """
-
- irc.IRCClient.join(self, channel, key=key)
-
- d = self._channels[normalize(channel)] = defer.Deferred()
-
- return d
-
- # ERR_CHANNELISFULL
- # ERR_INVITEONLYCHAN
- # ERR_BANNEDFROMCHAN
- # ERR_BADCHANNELKEY
-
- def _close_channel (self, channel) :
- """
- Remove channel from our list of channels.
-
- TODO: purge queued messages for channel?
- """
-
- del self._channels[normalize(channel)]
-
- def left (self, channel) :
- if normalize(channel) in self._channels :
- if isinstance(self._channels[normalize(channel)], defer.Deferred) :
- log.msg('IRCClient.left: part during join:', channel)
-
- else :
- log.msg('IRCClient.left: unexpected part:', channel)
- self._close_channel(channel)
-
- # XXX: assume this is: send PART, send JOIN, receive PART, receive JOIN
- #self._close_channel(channel)
- else :
- log.msg("IRCClient.left: parted channel:", channel)
-
- def kickedFrom (self, channel, kicker, message) :
- log.msg('IRCClient.kicked', channel, kicker, message)
-
- self._close_channel(channel)
-
- def joined (self, channel) :
- """
- Have joined given channel.
- """
-
- lookup = normalize(channel)
-
- d = self._channels[lookup]
- channel = self._channels[lookup] = IRCChannel(self, channel)
- d.callback(channel)
-
- @defer.inlineCallbacks
- def channel (self, channel, key=None) :
- """
- Defer a joined IRCChannel.
- """
-
- lookup = normalize(channel)
-
- log.msg('IRCClient.channel', lookup, channel)
-
- if lookup not in self._channels :
- channel = yield self.join(channel, key)
- else :
- # wait or get
- yield self._channels[lookup]
-
- channel = self._channels[lookup]
-
- log.msg('IRCClient.channel', lookup, channel)
-
- defer.returnValue(channel)
-
- ##
- def irc_ERR_CANNOTSENDTOCHAN (self, prefix, params) :
- nick, channel, error = params
-
- log.err(IRCError(channel, error))
-
- ## Quit
- def irc_ERROR (self, prefix, params) :
- msg, = params
- error = IRCError(None, msg)
-
- log.err(error)
-
- if self._registering :
- self._registering.errback(error)
- self._registering = None
-
- def connectionLost (self, reason) :
- irc.IRCClient.connectionLost(self, reason)
- log.err(reason)
-
- if self._registering :
- self._registering.errback(reason)
- self._registering = None
-
- # unregister channels
- for channel in self._channels :
- # errback Deferred or IRCChannel
- self._channels[channel].errback(reason)
-
- self._channels = { }
-
- # unregister client
- self.factory.clientLost(self)
-
- ## Logging
- def url (self, target=None) :
- """
- Format as URL.
- """
-
- if not self.transport : return 'IRC'
-
- # XXX: no isinstance() support
- if interfaces.ITCPTransport.providedBy(self.transport) :
- scheme = 'irc'
- else :
- # TODO: ssl?
- scheme = None
-
- peer = self.transport.getPeer()
-
- if peer.port == PORT :
- peer = "{irc.hostname}".format(irc=self, peer=peer)
- else :
- peer = "{irc.hostname}:{peer.port}".format(irc=self, peer=peer)
-
- if target :
- path = str(target)
- else :
- path = None
-
- return ''.join(part for part in (
- scheme, '://' if scheme else None,
- self.nickname, '@' if self.nickname else None,
- peer,
- '/' if path else None, path
- ) if part)
-
- __str__ = url
- logPrefix = url
-
-class IRCFactory (protocol.ClientFactory) :
- """
- Manage Clients and Targets
- """
-
- NICKNAME = 'irker'
-
- def __init__ (self, nickname=NICKNAME, username=None) :
- # default nickname
- self.nickname = nickname
- self.username = username
-
- # (scheme, host, port, nick) -> IRCClient
- self.clients = {}
-
- def buildProtocol (self, addr) :
- return IRCClient(self)
-
- def clientLost (self, client) :
- """
- Given IRCClient is no more.
- """
-
- log.msg("IRCFactory.clientLost", client)
-
- # remove from our clients
- self.clients = dict((k, c) for k, c in self.clients.iteritems() if c != client)
-
- @defer.inlineCallbacks
- def connect (self, url) :
- """
- Defer a connected, registered Client for given URL.
- """
-
- endpoint = url2endpoint(reactor, url)
-
- log.msg('IRCFactory.connect', url, ':', endpoint)
-
- # connect
- try :
- client = yield endpoint.connect(self)
-
- except error.ConnectError as ex :
- log.err(ex, ': '.join(str(x) for x in ('IRCFactory.connect', url, endpoint)))
- raise
-
- else :
- log.msg('IRCFactory.connect', url, ':', endpoint, ':', client)
-
- # register
- try :
- nickname = yield client.register(url.username or self.nickname,
- username = self.username,
- password = url.password,
- )
-
- except Exception as ex :
- log.err("register", ex)
- raise
-
- log.msg('IRCFactory.connect', url, ':', endpoint, ':', client, ':', nickname)
-
- # okay!
- defer.returnValue(client)
-
- @defer.inlineCallbacks
- def client (self, url) :
- """
- Return IRCClient for given URL.
- """
-
- lookup = (url.scheme, url.hostname, url.port, url.username)
-
- if lookup not in self.clients :
- # deferred for connect
- connect = self.clients[lookup] = self.connect(url)
-
- try :
- # wait on deferred, and then store IRCClient
- self.clients[lookup] = yield connect
-
- except Exception as ex :
- # failed, remove the attempted connect
- del self.clients[lookup]
- raise
-
- else :
- # wait for result, if deferred
- # XXX: this yields None, since the first inlineCallbacks yielding on the deferred returns None in its callback
- yield self.clients[lookup]
-
- # connected client
- client = self.clients.get(lookup)
-
- if client :
- log.msg('IRCFactory.client', url, ":", client)
-
- defer.returnValue(client)
- else :
- log.msg('IRCFactory.client', url, ": client connect failed")
-
- # XXX: get failure from first yield's errback... except inlineCallbacks drops it and goes to callback with None <_<
- raise Exception("Client connect failed")
-
- @defer.inlineCallbacks
- def target (self, url) :
- """
- Return IRCChannel for given URL.
- """
-
- client = yield self.client(url)
-
- channel = '#' + url.path.lstrip('/')
- channel = yield client.channel(channel)
-
- log.msg('IRCFactory.target', url, ":", channel)
-
- defer.returnValue(channel)
-
- @defer.inlineCallbacks
- def privmsg (self, url, *msg) :
- """
- Dispatch given messages to given target.
- """
-
- target = yield self.target(url)
-
- log.msg('IRCFactory.privmsg', url, ":", target, ':', *msg)
-
- target.privmsg(*msg)