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