web2py, WebSockets and Socket.IO: Part I – Basic Display

This entry is part 1 of 3 in the series web2py, WebScokets

I’ve been playing with web2py for a bit now, slowly building out my library of “functionality and items I use,” as many developers are wont to do with their favorite technology stack.  Of course one of the biggest hurdles that web software and websites can face is overloading the server’s request capacity with too many hits.  I won’t go into the full functionality and features behind WebSockets here, because there are plenty of places the web over that address that question in their own right. The web2py framework contains a basic implementation for part of the WebSocket protocol, and I have been working on trying to flesh that out some more functionality to allow a more dynamic use of the technology.

Included Functionality

At present web2py includes a script named comet_messaging.py in its “contrib” directory that the person deploying an installation can use to create a basic Tornado web server that will be the end point for a WebSocket connection point. Also included is a basic JavaScript function called “web2py_comet” that allows a page to connect to the server and listen for incoming WebSocket messages.

This limits you, however, to only receiving push messages from the server. And the current implementation also limits other possibilities.  As I have been working on exploring and experimenting with WebSockets I have been working with a website chat program as it is a prototypical bidirectional communication function.

Database Structure

I employed the following database structure in web2py for chat.

db.define_table('chat_channel',
    Field('name'),
    Field('restrictions', default='all', label='Access Level'),
    format='%(name)s')
db.define_table('chat',
    Field('channel', db.chat_channel, default=1)
    Field('speaker', db.auth_user, default=auth.user_id, writable=False, readable=False),
    Field('statement', 'text', requires=IS_NOT_EMPTY()),
    Field('time', 'datetime', default=datetime.now, writable=False, readable=False),
    Field('deleted', db.auth_user, writable=False, readable=False))
db.chat_channel.name.requires = [IS_NOT_IN_DB(db, db.chat_channel.name),IS_NOT_EMPTY()]
db.chat_channel.restrictions.requires = IS_IN_SET([('staff', 'Staff Only'),('all', 'All Players')])
db.chat.speaker.requires = IS_IN_DB(db, db.auth_user.id)
db.chat.deleted.requires = IS_IN_DB(db, db.auth_user.id

As you can see there is nothing particularly special about the database. It contains two tables: one is a list of channels available for chat and the other is a list of chat lines. Each line is associated with a single channel and a single speaker from the default auth code. A time stamp is kept for logging purposes. Staff of the chat room will also have the ability to delete offending posts, so we keep the user ID of whoever has deleted a line also for logging purposes.

A chat channel can be restricted to only certain peoples’ access, and this is recorded in the ‘restrictions’ field and enforced by the requires value of that field. Later in the program we will be sure this is enforced, but for now I have glossed over it in the controller.  I initialize a few fields with a call to the following

db.chat_channel.insert(name='General')
db.chat_channel.insert(name='Staff', restrictions='staff')

In production the admin might choose to set more chat channels, but for now there are only two, since the restrictions are not even enforced (I was more interested in the WebSocket functionality).

Main Controller

The main controller is relatively straightforward. It fetches the latest 35 undeleted lines of chat, formats them, and returns them along with the list of chat channels.  So here’s the basic code

@auth.requires_permission('speak', 'chat')
def rooms():
    '''
    Returns the initial set of lines to populate the page and
    the chat speaking form.
    '''
    staff=auth.has_permission('delete', 'chat')
    # Staff members should still see the delete lines
    if not staff:
	rawlines=db(db.chat.channel==request.args[0] and db.chat.deleted==None).select(limitby=(0,35),orderby=~db.chat.id)
    else:
	rawlines=db(db.chat.channel==request.args[0]).select(db.chat.ALL,limitby=(0,35),orderby=~db.chat.id)
    lines=[]
    for line in rawlines:
	# Treat emote lines and other lines different from each other
	clazz=(line.deleted!=None and 'chat-deleted') or ''
        if line.emote:
            lines.append(DIV(line.statement,_id=line.id,_class=clazz))
        else:
            lines.append(DIV(line.speaker.name, B(">"), " %s" % (line.statement, ),_id=line.id, _class=clazz))
    # This puts the lines in the direction that makes more sense to read from top to bottom
    lines.reverse()
    # The list of channels available
    channels=db().select(db.chat_channel.ALL)
    return dict(lines=lines,channels=channels,staff=staff,current=int(request.args[0]))

Ok, so let’s break this down a little.

Permissions

First, in order to access this controller, the user must have permission to speak in chat. This permission is given to users automatically when they are created by a custom function I have which handles that. Nothing special there.  Second, staff members in the chat will have permission to delete in chat, so we determine whether the current user has that right.

Fetch Lines

If the user is staff, we fetch the latest 35 lines and return them.  If the user is not staff, we fetch only those lines which are not deleted. We then render those with the web2py helper methods and ferry them out to the user in their correct order.

View

The view is nothing spectacular.  Just print out the lines and the chat form.

{{extend 'layout.html'}}

<div id="chat">
	<div id="chatlines" style="margin: 5px; border: 1px solid rgb(22,44,22)">
		{{for line in lines:}}
			{{=line}}
		{{pass}}
	</div>

	<div id="chatform_box">
		<form id="chatform">
			<fieldset>
				<div>
					<input type="text" name="statement" id="statement" length="100" />
					<input type="submit" value="chat" />
				</div>
				<div>
					<select name="channel" id="channel">
						{{for channel in channels:}}
						<option value="{{=channel.id}}"{{if channel.id==current:}} selected="selected"{{pass}}>{{=channel.name}}</option>
						{{pass}}
					</select>
				</div>
			</fieldset>
		</form>
	</div>
</div>

In Part II I will cover the basic AJAX callbacks to activate the form and get the chat working in browsers that support the WebSocket API.

Series Navigation@web2py, WebSockets and Socket.IO: Part II – AJAX + WebSockets
Thursday, April 28th, 2011 Server, Technology

Leave a Reply