Twisted: Static Content with Dynamic Pages
When I was first learning about the twisted.web server framework, I made the mistake of trying to include an external stylesheet from my server. Much to my dismay, it simply wouldn't load. Eventually I figured out that I wasn't telling Twisted to server those files, and since I wasn't going through any kind of encompassing server (nginx, Apache, etc), it made perfect sense that those files remained a mystery to the browser. The solution is simpler than you might assume. Let's define a nice, basic web page:
from twisted.web import resource
class Home(resource.Resource):
isLeaf = False
def getChild(self, name, request):
if name == '':
return self
return resource.Resource.geChild(self, name, request)
def render_GET(self, request):
return "<html>Hello, world!</html>"
Run the above script in your main method via something similar to the following:
if __name__ == "__main__":
from twisted.web import server
from twisted.internet import reactor
root = Home()
site = server.Site(root)
reactor.listenTCP(8080, site)
reactor.run()
Now if you run your script and access localhost:8080 in the browser, you'll see our nice "Hello, world!" message. If we wanted to have that method return a substantial chunk of HTML, rather than a simple string, there's a good chance that that HTML would point to external scripts and styles. To set up an external directory to server your files is as easy as changing your main method from the piece of code above, to the following:
if __name__ == "__main__":
from twisted.web import server, static
from twisted.internet import reactor
root = Home()
# We are adding static directories to our server
root.putChild('styles', static.File("./styles"))
root.putChild('scripts', static.File("./scripts"))
site = server.Site(root)
reactor.listenTCP(8080, site)
reactor.run()
You can tell Twisted to serve any directory on your filesystem (assuming permissions are correct), and can tell it to name it whatever you like. This makes it easy to host static content (HTML, CSS, and JavaScript), while integrating it with your Python scripts.
Integrating twisted.web and IRC
As a simple proof-of-concept to show that integration between the HTTP and IRC Protocols was not only possible, but easy, I wrote a small Python script that made use of Twisted to talk to the web and an IRC server.
First, we'll define our imports:
import sys from twisted.words.protocols import irc from twisted.web import server, resource from twisted.internet import protocol, reactor
Now that we've got all the requisite classes, we can define a basic web resource to act as our website:
class Home(resource.Resource):
isLeaf = True
def __init__(self, irc_factory):
self.irc_factory = irc_factory
def render_GET(self, request):
self.factory.send_msg("HTTP Access!")
return "<html>Welcome Home!</html>"
Notice that we're passing a factory into the constructor for our website. The reason for this will become apparent in the near future, so until we need to explain it, we'll go ahead and define a basic IRC Client:
class TalkBot(irc.IRCClient):
# The method below allows us to get the nickname from the factory
def _get_nickname(self):
return self.factory.nickname
nickname = property(_get_nickname)
def connectionMade(self):
# Since we override this method, we need to call the IRCClient one as well
irc.IRCClient.connectionMade(self)
# Add this instance to the client list
self.factory.clients.append(self)
def signedOn(self):
self.join(self.factory.channel)
print "Signed on as %s." % (self.nickname,)
def joined(self, channel):
print "Joined %s." % (channel,)
def privmsg(self, user, channel, msg):
print "#" + channel + " <" + user + "> " + msg
We'll now write a standard ClientFactory to create instances of our client:
class TalkBotFactory(protocol.ClientFactory):
protocol = TalkBot
def __init__(self, channel, nickname='talkbot'):
self.channel = channel
self.nickname = nickname
self.clients = []
def clientConnectionLost(self, connector, reason):
print "Lost connection (%s), reconnecting." % (reason,)
connector.connect()
def clientConnectionFailed(self, connector, reason):
print "Could not connect: %s" % (reason,)
def send_msg(self, msg):
for client in self.clients:
client.say(self.channel, msg)
With everything set up, we can now define the main function of our script:
if __name__ == "__main__":
chan = sys.argv[1]
factory = TalkBotFactory('#' + chan)
site = server.Site(Home(factory))
reactor.listenTCP(8080, site)
reactor.connectTCP('irc.freenode.net', 6667, factory)
reactor.run()
You'll notice that we create an instance of our TalkBotFactory by hand, rather than just passing the constructor into our reactor.connectTCP() method. This is because we need to pass the instance of the factory into our HTTP server, so it can perform the proper methods on it to send messages to all active clients.
This is just a rough concept, but it shows how simple it can be to combine a stateless protocol (HTTP) with a stateful one (IRC).