Completed
Pull Request — v3 (#306)
by
unknown
03:47
created

Teamspeak3   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 296
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Test Coverage

Coverage 28.41%

Importance

Changes 2
Bugs 0 Features 2
Metric Value
wmc 21
c 2
b 0
f 2
lcom 2
cbo 5
dl 0
loc 296
ccs 25
cts 88
cp 0.2841
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A beforeSend() 0 16 4
B processResponse() 0 65 7
B processProperties() 0 28 2
A processDetails() 0 22 2
A processChannels() 0 19 3
A processPlayers() 0 19 3
1
<?php
2
/**
3
 * This file is part of GameQ.
4
 *
5
 * GameQ is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU Lesser General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * GameQ is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Lesser General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Lesser General Public License
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18
19
namespace GameQ\Protocols;
20
21
use GameQ\Protocol;
22
use GameQ\Buffer;
23
use GameQ\Result;
24
use GameQ\Server;
25
use GameQ\Exception\Protocol as Exception;
26
27
/**
28
 * Teamspeak 3 Protocol Class
29
 *
30
 * All values are utf8 encoded upon processing
31
 *
32
 * This code ported from GameQ v1/v2. Credit to original author(s) as I just updated it to
33
 * work within this new system.
34
 *
35
 * @author Austin Bischoff <[email protected]>
36
 */
37
class Teamspeak3 extends Protocol
38
{
39
40
    /**
41
     * Array of packets we want to look up.
42
     * Each key should correspond to a defined method in this or a parent class
43
     *
44
     * @type array
45
     */
46
    protected $packets = [
47
        self::PACKET_DETAILS  => "use port=%d\x0Aserverinfo\x0A",
48
        self::PACKET_PLAYERS  => "use port=%d\x0Aclientlist\x0A",
49
        self::PACKET_CHANNELS => "use port=%d\x0Achannellist -topic\x0A",
50
    ];
51
52
    /**
53
     * The transport mode for this protocol is TCP
54
     *
55
     * @type string
56
     */
57
    protected $transport = self::TRANSPORT_TCP;
58
59
    /**
60
     * The query protocol used to make the call
61
     *
62
     * @type string
63
     */
64
    protected $protocol = 'teamspeak3';
65
66
    /**
67
     * String name of this protocol class
68
     *
69
     * @type string
70
     */
71
    protected $name = 'teamspeak3';
72
73
    /**
74
     * Longer string name of this protocol class
75
     *
76
     * @type string
77
     */
78
    protected $name_long = "Teamspeak 3";
79
80
    /**
81
     * The client join link
82
     *
83
     * @type string
84
     */
85
    protected $join_link = "ts3server://%s?port=%d";
86
87
    /**
88
     * Normalize settings for this protocol
89
     *
90
     * @type array
91
     */
92
    protected $normalize = [
93
        // General
94
        'general' => [
95
            'dedicated'  => 'dedicated',
96
            'hostname'   => 'virtualserver_name',
97
            'password'   => 'virtualserver_flag_password',
98
            'numplayers' => 'numplayers',
99
            'maxplayers' => 'virtualserver_maxclients',
100
        ],
101
        // Player
102
        'player'  => [
103
            'id'   => 'clid',
104
            'team' => 'cid',
105
            'name' => 'client_nickname',
106
        ],
107
        // Team
108
        'team'    => [
109
            'id'   => 'cid',
110
            'name' => 'channel_name',
111
        ],
112
    ];
113
114
    /**
115
     * Before we send off the queries we need to update the packets
116
     *
117
     * @param \GameQ\Server $server
118
     *
119
     * @throws \GameQ\Exception\Protocol
120
     */
121 5
    public function beforeSend(Server $server)
122
    {
123
124
        // Check to make sure we have a query_port because it is required
125 5
        if (!isset($this->options[Server::SERVER_OPTIONS_QUERY_PORT])
126 5
            || empty($this->options[Server::SERVER_OPTIONS_QUERY_PORT])
127 5
        ) {
128 1
            throw new Exception(__METHOD__ . " Missing required setting '" . Server::SERVER_OPTIONS_QUERY_PORT . "'.");
129
        }
130
131
        // Let's loop the packets and set the proper pieces
132 4
        foreach ($this->packets as $packet_type => $packet) {
133
            // Update with the client port for the server
134 4
            $this->packets[$packet_type] = sprintf($packet, $server->portClient());
135 4
        }
136 4
    }
137
138
    /**
139
     * Process the response
140
     *
141
     * @return array
142
     * @throws \GameQ\Exception\Protocol
143
     */
144 3
    public function processResponse()
145
    {
146
147
        // Make a new buffer out of all of the packets
148 3
        $buffer = new Buffer(implode('', $this->packets_response));
149
150
        // Check the header TS3
151 3
        if (($header = trim($buffer->readString("\n"))) !== 'TS3') {
152 1
            throw new Exception(__METHOD__ . " Expected header '{$header}' does not match expected 'TS3'.");
153
        }
154
155
        // Convert all the escaped characters
156 2
        $raw = str_replace(
157
            [
158 2
                '\\\\', // Translate escaped \
159 2
                '\\/', // Translate escaped /
160 2
            ],
161
            [
162 2
                '\\',
163 2
                '/',
164 2
            ],
165
            [
166 2
                '\\p',
167 2
                '|',
168 2
            ],
169 2
            $buffer->getBuffer()
170 2
        );
171
172
        // Explode the sections and filter to remove empty, junk ones
173
        $sections = array_filter(explode("\n", $raw), function ($value) {
174
175
            $value = trim($value);
176
177
            // Not empty string or a message response for "error id=\d"
178
            return !empty($value) && substr($value, 0, 5) !== 'error';
179
        });
180
181
        // Trim up the values to remove extra whitespace
182
        $sections = array_map('trim', $sections);
183
184
        // Set the result to a new result instance
185
        $result = new Result();
186
187
        // Iterate over the sections and offload the parsing
188
        foreach ($sections as $section) {
189
            // Grab a snip of the data so we can figure out what it is
190
            $check = substr(trim($section), 0, 4);
191
192
            // Use the first part of the response to figure out where we need to go
193
            if ($check == 'virt') {
194
                // Server info
195
                $this->processDetails($section, $result);
196
            } elseif ($check == 'cid=') {
197
                // Channels
198
                $this->processChannels($section, $result);
199
            } elseif ($check == 'clid') {
200
                // Clients (players)
201
                $this->processPlayers($section, $result);
202
            }
203
        }
204
205
        unset($buffer, $sections, $section, $check);
206
207
        return $result->fetch();
208
    }
209
210
    /*
211
     * Internal methods
212
     */
213
214
    /**
215
     * Process the properties of the data.
216
     *
217
     * Takes data in "key1=value1 key2=value2 ..." and processes it into a usable format
218
     *
219
     * @param $data
220
     *
221
     * @return array
222
     */
223
    protected function processProperties($data)
224
    {
225
226
        // Will hold the properties we are sending back
227
        $properties = [ ];
228
229
        // All of these are split on space
230
        $items = explode(' ', $data);
231
232
        // Iterate over the items
233
        foreach ($items as $item) {
234
            // Explode and make sure we always have 2 items in the array
235
            list($key, $value) = array_pad(explode('=', $item, 2), 2, '');
236
237
            // Convert spaces and other character changes
238
            $properties[$key] = utf8_encode(str_replace(
239
                [
240
                    '\\s', // Translate spaces
241
                ],
242
                [
243
                    ' ',
244
                ],
245
                $value
246
            ));
247
        }
248
249
        return $properties;
250
    }
251
252
    /**
253
     * Handles processing the details data into a usable format
254
     *
255
     * @param string        $data
256
     * @param \GameQ\Result $result
257
     */
258
    protected function processDetails($data, Result &$result)
259
    {
260
261
        // Offload the parsing for these values
262
        $properties = $this->processProperties($data);
263
264
        // Always dedicated
265
        $result->add('dedicated', 1);
266
267
        // Iterate over the properties
268
        foreach ($properties as $key => $value) {
269
            $result->add($key, $value);
270
        }
271
272
        // We need to manually figure out the number of players
273
        $result->add(
274
            'numplayers',
275
            ($properties['virtualserver_clientsonline'] - $properties['virtualserver_queryclientsonline'])
276
        );
277
278
        unset($data, $properties, $key, $value);
279
    }
280
281
    /**
282
     * Process the channel listing
283
     *
284
     * @param string        $data
285
     * @param \GameQ\Result $result
286
     */
287
    protected function processChannels($data, Result &$result)
288
    {
289
290
        // We need to split the data at the pipe
291
        $channels = explode('|', $data);
292
293
        // Iterate over the channels
294
        foreach ($channels as $channel) {
295
            // Offload the parsing for these values
296
            $properties = $this->processProperties($channel);
297
298
            // Iterate over the properties
299
            foreach ($properties as $key => $value) {
300
                $result->addTeam($key, $value);
301
            }
302
        }
303
304
        unset($data, $channel, $channels, $properties, $key, $value);
305
    }
306
307
    /**
308
     * Process the user listing
309
     *
310
     * @param string        $data
311
     * @param \GameQ\Result $result
312
     */
313
    protected function processPlayers($data, Result &$result)
314
    {
315
316
        // We need to split the data at the pipe
317
        $players = explode('|', $data);
318
319
        // Iterate over the channels
320
        foreach ($players as $player) {
321
            // Offload the parsing for these values
322
            $properties = $this->processProperties($player);
323
324
            // Iterate over the properties
325
            foreach ($properties as $key => $value) {
326
                $result->addPlayer($key, $value);
327
            }
328
        }
329
330
        unset($data, $player, $players, $properties, $key, $value);
331
    }
332
}
333