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