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
|