Passed
Push — master ( 24138f...77ef50 )
by
02:49
created

Api::invalidate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Ely\Mojang;
5
6
use DateTime;
7
use Ely\Mojang\Middleware\ResponseConverterMiddleware;
8
use Ely\Mojang\Middleware\RetryMiddleware;
9
use GuzzleHttp\Client as GuzzleClient;
10
use GuzzleHttp\ClientInterface;
11
use GuzzleHttp\Exception\GuzzleException;
12
use GuzzleHttp\HandlerStack;
13
use GuzzleHttp\Psr7\Request;
14
use GuzzleHttp\Psr7\Uri;
15
use InvalidArgumentException;
16
use Ramsey\Uuid\Uuid;
17
18
class Api {
19
20
    /**
21
     * @var ClientInterface
22
     */
23
    private $client;
24
25 28
    public function setClient(ClientInterface $client): void {
26 28
        $this->client = $client;
27 28
    }
28
29
    /**
30
     * @return \Ely\Mojang\Response\ApiStatus[]
31
     *
32
     * @throws GuzzleException
33
     *
34
     * @url https://wiki.vg/Mojang_API#API_Status
35
     */
36 1
    public function apiStatus(): array {
37 1
        $response = $this->getClient()->request('GET', 'https://status.mojang.com/check');
38 1
        $body = $this->decode($response->getBody()->getContents());
39
40 1
        $result = [];
41 1
        foreach ($body as $serviceDeclaration) {
42 1
            $serviceName = array_keys($serviceDeclaration)[0];
43 1
            $result[$serviceName] = new Response\ApiStatus($serviceName, $serviceDeclaration[$serviceName]);
44
        }
45
46 1
        return $result;
47
    }
48
49
    /**
50
     * @param string $username
51
     * @param int    $atTime
52
     *
53
     * @return \Ely\Mojang\Response\ProfileInfo
54
     *
55
     * @throws \Ely\Mojang\Exception\MojangApiException
56
     * @throws \GuzzleHttp\Exception\GuzzleException
57
     *
58
     * @url http://wiki.vg/Mojang_API#Username_-.3E_UUID_at_time
59
     */
60 3
    public function usernameToUUID(string $username, int $atTime = null): Response\ProfileInfo {
61 3
        $query = [];
62 3
        if ($atTime !== null) {
63 1
            $query['atTime'] = $atTime;
64
        }
65
66 3
        $response = $this->getClient()->request('GET', "https://api.mojang.com/users/profiles/minecraft/{$username}", [
67 3
            'query' => $query,
68
        ]);
69
70 3
        $data = $this->decode($response->getBody()->getContents());
71
72 3
        return Response\ProfileInfo::createFromResponse($data);
73
    }
74
75
    /**
76
     * @param string $uuid
77
     *
78
     * @return \Ely\Mojang\Response\NameHistoryItem[]
79
     *
80
     * @throws GuzzleException
81
     *
82
     * @url https://wiki.vg/Mojang_API#UUID_-.3E_Name_history
83
     */
84 1
    public function uuidToNameHistory(string $uuid): array {
85 1
        $response = $this->getClient()->request('GET', "https://api.mojang.com/user/profiles/{$uuid}/names");
86 1
        $data = $this->decode($response->getBody()->getContents());
87
88 1
        $result = [];
89 1
        foreach ($data as $record) {
90 1
            $date = null;
91 1
            if (isset($record['changedToAt'])) {
92 1
                $date = new DateTime('@' . ($record['changedToAt'] / 1000));
93
            }
94
95 1
            $result[] = new Response\NameHistoryItem($record['name'], $date);
96
        }
97
98 1
        return $result;
99
    }
100
101
    /**
102
     * @param string $uuid
103
     *
104
     * @return \Ely\Mojang\Response\ProfileResponse
105
     *
106
     * @throws \Ely\Mojang\Exception\MojangApiException
107
     * @throws \GuzzleHttp\Exception\GuzzleException
108
     *
109
     * @url http://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
110
     */
111 2
    public function uuidToTextures(string $uuid): Response\ProfileResponse {
112 2
        $response = $this->getClient()->request('GET', "https://sessionserver.mojang.com/session/minecraft/profile/{$uuid}", [
113 2
            'query' => [
114
                'unsigned' => false,
115
            ],
116
        ]);
117 2
        $body = $this->decode($response->getBody()->getContents());
118
119 2
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
120
    }
121
122
    /**
123
     * Helper method to exchange username to the corresponding textures.
124
     *
125
     * @param string $username
126
     *
127
     * @return \Ely\Mojang\Response\ProfileResponse
128
     *
129
     * @throws GuzzleException
130
     * @throws \Ely\Mojang\Exception\MojangApiException
131
     */
132 1
    public function usernameToTextures(string $username): Response\ProfileResponse {
133 1
        return $this->uuidToTextures($this->usernameToUUID($username)->getId());
134
    }
135
136
    /**
137
     * @param string[] $names list of users' names
138
     *
139
     * @return \Ely\Mojang\Response\ProfileInfo[] response array is indexed with the initial username case
140
     *
141
     * @throws \Ely\Mojang\Exception\MojangApiException
142
     * @throws \GuzzleHttp\Exception\GuzzleException
143
     *
144
     * @url https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs
145
     */
146 2
    public function playernamesToUuids(array $names): array {
147 2
        foreach ($names as $i => $name) {
148 2
            if (empty($name)) {
149 2
                unset($names[$i]);
150
            }
151
        }
152
153 2
        if (count($names) > 100) {
154 1
            throw new InvalidArgumentException('You cannot request more than 100 names per request');
155
        }
156
157 1
        $response = $this->getClient()->request('POST', 'https://api.mojang.com/profiles/minecraft', [
158 1
            'json' => array_values($names),
159
        ]);
160 1
        $body = $this->decode($response->getBody()->getContents());
161
162 1
        $result = [];
163 1
        foreach ($body as $record) {
164 1
            $object = Response\ProfileInfo::createFromResponse($record);
165 1
            $key = $object->getName();
166 1
            foreach ($names as $i => $name) {
167 1
                if (mb_strtolower($name) === mb_strtolower($object->getName())) {
168 1
                    unset($names[$i]);
169 1
                    $key = $name;
170 1
                    break;
171
                }
172
            }
173
174 1
            $result[$key] = $object;
175
        }
176
177 1
        return $result;
178
    }
179
180
    /**
181
     * @param string $accessToken
182
     * @param string $accountUuid
183
     * @param string $skinUrl
184
     * @param bool $isSlim
185
     *
186
     * @throws \Ely\Mojang\Exception\MojangApiException
187
     * @throws GuzzleException
188
     *
189
     * @url https://wiki.vg/Mojang_API#Change_Skin
190
     */
191 2
    public function changeSkin(string $accessToken, string $accountUuid, string $skinUrl, bool $isSlim): void {
192 2
        $this->getClient()->request('POST', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
193
            'form_params' => [
194 2
                'model' => $isSlim ? 'slim' : '',
195 2
                'url' => $skinUrl,
196
            ],
197
            'headers' => [
198 2
                'Authorization' => 'Bearer ' . $accessToken,
199
            ],
200
        ]);
201 2
    }
202
203
    /**
204
     * @param string $accessToken
205
     * @param string $accountUuid
206
     * @param \Psr\Http\Message\StreamInterface|resource|string $skinContents
207
     * @param bool $isSlim
208
     *
209
     * @throws GuzzleException
210
     *
211
     * @url https://wiki.vg/Mojang_API#Upload_Skin
212
     */
213 2
    public function uploadSkin(string $accessToken, string $accountUuid, $skinContents, bool $isSlim): void {
214 2
        $this->getClient()->request('PUT', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
215
            'multipart' => [
216
                [
217 2
                    'name' => 'file',
218 2
                    'contents' => $skinContents,
219 2
                    'filename' => 'char.png',
220
                ],
221
                [
222 2
                    'name' => 'model',
223 2
                    'contents' => $isSlim ? 'slim' : '',
224
                ],
225
            ],
226
            'headers' => [
227 2
                'Authorization' => 'Bearer ' . $accessToken,
228
            ],
229
        ]);
230 2
    }
231
232
    /**
233
     * @param string $accessToken
234
     * @param string $accountUuid
235
     *
236
     * @throws \Ely\Mojang\Exception\MojangApiException
237
     * @throws GuzzleException
238
     *
239
     * @url https://wiki.vg/Mojang_API#Reset_Skin
240
     */
241 1
    public function resetSkin(string $accessToken, string $accountUuid): void {
242 1
        $this->getClient()->request('DELETE', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
243
            'headers' => [
244 1
                'Authorization' => 'Bearer ' . $accessToken,
245
            ],
246
        ]);
247 1
    }
248
249
    /**
250
     * @return Response\BlockedServersCollection
251
     *
252
     * @throws GuzzleException
253
     *
254
     * @url https://wiki.vg/Mojang_API#Blocked_Servers
255
     */
256 1
    public function blockedServers(): Response\BlockedServersCollection {
257 1
        $response = $this->getClient()->request('GET', 'https://sessionserver.mojang.com/blockedservers');
258 1
        $hashes = explode("\n", trim($response->getBody()->getContents()));
259
260 1
        return new Response\BlockedServersCollection($hashes);
261
    }
262
263
    /**
264
     * @param string $login
265
     * @param string $password
266
     * @param string $clientToken
267
     *
268
     * @return \Ely\Mojang\Response\AuthenticateResponse
269
     *
270
     * @throws \GuzzleHttp\Exception\GuzzleException
271
     *
272
     * @url https://wiki.vg/Authentication#Authenticate
273
     */
274 3
    public function authenticate(
275
        string $login,
276
        string $password,
277
        string $clientToken = null
278
    ): Response\AuthenticateResponse {
279 3
        if ($clientToken === null) {
280
            /** @noinspection PhpUnhandledExceptionInspection */
281 2
            $clientToken = Uuid::uuid4()->toString();
282
        }
283
284 3
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
285 3
            'json' => [
286 3
                'username' => $login,
287 3
                'password' => $password,
288 3
                'clientToken' => $clientToken,
289
                'requestUser' => true,
290
                'agent' => [
291
                    'name' => 'Minecraft',
292
                    'version' => 1,
293
                ],
294
            ],
295
        ]);
296 2
        $body = $this->decode($response->getBody()->getContents());
297
298 2
        return new Response\AuthenticateResponse(
299 2
            $body['accessToken'],
300 2
            $body['clientToken'],
301 2
            $body['availableProfiles'],
302 2
            $body['selectedProfile'],
303 2
            $body['user']
304
        );
305
    }
306
307
    /**
308
     * @param string $accessToken
309
     * @param string $clientToken
310
     *
311
     * @return \Ely\Mojang\Response\AuthenticateResponse
312
     *
313
     * @throws \GuzzleHttp\Exception\GuzzleException
314
     *
315
     * @url https://wiki.vg/Authentication#Refresh
316
     */
317 2
    public function refresh(string $accessToken, string $clientToken): Response\AuthenticateResponse {
318 2
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/refresh', [
319 2
            'json' => [
320 2
                'accessToken' => $accessToken,
321 2
                'clientToken' => $clientToken,
322
                'requestUser' => true,
323
            ],
324
        ]);
325 1
        $body = $this->decode($response->getBody()->getContents());
326
327 1
        return new Response\AuthenticateResponse(
328 1
            $body['accessToken'],
329 1
            $body['clientToken'],
330 1
            [],
331 1
            $body['selectedProfile'],
332 1
            $body['user']
333
        );
334
    }
335
336
    /**
337
     * @param string $accessToken
338
     *
339
     * @return bool
340
     *
341
     * @throws \GuzzleHttp\Exception\GuzzleException
342
     *
343
     * @url https://wiki.vg/Authentication#Validate
344
     */
345 2
    public function validate(string $accessToken): bool {
346
        try {
347 2
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/validate', [
348
                'json' => [
349 2
                    'accessToken' => $accessToken,
350
                ],
351
            ]);
352 1
            if ($response->getStatusCode() === 204) {
353 1
                return true;
354
            }
355 1
        } catch (Exception\ForbiddenException $e) {
356
            // Suppress exception and let it just exit below
357
        }
358
359 1
        return false;
360
    }
361
362
    /**
363
     * @param string $accessToken
364
     * @param string $clientToken
365
     *
366
     * @throws GuzzleException
367
     *
368
     * @url https://wiki.vg/Authentication#Invalidate
369
     */
370 1
    public function invalidate(string $accessToken, string $clientToken): void {
371 1
        $this->getClient()->request('POST', 'https://authserver.mojang.com/invalidate', [
372
            'json' => [
373 1
                'accessToken' => $accessToken,
374 1
                'clientToken' => $clientToken,
375
            ],
376
        ]);
377 1
    }
378
379
    /**
380
     * @param string $login
381
     * @param string $password
382
     *
383
     * @return bool
384
     *
385
     * @throws GuzzleException
386
     *
387
     * @url https://wiki.vg/Authentication#Signout
388
     */
389 2
    public function signout(string $login, string $password): bool {
390
        try {
391 2
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/signout', [
392
                'json' => [
393 2
                    'username' => $login,
394 2
                    'password' => $password,
395
                ],
396
            ]);
397 1
            if ($response->getStatusCode() === 204) {
398 1
                return true;
399
            }
400 1
        } catch (Exception\ForbiddenException $e) {
401
            // Suppress exception and let it just exit below
402
        }
403
404 1
        return false;
405
    }
406
407
    /**
408
     * @param string $accessToken
409
     * @param string $accountUuid
410
     * @param string $serverId
411
     *
412
     * @throws GuzzleException
413
     *
414
     * @url https://wiki.vg/Protocol_Encryption#Client
415
     */
416 1
    public function joinServer(string $accessToken, string $accountUuid, string $serverId): void {
417 1
        $this->getClient()->request('POST', 'https://sessionserver.mojang.com/session/minecraft/join', [
418
            'json' => [
419 1
                'accessToken' => $accessToken,
420 1
                'selectedProfile' => $accountUuid,
421 1
                'serverId' => $serverId,
422
            ],
423
        ]);
424 1
    }
425
426
    /**
427
     * @param string $username
428
     * @param string $serverId
429
     *
430
     * @return \Ely\Mojang\Response\ProfileResponse
431
     *
432
     * @throws \Ely\Mojang\Exception\NoContentException
433
     * @throws GuzzleException
434
     *
435
     * @url https://wiki.vg/Protocol_Encryption#Server
436
     */
437 2
    public function hasJoinedServer(string $username, string $serverId): Response\ProfileResponse {
438 2
        $uri = (new Uri('https://sessionserver.mojang.com/session/minecraft/hasJoined'))
439 2
            ->withQuery(http_build_query([
440 2
                'username' => $username,
441 2
                'serverId' => $serverId,
442 2
            ], '', '&', PHP_QUERY_RFC3986));
443 2
        $request = new Request('GET', $uri);
444 2
        $response = $this->getClient()->send($request);
445 2
        $rawBody = $response->getBody()->getContents();
446 2
        if (empty($rawBody)) {
447 1
            throw new Exception\NoContentException($request, $response);
448
        }
449
450 1
        $body = $this->decode($rawBody);
451
452 1
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
453
    }
454
455
    /**
456
     * @return ClientInterface
457
     */
458 27
    protected function getClient(): ClientInterface {
459 27
        if ($this->client === null) {
460 1
            $this->client = $this->createDefaultClient();
461
        }
462
463 27
        return $this->client;
464
    }
465
466 1
    private function createDefaultClient(): ClientInterface {
467 1
        $stack = HandlerStack::create();
468
        // use after method because middleware executes in reverse order
469 1
        $stack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_response_converter');
470 1
        $stack->push(RetryMiddleware::create(), 'retry');
471
472 1
        return new GuzzleClient([
473 1
            'handler' => $stack,
474 1
            'timeout' => 10,
475
        ]);
476
    }
477
478 11
    private function decode(string $response): array {
479 11
        return json_decode($response, true);
480
    }
481
482
}
483