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