annotate content/Coding/011-ts3-python-javascript.rst @ 17:fa54eda9b809

Fix typo in get_profile() post.
author Brian Neal <bgneal@gmail.com>
date Wed, 30 Jul 2014 20:16:40 -0500
parents 49bebfa6f9d3
children
rev   line source
bgneal@4 1 A TeamSpeak 3 viewer with Python & Javascript
bgneal@4 2 #############################################
bgneal@4 3
bgneal@4 4 :date: 2012-01-20 19:15
bgneal@4 5 :tags: Python, Javascript, TeamSpeak
bgneal@4 6 :slug: a-teamspeak-3-viewer-with-python-javascript
bgneal@4 7 :author: Brian Neal
bgneal@7 8 :summary: Here's how I cobbled together a TeamSpeak 3 viewer in Python & Javascript. And there might even be some PHP involved too...
bgneal@4 9
bgneal@4 10 The Problem
bgneal@4 11 ===========
bgneal@4 12
bgneal@4 13 My gaming clan started using `TeamSpeak 3`_ (TS3) for voice communications, so
bgneal@4 14 it wasn't long before we wanted to see who was on the TS3 server from the clan's
bgneal@4 15 server status page. Long ago, before I met Python, I had built the clan a server
bgneal@4 16 status page in PHP. This consisted of cobbling together various home-made and
bgneal@4 17 3rd party PHP scripts for querying game servers (Call of Duty, Battlefield) and
bgneal@4 18 voice servers (TeamSpeak 2 and Mumble). But TeamSpeak 3 was a new one for us,
bgneal@4 19 and I didn't have anything to query that. My interests in PHP are long behind
bgneal@4 20 me, but we needed to add a TS3 viewer to the PHP page. The gaming clan's web
bgneal@4 21 hosting is pretty vanilla; in other words PHP is the first class citizen. If I
bgneal@4 22 really wanted to host a Python app I probably could have resorted to Fast CGI or
bgneal@4 23 something. But I had no experience in that and no desire to go that way.
bgneal@4 24
bgneal@4 25 I briefly thought about finding a 3rd party PHP library to query a TS3 server.
bgneal@4 26 The libraries are out there, but they are as you might expect: overly
bgneal@4 27 complicated and/or pretty amateurish (no public source code repository). I even
bgneal@4 28 considered writing my own PHP code to do the job, so I started looking for any
bgneal@4 29 documentation on the TS3 server query protocol. Luckily, there is a `TS3
bgneal@4 30 query protocol document`_, and it is fairly decent.
bgneal@4 31
bgneal@4 32 But, I just could not bring myself to write PHP again. On top of this, the
bgneal@4 33 gaming clan's shared hosting blocks non-standard ports. If I did have a PHP
bgneal@4 34 solution, the outgoing query to the TS3 server would have been blocked by the
bgneal@4 35 host's firewall. It is a hassle to contact their technical support and try to
bgneal@4 36 find a person who knows what a port is and get it unblocked (we've had to do
bgneal@4 37 this over and over as each game comes out). Thus it ultimately boiled down to me
bgneal@4 38 wanting to do this in Python. For me, life is too short to write PHP scripts.
bgneal@4 39
bgneal@4 40 I started thinking about writing a query application in Python using my
bgneal@4 41 dedicated server that I use to host a few Django_ powered websites. At first I
bgneal@4 42 thought I'd generate the server status HTML on my server and display it in an
bgneal@4 43 ``<iframe>`` on the gaming clan's server. But then it hit me that all I really
bgneal@4 44 needed to do is have my Django_ application output a representation of the TS3
bgneal@4 45 server status in JSON_, and then perhaps I could find a slick jQuery_ tree menu
bgneal@4 46 to display the status graphically. I really liked this idea, so here is a post
bgneal@4 47 about the twists and turns I took implementing it.
bgneal@4 48
bgneal@4 49 The Javascript
bgneal@4 50 ==============
bgneal@4 51
bgneal@4 52 My searching turned up several jQuery tree menu plugins, but in the end I
bgneal@4 53 settled on dynatree_. Dynatree had clear documentation I could understand, it
bgneal@4 54 seems to be actively maintained, and it can generate a menu from JSON. After one
bgneal@4 55 evening of reading the docs, I built a static test HTML page that could display
bgneal@4 56 a tree menu built from JSON. Here the Javascript code I put in the test page's
bgneal@4 57 ``<head>`` section:
bgneal@4 58
bgneal@4 59 .. sourcecode:: javascript
bgneal@4 60
bgneal@4 61 var ts3_data = [
bgneal@4 62 {title: "Phantom Aces", isFolder: true, expand: true,
bgneal@4 63 children: [
bgneal@4 64 {title: "MW3", isFolder: true, expand: true,
bgneal@4 65 children: [
bgneal@4 66 {title: "Hogan", icon: "client.png"},
bgneal@4 67 {title: "Fritz!!", icon: "client.png"}
bgneal@4 68 ]
bgneal@4 69 },
bgneal@4 70 {title: "COD4", isFolder: true, expand: true,
bgneal@4 71 children: [
bgneal@4 72 {title: "Klink", icon: "client.png"}
bgneal@4 73 ]
bgneal@4 74 },
bgneal@4 75 {title: "Away", isFolder: true, children: [], expand: true}
bgneal@4 76 ]
bgneal@4 77 }
bgneal@4 78 ];
bgneal@4 79
bgneal@4 80 $(function(){
bgneal@4 81 $("#ts3-tree").dynatree({
bgneal@4 82 persist: false,
bgneal@4 83 children: ts3_data
bgneal@4 84 });
bgneal@4 85 });
bgneal@4 86
bgneal@4 87 Note that ``client.png`` is a small icon I found that I use in place of
bgneal@4 88 dynatree's default file icon to represent TS3 clients. If I omitted the icon
bgneal@4 89 attribute, the TS3 client would have appeared as a small file icon. Channels
bgneal@4 90 appear as folder icons, and this didn't seem to unreasonable to me. In other
bgneal@4 91 words I had no idea what a channel icon would look like. A folder was fine.
bgneal@4 92
bgneal@4 93 With dynatree, you don't need a lot of HTML markup, it does all the heavy
bgneal@4 94 lifting. You simply have to give it an empty ``<div>`` tag it can render
bgneal@4 95 into.
bgneal@4 96
bgneal@4 97 .. sourcecode:: html
bgneal@4 98
bgneal@4 99 <body>
bgneal@4 100 <div id="ts3-tree"></div>
bgneal@4 101 </body>
bgneal@4 102 </html>
bgneal@4 103
bgneal@4 104 Here is a screenshot of the static test page in action.
bgneal@4 105
bgneal@4 106 .. image:: /images/011-tree1.png
bgneal@7 107 :alt: screenshot
bgneal@4 108
bgneal@4 109 Nice! Thanks dynatree! Now all I need to do is figure out how to dynamically
bgneal@4 110 generate the JSON data and get it into the gaming clan's server status page.
bgneal@4 111
bgneal@4 112 The Python
bgneal@4 113 ==========
bgneal@4 114
bgneal@4 115 Looking through the `TS3 protocol documentation`_ I was somewhat surprised to
bgneal@4 116 see that TS3 used the Telnet protocol for queries. So from my trusty shell I
bgneal@4 117 telnet'ed into the TS3 server and played with the available commands. I made
bgneal@4 118 notes on what commands I needed to issue to build my status display.
bgneal@4 119
bgneal@4 120 My experiments worked, and I could see a path forward, but there were still some
bgneal@4 121 kinks to be worked out with the TS3 protocol. The data it sent back was escaped
bgneal@4 122 in a strange way for one thing. I would have to post-process the data in Python
bgneal@4 123 before I could use it. I didn't want to reinvent the wheel, so I did a quick
bgneal@4 124 search for Python libraries for working with TS3. I found a few, but quickly
bgneal@4 125 settled on Andrew William's python-ts3_ library. It was small, easy to
bgneal@4 126 understand, had tests, and a GitHub page. Perfect.
bgneal@4 127
bgneal@4 128 One of the great things about Python, of course, is the interactive shell. Armed
bgneal@4 129 with the `TS3 protocol documentation`_, python-ts3_, and the Python shell, I was
bgneal@4 130 able to interactively connect to the TS3 server and poke around again. This time
bgneal@4 131 I was sitting above telnet using python-ts3_ and I confirmed it would do the job
bgneal@4 132 for me.
bgneal@4 133
bgneal@4 134 Another evening was spent coding up a Django view to query the TS3 server using
bgneal@4 135 python-ts3_ and to output the channel status as JSON.
bgneal@4 136
bgneal@4 137 .. sourcecode:: python
bgneal@4 138
bgneal@4 139 from django.conf import settings
bgneal@4 140 from django.core.cache import cache
bgneal@4 141 from django.http import HttpResponse, HttpResponseServerError
bgneal@4 142 from django.utils import simplejson
bgneal@4 143 import ts3
bgneal@4 144
bgneal@4 145 CACHE_KEY = 'ts3-json'
bgneal@4 146 CACHE_TIMEOUT = 2 * 60
bgneal@4 147
bgneal@4 148 def ts3_query(request):
bgneal@4 149 """
bgneal@4 150 Query the TeamSpeak3 server for status, and output a JSON
bgneal@4 151 representation.
bgneal@4 152
bgneal@4 153 The JSON we return is targeted towards the jQuery plugin Dynatree
bgneal@4 154 http://code.google.com/p/dynatree/
bgneal@4 155
bgneal@4 156 """
bgneal@4 157 # Do we have the result cached?
bgneal@4 158 result = cache.get(CACHE_KEY)
bgneal@4 159 if result:
bgneal@4 160 return HttpResponse(result, content_type='application/json')
bgneal@4 161
bgneal@4 162 # Cache miss, go query the remote server
bgneal@4 163
bgneal@4 164 try:
bgneal@4 165 svr = ts3.TS3Server(settings.TS3_IP, settings.TS3_PORT,
bgneal@4 166 settings.TS3_VID)
bgneal@4 167 except ts3.ConnectionError:
bgneal@4 168 return HttpResponseServerError()
bgneal@4 169
bgneal@4 170 response = svr.send_command('serverinfo')
bgneal@4 171 if response.response['msg'] != 'ok':
bgneal@4 172 return HttpResponseServerError()
bgneal@4 173 svr_info = response.data[0]
bgneal@4 174
bgneal@4 175 response = svr.send_command('channellist')
bgneal@4 176 if response.response['msg'] != 'ok':
bgneal@4 177 return HttpResponseServerError()
bgneal@4 178 channel_list = response.data
bgneal@4 179
bgneal@4 180 response = svr.send_command('clientlist')
bgneal@4 181 if response.response['msg'] != 'ok':
bgneal@4 182 return HttpResponseServerError()
bgneal@4 183 client_list = response.data
bgneal@4 184
bgneal@4 185 # Start building the channel / client tree.
bgneal@4 186 # We save tree nodes in a dictionary, keyed by their id so we can find
bgneal@4 187 # them later in order to support arbitrary channel hierarchies.
bgneal@4 188 channels = {}
bgneal@4 189
bgneal@4 190 # Build the root, or channel 0
bgneal@4 191 channels[0] = {
bgneal@4 192 'title': svr_info['virtualserver_name'],
bgneal@4 193 'isFolder': True,
bgneal@4 194 'expand': True,
bgneal@4 195 'children': []
bgneal@4 196 }
bgneal@4 197
bgneal@4 198 # Add the channels to our tree
bgneal@4 199
bgneal@4 200 for channel in channel_list:
bgneal@4 201 node = {
bgneal@4 202 'title': channel['channel_name'],
bgneal@4 203 'isFolder': True,
bgneal@4 204 'expand': True,
bgneal@4 205 'children': []
bgneal@4 206 }
bgneal@4 207 parent = channels[int(channel['pid'])]
bgneal@4 208 parent['children'].append(node)
bgneal@4 209 channels[int(channel['cid'])] = node
bgneal@4 210
bgneal@4 211 # Add the clients to the tree
bgneal@4 212
bgneal@4 213 for client in client_list:
bgneal@4 214 if client['client_type'] == '0':
bgneal@4 215 node = {
bgneal@4 216 'title': client['client_nickname'],
bgneal@4 217 'icon': 'client.png'
bgneal@4 218 }
bgneal@4 219 channel = channels[int(client['cid'])]
bgneal@4 220 channel['children'].append(node)
bgneal@4 221
bgneal@4 222 tree = [channels[0]]
bgneal@4 223
bgneal@4 224 # convert to JSON
bgneal@4 225 json = simplejson.dumps(tree)
bgneal@4 226
bgneal@4 227 cache.set(CACHE_KEY, json, CACHE_TIMEOUT)
bgneal@4 228
bgneal@4 229 return HttpResponse(json, content_type='application/json')
bgneal@4 230
bgneal@4 231 I have to make three queries to the TS3 server to get all the information I
bgneal@4 232 need. The ``serverinfo`` command is issued to retrieve the TS3 virtual server's
bgneal@4 233 name. The ``channellist`` command retrieves the list of channels. The
bgneal@4 234 ``clientlist`` command gets the list of TS3 clients that are currently
bgneal@4 235 connected. For more information on these three commands see the TS3 query
bgneal@4 236 protocol document.
bgneal@4 237
bgneal@4 238 The only real tricky part of this code was figuring out how to represent an
bgneal@4 239 arbitrary, deeply-nested channel tree in Python. I ended up guessing that
bgneal@4 240 ``cid`` meant channel ID and ``pid`` meant parent ID in the TS3 query data. I
bgneal@4 241 squirrel away the channels in a ``channels`` dictionary, keyed by channel ID.
bgneal@4 242 The root channel has an ID of 0. While iterating over the channel list, I can
bgneal@4 243 retrieve the parent channel from the ``channels`` dictionary by ID and append
bgneal@4 244 the new channel to the parent's ``children`` list. Clients are handled the same
bgneal@4 245 way, but have different attributes. By inspecting the ``clientlist`` data in the
bgneal@4 246 Python shell, I noticed that my Telnet client also showed up in that list.
bgneal@4 247 However it had a ``client_type`` of 1, whereas the normal PC clients had a
bgneal@4 248 ``client_type`` of 0.
bgneal@4 249
bgneal@4 250 I decided to cache the results for 2 minutes to reduce hits on the TS3 server,
bgneal@4 251 as it has flood protection. This probably isn't needed given the size of our
bgneal@4 252 gaming clan, but Django makes it easy to do, so why not?
bgneal@4 253
bgneal@4 254 Putting it all together
bgneal@4 255 =======================
bgneal@4 256
bgneal@4 257 At this point I knew how to use my Django application to query the TS3 server
bgneal@4 258 and build status in JSON format. I also knew what the Javascript and HTML on the
bgneal@4 259 gaming clan's server status page (written in PHP) had to look like to render
bgneal@4 260 that JSON status.
bgneal@4 261
bgneal@4 262 The problem was the server status page was on one server, and my Django
bgneal@4 263 application was on another. At first I thought it would be no problem for the
bgneal@4 264 Javascript to do a ``GET`` on my Django server and retrieve the JSON. However I
bgneal@4 265 had some vague memory of the browser security model, and after some googling I
bgneal@4 266 was reminded of the `same origin policy`_. Rats. That wasn't going to work.
bgneal@4 267
bgneal@4 268 I briefly researched JSONP_, which is the technique that Facebook & Google use
bgneal@4 269 to embed those little "like" and "+1" buttons on your web pages. But in the end
bgneal@4 270 it was just as easy to have the PHP script make the ``GET`` request to my Django
bgneal@4 271 application using a `file_get_contents()`_ call. The PHP can then embed the JSON
bgneal@4 272 directly into the server status page:
bgneal@4 273
bgneal@4 274 .. sourcecode:: php
bgneal@4 275
bgneal@4 276 $ts3_source = 'http://example.com/ts3/';
bgneal@4 277 $ts3_json = file_get_contents($ts3_source);
bgneal@4 278
bgneal@4 279 require_once 'header.php';
bgneal@4 280
bgneal@4 281 And in header.php, some HTML sprinkled with some PHP:
bgneal@4 282
bgneal@4 283 .. sourcecode:: html
bgneal@4 284
bgneal@4 285 <script type="text/javascript">
bgneal@4 286 var ts3_data = <?php echo $ts3_json; ?>;
bgneal@4 287
bgneal@4 288 $(function(){
bgneal@4 289 $("#ts3-tree").dynatree({
bgneal@4 290 persist: false,
bgneal@4 291 children: ts3_data
bgneal@4 292 });
bgneal@4 293 });
bgneal@4 294 </script>
bgneal@4 295
bgneal@4 296 That did the trick. In the end I had to touch a little PHP, but it was
bgneal@4 297 tolerable. That was a very round-about solution to building a TS3 viewer in
bgneal@4 298 Python and Javascript. While I doubt you will have the same strange requirements
bgneal@4 299 that I had (multiple servers), I hope you can see how to combine a few
bgneal@4 300 technologies to make a TS3 viewer in Python.
bgneal@4 301
bgneal@4 302
bgneal@4 303 .. _TeamSpeak 3: http://teamspeak.com/?page=teamspeak3
bgneal@4 304 .. _TS3 query protocol document: http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf
bgneal@4 305 .. _Django: https://www.djangoproject.org
bgneal@4 306 .. _JSON: http://json.org
bgneal@4 307 .. _jQuery: http://jquery.org
bgneal@4 308 .. _dynatree: http://code.google.com/p/dynatree/
bgneal@4 309 .. _python-ts3: http://pypi.python.org/pypi/python-ts3/0.1
bgneal@4 310 .. _same origin policy: http://en.wikipedia.org/wiki/Same_origin_policy
bgneal@4 311 .. _JSONP: http://en.wikipedia.org/wiki/JSONP
bgneal@4 312 .. _file_get_contents(): http://php.net/manual/en/function.file-get-contents.php
bgneal@4 313 .. _TS3 protocol documentation: http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf