The Problem

My gaming clan started using TeamSpeak 3 (TS3) for voice communications, so it wasn't long before we wanted to see who was on the TS3 server from the clan's server status page. Long ago, before I met Python, I had built the clan a server status page in PHP. This consisted of cobbling together various home-made and 3rd party PHP scripts for querying game servers (Call of Duty, Battlefield) and voice servers (TeamSpeak 2 and Mumble). But TeamSpeak 3 was a new one for us, and I didn't have anything to query that. My interests in PHP are long behind me, but we needed to add a TS3 viewer to the PHP page. The gaming clan's web hosting is pretty vanilla; in other words PHP is the first class citizen. If I really wanted to host a Python app I probably could have resorted to Fast CGI or something. But I had no experience in that and no desire to go that way.

I briefly thought about finding a 3rd party PHP library to query a TS3 server. The libraries are out there, but they are as you might expect: overly complicated and/or pretty amateurish (no public source code repository). I even considered writing my own PHP code to do the job, so I started looking for any documentation on the TS3 server query protocol. Luckily, there is a TS3 query protocol document, and it is fairly decent.

But, I just could not bring myself to write PHP again. On top of this, the gaming clan's shared hosting blocks non-standard ports. If I did have a PHP solution, the outgoing query to the TS3 server would have been blocked by the host's firewall. It is a hassle to contact their technical support and try to find a person who knows what a port is and get it unblocked (we've had to do this over and over as each game comes out). Thus it ultimately boiled down to me wanting to do this in Python. For me, life is too short to write PHP scripts.

I started thinking about writing a query application in Python using my dedicated server that I use to host a few Django powered websites. At first I thought I'd generate the server status HTML on my server and display it in an <iframe> on the gaming clan's server. But then it hit me that all I really needed to do is have my Django application output a representation of the TS3 server status in JSON, and then perhaps I could find a slick jQuery tree menu to display the status graphically. I really liked this idea, so here is a post about the twists and turns I took implementing it.

The Javascript

My searching turned up several jQuery tree menu plugins, but in the end I settled on dynatree. Dynatree had clear documentation I could understand, it seems to be actively maintained, and it can generate a menu from JSON. After one evening of reading the docs, I built a static test HTML page that could display a tree menu built from JSON. Here the Javascript code I put in the test page's <head> section:

var ts3_data = [
   {title: "Phantom Aces", isFolder: true, expand: true,
      children: [
         {title: "MW3", isFolder: true, expand: true,
            children: [
               {title: "Hogan", icon: "client.png"},
               {title: "Fritz!!", icon: "client.png"}
            ]
         },
         {title: "COD4", isFolder: true, expand: true,
            children: [
               {title: "Klink", icon: "client.png"}
            ]
         },
         {title: "Away", isFolder: true, children: [], expand: true}
     ]
   }
];

$(function(){
   $("#ts3-tree").dynatree({
      persist: false,
      children: ts3_data
   });
 });

Note that client.png is a small icon I found that I use in place of dynatree's default file icon to represent TS3 clients. If I omitted the icon attribute, the TS3 client would have appeared as a small file icon. Channels appear as folder icons, and this didn't seem to unreasonable to me. In other words I had no idea what a channel icon would look like. A folder was fine.

With dynatree, you don't need a lot of HTML markup, it does all the heavy lifting. You simply have to give it an empty <div> tag it can render into.

<body>
   <div id="ts3-tree"></div>
</body>
</html>

Here is a screenshot of the static test page in action.

screenshot

Nice! Thanks dynatree! Now all I need to do is figure out how to dynamically generate the JSON data and get it into the gaming clan's server status page.

The Python

Looking through the TS3 protocol documentation I was somewhat surprised to see that TS3 used the Telnet protocol for queries. So from my trusty shell I telnet'ed into the TS3 server and played with the available commands. I made notes on what commands I needed to issue to build my status display.

My experiments worked, and I could see a path forward, but there were still some kinks to be worked out with the TS3 protocol. The data it sent back was escaped in a strange way for one thing. I would have to post-process the data in Python before I could use it. I didn't want to reinvent the wheel, so I did a quick search for Python libraries for working with TS3. I found a few, but quickly settled on Andrew William's python-ts3 library. It was small, easy to understand, had tests, and a GitHub page. Perfect.

One of the great things about Python, of course, is the interactive shell. Armed with the TS3 protocol documentation, python-ts3, and the Python shell, I was able to interactively connect to the TS3 server and poke around again. This time I was sitting above telnet using python-ts3 and I confirmed it would do the job for me.

Another evening was spent coding up a Django view to query the TS3 server using python-ts3 and to output the channel status as JSON.

from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse, HttpResponseServerError
from django.utils import simplejson
import ts3

CACHE_KEY = 'ts3-json'
CACHE_TIMEOUT = 2 * 60

def ts3_query(request):
    """
    Query the TeamSpeak3 server for status, and output a JSON
    representation.

    The JSON we return is targeted towards the jQuery plugin Dynatree
    http://code.google.com/p/dynatree/

    """
    # Do we have the result cached?
    result = cache.get(CACHE_KEY)
    if result:
        return HttpResponse(result, content_type='application/json')

    # Cache miss, go query the remote server

    try:
        svr = ts3.TS3Server(settings.TS3_IP, settings.TS3_PORT,
                settings.TS3_VID)
    except ts3.ConnectionError:
        return HttpResponseServerError()

    response = svr.send_command('serverinfo')
    if response.response['msg'] != 'ok':
        return HttpResponseServerError()
    svr_info = response.data[0]

    response = svr.send_command('channellist')
    if response.response['msg'] != 'ok':
        return HttpResponseServerError()
    channel_list = response.data

    response = svr.send_command('clientlist')
    if response.response['msg'] != 'ok':
        return HttpResponseServerError()
    client_list = response.data

    # Start building the channel / client tree.
    # We save tree nodes in a dictionary, keyed by their id so we can find
    # them later in order to support arbitrary channel hierarchies.
    channels = {}

    # Build the root, or channel 0
    channels[0] = {
        'title': svr_info['virtualserver_name'],
        'isFolder': True,
        'expand': True,
        'children': []
    }

    # Add the channels to our tree

    for channel in channel_list:
        node = {
            'title': channel['channel_name'],
            'isFolder': True,
            'expand': True,
            'children': []
        }
        parent = channels[int(channel['pid'])]
        parent['children'].append(node)
        channels[int(channel['cid'])] = node

    # Add the clients to the tree

    for client in client_list:
        if client['client_type'] == '0':
            node = {
                'title': client['client_nickname'],
                'icon': 'client.png'
            }
            channel = channels[int(client['cid'])]
            channel['children'].append(node)

    tree = [channels[0]]

    # convert to JSON
    json = simplejson.dumps(tree)

    cache.set(CACHE_KEY, json, CACHE_TIMEOUT)

    return HttpResponse(json, content_type='application/json')

I have to make three queries to the TS3 server to get all the information I need. The serverinfo command is issued to retrieve the TS3 virtual server's name. The channellist command retrieves the list of channels. The clientlist command gets the list of TS3 clients that are currently connected. For more information on these three commands see the TS3 query protocol document.

The only real tricky part of this code was figuring out how to represent an arbitrary, deeply-nested channel tree in Python. I ended up guessing that cid meant channel ID and pid meant parent ID in the TS3 query data. I squirrel away the channels in a channels dictionary, keyed by channel ID. The root channel has an ID of 0. While iterating over the channel list, I can retrieve the parent channel from the channels dictionary by ID and append the new channel to the parent's children list. Clients are handled the same way, but have different attributes. By inspecting the clientlist data in the Python shell, I noticed that my Telnet client also showed up in that list. However it had a client_type of 1, whereas the normal PC clients had a client_type of 0.

I decided to cache the results for 2 minutes to reduce hits on the TS3 server, as it has flood protection. This probably isn't needed given the size of our gaming clan, but Django makes it easy to do, so why not?

Putting it all together

At this point I knew how to use my Django application to query the TS3 server and build status in JSON format. I also knew what the Javascript and HTML on the gaming clan's server status page (written in PHP) had to look like to render that JSON status.

The problem was the server status page was on one server, and my Django application was on another. At first I thought it would be no problem for the Javascript to do a GET on my Django server and retrieve the JSON. However I had some vague memory of the browser security model, and after some googling I was reminded of the same origin policy. Rats. That wasn't going to work.

I briefly researched JSONP, which is the technique that Facebook & Google use to embed those little "like" and "+1" buttons on your web pages. But in the end it was just as easy to have the PHP script make the GET request to my Django application using a file_get_contents() call. The PHP can then embed the JSON directly into the server status page:

$ts3_source = 'http://example.com/ts3/';
$ts3_json = file_get_contents($ts3_source);

require_once 'header.php';

And in header.php, some HTML sprinkled with some PHP:

<script type="text/javascript">
   var ts3_data = <?php echo $ts3_json; ?>;

   $(function(){
      $("#ts3-tree").dynatree({
         persist: false,
         children: ts3_data
      });
    });
</script>

That did the trick. In the end I had to touch a little PHP, but it was tolerable. That was a very round-about solution to building a TS3 viewer in Python and Javascript. While I doubt you will have the same strange requirements that I had (multiple servers), I hope you can see how to combine a few technologies to make a TS3 viewer in Python.


Comments

comments powered by Disqus