--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/pvl.irker Fri Jan 11 17:13:11 2013 +0200
@@ -0,0 +1,117 @@
+#!/usr/bin/python
+
+from twisted.internet import reactor, endpoints, defer
+from twisted.python import usage, log
+
+import urlparse
+
+import pvl.irker.irk
+import pvl.irker.irc
+
+class Options (usage.Options) :
+ optParameters = [
+ [ 'irc-nickname', 'n', pvl.irker.irc.IRCFactory.NICKNAME, "Default IRC nickname" ],
+ ]
+
+ def __init__ (self) :
+ usage.Options.__init__(self)
+
+ self.listen = []
+ self.connect = []
+ self.target = []
+ self.privmsg = []
+
+ def opt_listen_tcp (self, listen) :
+ """
+ Twisted endpoint.
+ """
+
+ self.listen.append((endpoints.TCP4ServerEndpoint, (int(listen), )))
+
+ def opt_connect (self, connect) :
+ """
+ Connect to given target.
+ """
+
+ self.connect.append(urlparse.urlparse(connect))
+
+ def opt_target (self, target) :
+ """
+ Join given target.
+ """
+
+ self.target.append(urlparse.urlparse(target))
+
+ def opt_privmsg (self, privmsg) :
+ """
+ Send message to targets
+ """
+
+ self.privmsg.append(privmsg)
+
+@defer.inlineCallbacks
+def connect (irc, connect) :
+ """
+ Connect to given urls.
+ """
+
+ try :
+ clients = yield defer.gatherResults([irc.client(url) for url in connect])
+
+ except Exception as ex :
+ log.err(ex)
+ return
+
+ for client in clients :
+ log.msg('--connect', client)
+
+@defer.inlineCallbacks
+def target (irc, target, privmsg) :
+ """
+ Connect to given urls.
+ """
+
+ try :
+ targets = yield defer.gatherResults([irc.target(url) for url in target])
+
+ except Exception as ex :
+ log.err(ex)
+ return
+
+ for target in targets :
+ log.msg('--target', target)
+
+ target.privmsg(*privmsg)
+
+def main (args) :
+ options = Options()
+ options.parseOptions(args)
+
+ # logging
+ log.startLogging(sys.stderr, setStdout=False)
+
+ # connect
+ irc = pvl.irker.irc.IRCFactory()
+
+ connect(irc, options.connect)
+ target(irc, options.target, options.privmsg)
+
+ # listen
+ irk = pvl.irker.irk.IrkFactory(irc)
+
+ for endpoint, args in options.listen :
+ endpoint = endpoint(reactor, *args)
+
+ log.msg("listen:", endpoint)
+
+ endpoint.listen(irk)
+
+
+ # go
+ reactor.run()
+
+ return 0
+
+if __name__ == '__main__' :
+ import sys
+ sys.exit(main(sys.argv[1:]))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/irker/irc.py Fri Jan 11 17:13:11 2013 +0200
@@ -0,0 +1,389 @@
+"""
+ 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
+
+ def privmsg (self, *msgs) :
+ for msg in msgs :
+ # XXX: encode
+ self.client.msg(self.channel, msg.encode(self.encoding))
+
+ 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 = { }
+
+ 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 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
+
+ log.err(IRCError(None, msg))
+
+ 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.
+ """
+
+ # 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) :
+ # default nickname
+ self.nickname = nickname
+
+ # (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, 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[lookup]
+
+ log.msg('IRCFactory.client', url, ":", client)
+
+ defer.returnValue(client)
+
+ @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)