Api::decode()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 2
ccs 2
cts 2
cp 1
crap 1
rs 10
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 Ely\Mojang\Response\QuestionResponse;
10
use GuzzleHttp\Client as GuzzleClient;
11
use GuzzleHttp\ClientInterface;
12
use GuzzleHttp\Exception\GuzzleException;
13
use GuzzleHttp\HandlerStack;
14
use GuzzleHttp\Psr7\Request;
15
use GuzzleHttp\Psr7\Uri;
16
use GuzzleHttp\RequestOptions;
17
use InvalidArgumentException;
18
use Ramsey\Uuid\Uuid;
19
20
class Api {
21
22
    /**
23
     * @var ClientInterface
24
     */
25
    private $client;
26
27 37
    public function setClient(ClientInterface $client): void {
28 37
        $this->client = $client;
29 37
    }
30
31
    /**
32
     * @return \Ely\Mojang\Response\ApiStatus[]
33
     *
34
     * @throws GuzzleException
35
     *
36
     * @url https://wiki.vg/Mojang_API#API_Status
37
     */
38 1
    public function apiStatus(): array {
39 1
        $response = $this->getClient()->request('GET', 'https://status.mojang.com/check');
40 1
        $body = $this->decode($response->getBody()->getContents());
41
42 1
        $result = [];
43 1
        foreach ($body as $serviceDeclaration) {
44 1
            $serviceName = array_keys($serviceDeclaration)[0];
45 1
            $result[$serviceName] = new Response\ApiStatus($serviceName, $serviceDeclaration[$serviceName]);
46
        }
47
48 1
        return $result;
49
    }
50
51
    /**
52
     * @param string $username
53
     * @param int    $atTime
54
     *
55
     * @return \Ely\Mojang\Response\ProfileInfo
56
     *
57
     * @throws \Ely\Mojang\Exception\MojangApiException
58
     * @throws \GuzzleHttp\Exception\GuzzleException
59
     *
60
     * @url http://wiki.vg/Mojang_API#Username_-.3E_UUID_at_time
61
     */
62 3
    public function usernameToUUID(string $username, int $atTime = null): Response\ProfileInfo {
63 3
        $query = [];
64 3
        if ($atTime !== null) {
65 1
            $query['atTime'] = $atTime;
66
        }
67
68 3
        $response = $this->getClient()->request('GET', "https://api.mojang.com/users/profiles/minecraft/{$username}", [
69 3
            'query' => $query,
70
        ]);
71
72 3
        $data = $this->decode($response->getBody()->getContents());
73
74 3
        return Response\ProfileInfo::createFromResponse($data);
75
    }
76
77
    /**
78
     * @param string $uuid
79
     *
80
     * @return \Ely\Mojang\Response\NameHistoryItem[]
81
     *
82
     * @throws GuzzleException
83
     *
84
     * @url https://wiki.vg/Mojang_API#UUID_-.3E_Name_history
85
     */
86 1
    public function uuidToNameHistory(string $uuid): array {
87 1
        $response = $this->getClient()->request('GET', "https://api.mojang.com/user/profiles/{$uuid}/names");
88 1
        $data = $this->decode($response->getBody()->getContents());
89
90 1
        $result = [];
91 1
        foreach ($data as $record) {
92 1
            $date = null;
93 1
            if (isset($record['changedToAt'])) {
94 1
                $date = new DateTime('@' . ($record['changedToAt'] / 1000));
95
            }
96
97 1
            $result[] = new Response\NameHistoryItem($record['name'], $date);
98
        }
99
100 1
        return $result;
101
    }
102
103
    /**
104
     * @param string $uuid
105
     *
106
     * @return \Ely\Mojang\Response\ProfileResponse
107
     *
108
     * @throws \Ely\Mojang\Exception\MojangApiException
109
     * @throws \GuzzleHttp\Exception\GuzzleException
110
     *
111
     * @url http://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
112
     */
113 2
    public function uuidToTextures(string $uuid): Response\ProfileResponse {
114 2
        $response = $this->getClient()->request('GET', "https://sessionserver.mojang.com/session/minecraft/profile/{$uuid}", [
115 2
            'query' => [
116
                'unsigned' => false,
117
            ],
118
        ]);
119 2
        $body = $this->decode($response->getBody()->getContents());
120
121 2
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
122
    }
123
124
    /**
125
     * Helper method to exchange username to the corresponding textures.
126
     *
127
     * @param string $username
128
     *
129
     * @return \Ely\Mojang\Response\ProfileResponse
130
     *
131
     * @throws GuzzleException
132
     * @throws \Ely\Mojang\Exception\MojangApiException
133
     */
134 1
    public function usernameToTextures(string $username): Response\ProfileResponse {
135 1
        return $this->uuidToTextures($this->usernameToUUID($username)->getId());
136
    }
137
138
    /**
139
     * @param string[] $names list of users' names
140
     *
141
     * @return \Ely\Mojang\Response\ProfileInfo[] response array is indexed with the initial username case
142
     *
143
     * @throws \Ely\Mojang\Exception\MojangApiException
144
     * @throws \GuzzleHttp\Exception\GuzzleException
145
     *
146
     * @url https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs
147
     */
148 2
    public function playernamesToUuids(array $names): array {
149 2
        foreach ($names as $i => $name) {
150 2
            if (empty($name)) {
151 2
                unset($names[$i]);
152
            }
153
        }
154
155 2
        if (count($names) > 10) {
156 1
            throw new InvalidArgumentException('You cannot request more than 10 names per request');
157
        }
158
159 1
        $response = $this->getClient()->request('POST', 'https://api.mojang.com/profiles/minecraft', [
160 1
            'json' => array_values($names),
161
        ]);
162 1
        $body = $this->decode($response->getBody()->getContents());
163
164 1
        $result = [];
165 1
        foreach ($body as $record) {
166 1
            $object = Response\ProfileInfo::createFromResponse($record);
167 1
            $key = $object->getName();
168 1
            foreach ($names as $i => $name) {
169 1
                if (mb_strtolower($name) === mb_strtolower($object->getName())) {
170 1
                    unset($names[$i]);
171 1
                    $key = $name;
172 1
                    break;
173
                }
174
            }
175
176 1
            $result[$key] = $object;
177
        }
178
179 1
        return $result;
180
    }
181
182
    /**
183
     * @param string $accessToken
184
     * @param string $accountUuid
185
     * @param string $skinUrl
186
     * @param bool $isSlim
187
     *
188
     * @throws \Ely\Mojang\Exception\MojangApiException
189
     * @throws GuzzleException
190
     *
191
     * @url https://wiki.vg/Mojang_API#Change_Skin
192
     */
193 2
    public function changeSkin(string $accessToken, string $accountUuid, string $skinUrl, bool $isSlim): void {
194 2
        $this->getClient()->request('POST', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
195
            'form_params' => [
196 2
                'model' => $isSlim ? 'slim' : '',
197 2
                'url' => $skinUrl,
198
            ],
199
            'headers' => [
200 2
                'Authorization' => 'Bearer ' . $accessToken,
201
            ],
202
        ]);
203 2
    }
204
205
    /**
206
     * @param string $accessToken
207
     * @param string $accountUuid
208
     * @param \Psr\Http\Message\StreamInterface|resource|string $skinContents
209
     * @param bool $isSlim
210
     *
211
     * @throws GuzzleException
212
     *
213
     * @deprecated
214
     * @url https://wiki.vg/Mojang_API#Upload_Skin
215
     */
216 2
    public function uploadSkin(string $accessToken, string $accountUuid, $skinContents, bool $isSlim): void {
217 2
        $this->getClient()->request('PUT', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
218
            'multipart' => [
219
                [
220 2
                    'name' => 'file',
221 2
                    'contents' => $skinContents,
222 2
                    'filename' => 'char.png',
223
                ],
224
                [
225 2
                    'name' => 'model',
226 2
                    'contents' => $isSlim ? 'slim' : '',
227
                ],
228
            ],
229
            'headers' => [
230 2
                'Authorization' => 'Bearer ' . $accessToken,
231
            ],
232
        ]);
233 2
    }
234
235
    /**
236
     * @param string $accessToken
237
     * @param string $accountUuid
238
     *
239
     * @throws \Ely\Mojang\Exception\MojangApiException
240
     * @throws GuzzleException
241
     *
242
     * @url https://wiki.vg/Mojang_API#Reset_Skin
243
     */
244 1
    public function resetSkin(string $accessToken, string $accountUuid): void {
245 1
        $this->getClient()->request('DELETE', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
246
            'headers' => [
247 1
                'Authorization' => 'Bearer ' . $accessToken,
248
            ],
249
        ]);
250 1
    }
251
252
    /**
253
     * @return Response\BlockedServersCollection
254
     *
255
     * @throws GuzzleException
256
     *
257
     * @url https://wiki.vg/Mojang_API#Blocked_Servers
258
     */
259 1
    public function blockedServers(): Response\BlockedServersCollection {
260 1
        $response = $this->getClient()->request('GET', 'https://sessionserver.mojang.com/blockedservers');
261 1
        $hashes = explode("\n", trim($response->getBody()->getContents()));
262
263 1
        return new Response\BlockedServersCollection($hashes);
264
    }
265
266
    /**
267
     * @param string $login
268
     * @param string $password
269
     * @param string $clientToken
270
     *
271
     * @return \Ely\Mojang\Response\AuthenticateResponse
272
     *
273
     * @throws \GuzzleHttp\Exception\GuzzleException
274
     *
275
     * @url https://wiki.vg/Authentication#Authenticate
276
     */
277 3
    public function authenticate(
278
        string $login,
279
        string $password,
280
        string $clientToken = null
281
    ): Response\AuthenticateResponse {
282 3
        if ($clientToken === null) {
283
            /** @noinspection PhpUnhandledExceptionInspection */
284 2
            $clientToken = Uuid::uuid4()->toString();
285
        }
286
287 3
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
288 3
            'json' => [
289 3
                'username' => $login,
290 3
                'password' => $password,
291 3
                'clientToken' => $clientToken,
292
                'requestUser' => true,
293
                'agent' => [
294
                    'name' => 'Minecraft',
295
                    'version' => 1,
296
                ],
297
            ],
298
        ]);
299 2
        $body = $this->decode($response->getBody()->getContents());
300
301 2
        return new Response\AuthenticateResponse(
302 2
            $body['accessToken'],
303 2
            $body['clientToken'],
304 2
            $body['availableProfiles'],
305 2
            $body['selectedProfile'],
306 2
            $body['user']
307
        );
308
    }
309
310
    /**
311
     * @param string $accessToken
312
     * @param string $clientToken
313
     *
314
     * @return \Ely\Mojang\Response\AuthenticateResponse
315
     *
316
     * @throws \GuzzleHttp\Exception\GuzzleException
317
     *
318
     * @url https://wiki.vg/Authentication#Refresh
319
     */
320 2
    public function refresh(string $accessToken, string $clientToken): Response\AuthenticateResponse {
321 2
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/refresh', [
322 2
            'json' => [
323 2
                'accessToken' => $accessToken,
324 2
                'clientToken' => $clientToken,
325
                'requestUser' => true,
326
            ],
327
        ]);
328 1
        $body = $this->decode($response->getBody()->getContents());
329
330 1
        return new Response\AuthenticateResponse(
331 1
            $body['accessToken'],
332 1
            $body['clientToken'],
333 1
            [],
334 1
            $body['selectedProfile'],
335 1
            $body['user']
336
        );
337
    }
338
339
    /**
340
     * @param string $accessToken
341
     *
342
     * @return bool
343
     *
344
     * @throws \GuzzleHttp\Exception\GuzzleException
345
     *
346
     * @url https://wiki.vg/Authentication#Validate
347
     */
348 2
    public function validate(string $accessToken): bool {
349
        try {
350 2
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/validate', [
351
                'json' => [
352 2
                    'accessToken' => $accessToken,
353
                ],
354
            ]);
355 1
            if ($response->getStatusCode() === 204) {
356 1
                return true;
357
            }
358 1
        } catch (Exception\ForbiddenException $e) {
359
            // Suppress exception and let it just exit below
360
        }
361
362 1
        return false;
363
    }
364
365
    /**
366
     * @param string $accessToken
367
     * @param string $clientToken
368
     *
369
     * @throws GuzzleException
370
     *
371
     * @url https://wiki.vg/Authentication#Invalidate
372
     */
373 1
    public function invalidate(string $accessToken, string $clientToken): void {
374 1
        $this->getClient()->request('POST', 'https://authserver.mojang.com/invalidate', [
375
            'json' => [
376 1
                'accessToken' => $accessToken,
377 1
                'clientToken' => $clientToken,
378
            ],
379
        ]);
380 1
    }
381
382
    /**
383
     * @param string $login
384
     * @param string $password
385
     *
386
     * @return bool
387
     *
388
     * @throws GuzzleException
389
     *
390
     * @url https://wiki.vg/Authentication#Signout
391
     */
392 2
    public function signout(string $login, string $password): bool {
393
        try {
394 2
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/signout', [
395
                'json' => [
396 2
                    'username' => $login,
397 2
                    'password' => $password,
398
                ],
399
            ]);
400 1
            if ($response->getStatusCode() === 204) {
401 1
                return true;
402
            }
403 1
        } catch (Exception\ForbiddenException $e) {
404
            // Suppress exception and let it just exit below
405
        }
406
407 1
        return false;
408
    }
409
410
    /**
411
     * @param string $accessToken
412
     * @param string $accountUuid
413
     * @param string $serverId
414
     *
415
     * @throws GuzzleException
416
     *
417
     * @url https://wiki.vg/Protocol_Encryption#Client
418
     */
419 1
    public function joinServer(string $accessToken, string $accountUuid, string $serverId): void {
420 1
        $this->getClient()->request('POST', 'https://sessionserver.mojang.com/session/minecraft/join', [
421
            'json' => [
422 1
                'accessToken' => $accessToken,
423 1
                'selectedProfile' => $accountUuid,
424 1
                'serverId' => $serverId,
425
            ],
426
        ]);
427 1
    }
428
429
    /**
430
     * @param string $username
431
     * @param string $serverId
432
     *
433
     * @return \Ely\Mojang\Response\ProfileResponse
434
     *
435
     * @throws \Ely\Mojang\Exception\NoContentException
436
     * @throws GuzzleException
437
     *
438
     * @url https://wiki.vg/Protocol_Encryption#Server
439
     */
440 2
    public function hasJoinedServer(string $username, string $serverId): Response\ProfileResponse {
441 2
        $uri = (new Uri('https://sessionserver.mojang.com/session/minecraft/hasJoined'))
442 2
            ->withQuery(http_build_query([
443 2
                'username' => $username,
444 2
                'serverId' => $serverId,
445 2
            ], '', '&', PHP_QUERY_RFC3986));
446 2
        $request = new Request('GET', $uri);
447 2
        $response = $this->getClient()->send($request);
448 2
        $rawBody = $response->getBody()->getContents();
449 2
        if (empty($rawBody)) {
450 1
            throw new Exception\NoContentException($request, $response);
451
        }
452
453 1
        $body = $this->decode($rawBody);
454
455 1
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
456
    }
457
458
    /**
459
     * @param string $accessToken
460
     * @throws GuzzleException
461
     *
462
     * @url https://wiki.vg/Mojang_API#Check_if_security_questions_are_needed
463
     */
464 3
    public function isSecurityQuestionsNeeded(string $accessToken): void {
465 3
        $request = new Request(
466 3
            'GET',
467 3
            'https://api.mojang.com/user/security/location',
468 3
            ['Authorization' => 'Bearer ' . $accessToken]
469
        );
470 3
        $response = $this->getClient()->send($request);
471 1
        $rawBody = $response->getBody()->getContents();
472 1
        if (!empty($rawBody)) {
473 1
            $body = $this->decode($rawBody);
474 1
            throw new Exception\OperationException($body['errorMessage'], $request, $response);
475
        }
476
    }
477
478
    /**
479
     * @param string $accessToken
480
     * @return array
481
     * @throws GuzzleException
482
     *
483
     * @url https://wiki.vg/Mojang_API#Get_list_of_questions
484
     */
485 1
    public function questions(string $accessToken): array {
486 1
        $request = new Request(
487 1
            'GET',
488 1
            'https://api.mojang.com/user/security/challenges',
489 1
            ['Authorization' => 'Bearer ' . $accessToken]
490
        );
491 1
        $response = $this->getClient()->send($request);
492 1
        $result = [];
493 1
        $body = $this->decode($response->getBody()->getContents());
494 1
        foreach ($body as $question) {
495 1
            $result[] = new QuestionResponse($question['question']['id'], $question['question']['question'], $question['answer']['id']);
496
        }
497
498 1
        return $result;
499
    }
500
501
    /**
502
     * @param string $accessToken
503
     * @param array $answers
504
     * @throws GuzzleException
505
     * @return bool
506
     *
507
     * @url https://wiki.vg/Mojang_API#Send_back_the_answers
508
     */
509 1
    public function answer(string $accessToken, array $answers): bool {
510 1
        $request = new Request(
511 1
            'POST',
512 1
            'https://api.mojang.com/user/security/location',
513 1
            ['Authorization' => 'Bearer ' . $accessToken],
514 1
            json_encode($answers)
515
        );
516 1
        $response = $this->getClient()->send($request);
517 1
        $rawBody = $response->getBody()->getContents();
518
519 1
        return empty($rawBody);
520
    }
521
522
    /**
523
     * @param array $metricKeys
524
     * @return Response\StatisticsResponse
525
     * @throws GuzzleException
526
     *
527
     * @url https://wiki.vg/Mojang_API#Statistics
528
     */
529 1
    public function statistics(array $metricKeys) { // TODO: missing return type annotation
530 1
        $response = $this->getClient()->request('POST', 'https://api.mojang.com/orders/statistics', [
531
            'json' => [
532 1
                'metricKeys' => $metricKeys,
533
            ],
534
        ]);
535 1
        $body = $this->decode($response->getBody()->getContents());
536
537 1
        return new Response\StatisticsResponse($body['total'], $body['last24h'], $body['saleVelocityPerSeconds']);
538
    }
539
540
    /**
541
     * @param string $accessToken
542
     * @return \Ely\Mojang\Response\MinecraftServicesProfileResponse
543
     * @throws GuzzleException
544
     * @see https://wiki.vg/Mojang_API#Profile_Information
545
     */
546 1
    public function getProfile(string $accessToken): Response\MinecraftServicesProfileResponse {
547 1
        $response = $this->getClient()->request('GET', 'https://api.minecraftservices.com/minecraft/profile', [
548 1
            RequestOptions::HEADERS => [
549 1
                'Authorization' => 'Bearer ' . $accessToken,
550
            ],
551
        ]);
552 1
        $body = $this->decode($response->getBody()->getContents());
553
554 1
        return new Response\MinecraftServicesProfileResponse(
555 1
            $body['id'],
556 1
            $body['name'],
557
            array_map(function(array $item) {
558 1
                return new Response\MinecraftServicesProfileSkin(
559 1
                    $item['id'],
560 1
                    $item['state'],
561 1
                    $item['url'],
562 1
                    $item['variant'],
563 1
                    $item['alias'] ?? null
564
                );
565 1
            }, $body['skins']),
566
            array_map(function(array $item) {
567 1
                return new Response\MinecraftServicesProfileCape(
568 1
                    $item['id'],
569 1
                    $item['state'],
570 1
                    $item['url'],
571 1
                    $item['alias']
572
                );
573 1
            }, $body['capes'])
574
        );
575
    }
576
577
    /**
578
     * @param string $accessToken
579
     * @param \Psr\Http\Message\StreamInterface|resource|string $skinContents
580
     * @param bool $isSlim
581
     *
582
     * @throws GuzzleException
583
     *
584
     * @url https://wiki.vg/Mojang_API#Upload_Skin
585
     */
586 1
    public function uploadSkinByFile(string $accessToken, $skinContents, bool $isSlim): void {
587 1
        $this->getClient()->request('POST', 'https://api.minecraftservices.com/minecraft/profile/skins', [
588 1
            RequestOptions::MULTIPART => [
589
                [
590 1
                    'name' => 'file',
591 1
                    'contents' => $skinContents,
592 1
                    'filename' => 'char.png',
593
                ],
594
                [
595 1
                    'name' => 'variant',
596 1
                    'contents' => $isSlim ? 'slim' : 'classic',
597
                ],
598
            ],
599 1
            RequestOptions::HEADERS => [
600 1
                'Authorization' => 'Bearer ' . $accessToken,
601
            ],
602
        ]);
603 1
    }
604
605
    /**
606
     * @param string $accessToken
607
     * @param string $skinUrl
608
     * @param bool $isSlim
609
     *
610
     * @throws GuzzleException
611
     *
612
     * @url https://wiki.vg/Mojang_API#Change_Skin
613
     */
614 1
    public function uploadSkinByUrl(string $accessToken, string $skinUrl, bool $isSlim): void {
615 1
        $this->getClient()->request('POST', 'https://api.minecraftservices.com/minecraft/profile/skins', [
616 1
            RequestOptions::JSON => [
617 1
                'variant' => $isSlim ? 'slim' : 'classic',
618 1
                'url' => $skinUrl,
619
            ],
620 1
            RequestOptions::HEADERS => [
621 1
                'Authorization' => 'Bearer ' . $accessToken,
622
            ],
623
        ]);
624 1
    }
625
626
    /**
627
     * @return ClientInterface
628
     */
629 36
    protected function getClient(): ClientInterface {
630 36
        if ($this->client === null) {
631 1
            $this->client = $this->createDefaultClient();
632
        }
633
634 36
        return $this->client;
635
    }
636
637 1
    private function createDefaultClient(): ClientInterface {
638 1
        $stack = HandlerStack::create();
639
        // use after method because middleware executes in reverse order
640 1
        $stack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_response_converter');
641 1
        $stack->push(RetryMiddleware::create(), 'retry');
642
643 1
        return new GuzzleClient([
644 1
            'handler' => $stack,
645 1
            'timeout' => 10,
646
        ]);
647
    }
648
649 15
    private function decode(string $response): array {
650 15
        return json_decode($response, true);
651
    }
652
653
}
654