Creating a webchat with web.py

tutorial html javascript python webdev webpy

BitBucket Repository

I have previously written a tutorial on how to do long polling on webpy framework. Well, this is an updated version with a code repository that you can use to include an webchat subapp in your webpy applications.

Tools required

Frontend implementation - Layouts

For the main page, it would consist of one main area where the messages will be displayed. And an area at the bottom which consist of a type input, where the messages are submitted.

All the javascript coding will be included in the webchat.js, which will be described in more detail later.

/static/layout/index.html:

$def with (msgs)
<script src='static/js/jquery-1.11.1.min.js'></script><script src='static/js/webchat.js'></script>
<div id='msgs'>
    <h1>Messages</h1>
    $for msg in msgs:
    <div class='msg'>$msg</div>
</div>
<div id='entry'>
    <input id='message' type='text' placeholder='Message here ...' />
    <input type='submit' value='submit' />
</div>

The styling is not so important for this project, but you can view the CSS style in the code repository.

Storing the messages

We will be using sqlite to store our messages. So, open up your console, and type in sqlite3 database.db, and create the table using the following schema:

CREATE TABLE msgs(
    msg_id INTEGER PRIMARY KEY,
    msg_content TEXT NOT NULL
);

Simulating real-time events - Client side

To simplify our lives, we will be using jquery for the ajax calls. Everytime a call is returned, the message wrapper(#msgs) will be updated and another call is made.

Another function is needed to send the message to the server for storing into the database.

These functions will be included inside /static/js/webchat.js:

function sendMsg(){
    var div = $("#message")[0];
    var msg = div.value;

    if(msg==""){
        alert("Message cannot be blank");
    }else{
        div.value = "";
        $.ajax({url:"/send?msg="+msg});
    }
}

function longPoll(idx){
    $.get(
        url="/get?idx="+idx
    ).done(function(data){
        data = eval("("+data+")");
        var msgs = data["msgs"];

        if(msgs){
            for(var i=0; i<msgs.length; i++){
                $("#msgs")[0].innerHTML += '<div class='msg'>'+msgs[i]+'</div>'
            }
        }

        longPoll(data["idx"]);
    });
}

Simulating real-time events - Server side

Framework for the webpy application

This is the basic framework for webchat.py:

import web, time, json

urls = [
    "/", "Index",
    "/send", "SendMsg",
    "/get", "GetMsg"
]
render = web.template.render("layouts")
app = web.application(urls, globals())
db = web.database(dbn="sqlite", db="database.db")

class Index:
    def GET(self):
        return

class SendMsg:
    def GET(self):
        return

class GetMsg:
    def GET(self):
        return

if __name__ == "__main__":
    app.run()

Serving the existing messages

Alter the Index class to read messages from the database and render it inside the layout:

class Index:
    def GET(self):
        res = db.select("msgs")
        msgs = [r for r in res]

        content = [m["msg_content"] for m in msgs]
        return render.index(content, msgs[-1]["msg_id"])

Add new messages to the database

Insert the content using /send into the database:

class SendMsg:
    def GET(self):
        i = web.input()
        if i.get("msg"):
            db.insert("msgs", msg_content=i.get("msg"))
            return
        else:
            return web.notfound()

Getting new messages - Long Polling

This is the important bit. The server side code for the long polling process:

class GetMsg:
    def GET(self):
        i = web.input().get("idx")
        print i
        if not i or not i.isdigit(): i = "0"

        max_iter = 20; iter = 0
        msgs = []

        while not len(msgs) and iter"+i):
            msgs = [r for r in res]
            iter += 1
            time.sleep(i)

        if len(msgs): i=msgs[-1]["msg_id"]

        return json.dumps({
            "msgs":[m["msg_content"] for m in msgs],
            "idx":i
        })

A bit of explanation here. The while loop and the time.sleep is what makes the whole idea works. The server will keep the request open until it detects a change in the database.

Running the server - Gevent WSGIServer

If you are running this as a subapp. Here is all you have to know. If you are running it as a standalone application, do read on.

Webpy’s default web server is not that great for production. Instead, we will be using the Gevent WSGIServer to run the application.

from gevent import monkey, pywsgi;
monkey.patch_all();

""" Whatever code you have """

if __name__ == "__main__":
    print "WSGISever on 8080"
    application = app.wsgifunc()
    pywsgi.WSGIServer(("", 8080), application).serve_forever()

Fixing the static directory problem

The WSGIServer does not automatically serve static file like webpy. Fortunately, there is a fix for this:

urls = [
    "/static/(.*)", "Static",
    "/", "Index",
    "/send", "SendMsg",
    "/get", "GetMsg"
]

class Static:
    def GET(self, file):
        try:
            f = open("static/"+file, "rb")
            return f.read()
        except:
            return web.notfound()

Going on from here

If you are lost in any of the steps above, do visit the code repository. It might be helpful to have a working sample where you can follow. Any troubles, drop me an email or leave it in the comments, I will help to be the best of my abilities.

BitBucket Repository