view content/Coding/011-ts3-python-javascript.rst @ 16:4280c90d5030

New Django related post about migrating away from get_profile().
author Brian Neal <bgneal@gmail.com>
date Sat, 24 May 2014 15:33:04 -0500
parents 49bebfa6f9d3
children
line wrap: on
line source
A TeamSpeak 3 viewer with Python & Javascript
#############################################

:date: 2012-01-20 19:15
:tags: Python, Javascript, TeamSpeak
:slug: a-teamspeak-3-viewer-with-python-javascript
:author: Brian Neal
:summary: Here's how I cobbled together a TeamSpeak 3 viewer in Python & Javascript. And there might even be some PHP involved too...

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:

.. sourcecode:: javascript

      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.

.. sourcecode:: html

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

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

.. image:: /images/011-tree1.png
   :alt: 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.

.. sourcecode:: python

   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:

.. sourcecode:: php

   $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:

.. sourcecode:: html

   <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.


.. _TeamSpeak 3: http://teamspeak.com/?page=teamspeak3
.. _TS3 query protocol document: http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf
.. _Django: https://www.djangoproject.org
.. _JSON: http://json.org
.. _jQuery: http://jquery.org
.. _dynatree: http://code.google.com/p/dynatree/
.. _python-ts3: http://pypi.python.org/pypi/python-ts3/0.1
.. _same origin policy: http://en.wikipedia.org/wiki/Same_origin_policy
.. _JSONP: http://en.wikipedia.org/wiki/JSONP
.. _file_get_contents(): http://php.net/manual/en/function.file-get-contents.php
.. _TS3 protocol documentation: http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf