Mercurial > public > pelican-blog
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