diff content/Coding/011-ts3-python-javascript.rst @ 4:7ce6393e6d30

Adding converted blog posts from old blog.
author Brian Neal <bgneal@gmail.com>
date Thu, 30 Jan 2014 21:45:03 -0600
parents
children 49bebfa6f9d3
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content/Coding/011-ts3-python-javascript.rst	Thu Jan 30 21:45:03 2014 -0600
@@ -0,0 +1,311 @@
+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
+
+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
+
+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