Passed
Pull Request — master (#2)
by
unknown
05:52
created

Api::isSecurityQuestionsNeeded()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
ccs 8
cts 8
cp 1
crap 2
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\AnswerResponse;
10
use Ely\Mojang\Response\QuestionResponse;
11
use GuzzleHttp\Client as GuzzleClient;
12
use GuzzleHttp\ClientInterface;
13
use GuzzleHttp\Exception\GuzzleException;
14
use GuzzleHttp\HandlerStack;
15
use GuzzleHttp\Psr7\Request;
16
use GuzzleHttp\Psr7\Uri;
17
use InvalidArgumentException;
18
use Ramsey\Uuid\Uuid;
19
20
class Api {
21
22
    /**
23
     * @var ClientInterface
24
     */
25
    private $client;
26
27 34
    public function setClient(ClientInterface $client): void {
28 34
        $this->client = $client;
29 34
    }
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
     * @url https://wiki.vg/Mojang_API#Upload_Skin
214
     */
215 2
    public function uploadSkin(string $accessToken, string $accountUuid, $skinContents, bool $isSlim): void {
216 2
        $this->getClient()->request('PUT', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
217
            'multipart' => [
218
                [
219 2
                    'name' => 'file',
220 2
                    'contents' => $skinContents,
221 2
                    'filename' => 'char.png',
222
                ],
223
                [
224 2
                    'name' => 'model',
225 2
                    'contents' => $isSlim ? 'slim' : '',
226
                ],
227
            ],
228
            'headers' => [
229 2
                'Authorization' => 'Bearer ' . $accessToken,
230
            ],
231
        ]);
232 2
    }
233
234
    /**
235
     * @param string $accessToken
236
     * @param string $accountUuid
237
     *
238
     * @throws \Ely\Mojang\Exception\MojangApiException
239
     * @throws GuzzleException
240
     *
241
     * @url https://wiki.vg/Mojang_API#Reset_Skin
242
     */
243 1
    public function resetSkin(string $accessToken, string $accountUuid): void {
244 1
        $this->getClient()->request('DELETE', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
245
            'headers' => [
246 1
                'Authorization' => 'Bearer ' . $accessToken,
247
            ],
248
        ]);
249 1
    }
250
251
    /**
252
     * @return Response\BlockedServersCollection
253
     *
254
     * @throws GuzzleException
255
     *
256
     * @url https://wiki.vg/Mojang_API#Blocked_Servers
257
     */
258 1
    public function blockedServers(): Response\BlockedServersCollection {
259 1
        $response = $this->getClient()->request('GET', 'https://sessionserver.mojang.com/blockedservers');
260 1
        $hashes = explode("\n", trim($response->getBody()->getContents()));
261
262 1
        return new Response\BlockedServersCollection($hashes);
263
    }
264
265
    /**
266
     * @param string $login
267
     * @param string $password
268
     * @param string $clientToken
269
     *
270
     * @return \Ely\Mojang\Response\AuthenticateResponse
271
     *
272
     * @throws \GuzzleHttp\Exception\GuzzleException
273
     *
274
     * @url https://wiki.vg/Authentication#Authenticate
275
     */
276 3
    public function authenticate(
277
        string $login,
278
        string $password,
279
        string $clientToken = null
280
    ): Response\AuthenticateResponse {
281 3
        if ($clientToken === null) {
282
            /** @noinspection PhpUnhandledExceptionInspection */
283 2
            $clientToken = Uuid::uuid4()->toString();
284
        }
285
286 3
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
287 3
            'json' => [
288 3
                'username' => $login,
289 3
                'password' => $password,
290 3
                'clientToken' => $clientToken,
291
                'requestUser' => true,
292
                'agent' => [
293
                    'name' => 'Minecraft',
294
                    'version' => 1,
295
                ],
296
            ],
297
        ]);
298 2
        $body = $this->decode($response->getBody()->getContents());
299
300 2
        return new Response\AuthenticateResponse(
301 2
            $body['accessToken'],
302 2
            $body['clientToken'],
303 2
            $body['availableProfiles'],
304 2
            $body['selectedProfile'],
305 2
            $body['user']
306
        );
307
    }
308
309
    /**
310
     * @param string $accessToken
311
     * @param string $clientToken
312
     *
313
     * @return \Ely\Mojang\Response\AuthenticateResponse
314
     *
315
     * @throws \GuzzleHttp\Exception\GuzzleException
316
     *
317
     * @url https://wiki.vg/Authentication#Refresh
318
     */
319 2
    public function refresh(string $accessToken, string $clientToken): Response\AuthenticateResponse {
320 2
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/refresh', [
321 2
            'json' => [
322 2
                'accessToken' => $accessToken,
323 2
                'clientToken' => $clientToken,
324
                'requestUser' => true,
325
            ],
326
        ]);
327 1
        $body = $this->decode($response->getBody()->getContents());
328
329 1
        return new Response\AuthenticateResponse(
330 1
            $body['accessToken'],
331 1
            $body['clientToken'],
332 1
            [],
333 1
            $body['selectedProfile'],
334 1
            $body['user']
335
        );
336
    }
337
338
    /**
339
     * @param string $accessToken
340
     *
341
     * @return bool
342
     *
343
     * @throws \GuzzleHttp\Exception\GuzzleException
344
     *
345
     * @url https://wiki.vg/Authentication#Validate
346
     */
347 2
    public function validate(string $accessToken): bool {
348
        try {
349 2
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/validate', [
350
                'json' => [
351 2
                    'accessToken' => $accessToken,
352
                ],
353
            ]);
354 1
            if ($response->getStatusCode() === 204) {
355 1
                return true;
356
            }
357 1
        } catch (Exception\ForbiddenException $e) {
358
            // Suppress exception and let it just exit below
359
        }
360
361 1
        return false;
362
    }
363
364
    /**
365
     * @param string $accessToken
366
     * @param string $clientToken
367
     *
368
     * @throws GuzzleException
369
     *
370
     * @url https://wiki.vg/Authentication#Invalidate
371
     */
372 1
    public function invalidate(string $accessToken, string $clientToken): void {
373 1
        $this->getClient()->request('POST', 'https://authserver.mojang.com/invalidate', [
374
            'json' => [
375 1
                'accessToken' => $accessToken,
376 1
                'clientToken' => $clientToken,
377
            ],
378
        ]);
379 1
    }
380
381
    /**
382
     * @param string $login
383
     * @param string $password
384
     *
385
     * @return bool
386
     *
387
     * @throws GuzzleException
388
     *
389
     * @url https://wiki.vg/Authentication#Signout
390
     */
391 2
    public function signout(string $login, string $password): bool {
392
        try {
393 2
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/signout', [
394
                'json' => [
395 2
                    'username' => $login,
396 2
                    'password' => $password,
397
                ],
398
            ]);
399 1
            if ($response->getStatusCode() === 204) {
400 1
                return true;
401
            }
402 1
        } catch (Exception\ForbiddenException $e) {
403
            // Suppress exception and let it just exit below
404
        }
405
406 1
        return false;
407
    }
408
409
    /**
410
     * @param string $accessToken
411
     * @param string $accountUuid
412
     * @param string $serverId
413
     *
414
     * @throws GuzzleException
415
     *
416
     * @url https://wiki.vg/Protocol_Encryption#Client
417
     */
418 1
    public function joinServer(string $accessToken, string $accountUuid, string $serverId): void {
419 1
        $this->getClient()->request('POST', 'https://sessionserver.mojang.com/session/minecraft/join', [
420
            'json' => [
421 1
                'accessToken' => $accessToken,
422 1
                'selectedProfile' => $accountUuid,
423 1
                'serverId' => $serverId,
424
            ],
425
        ]);
426 1
    }
427
428
    /**
429
     * @param string $username
430
     * @param string $serverId
431
     *
432
     * @return \Ely\Mojang\Response\ProfileResponse
433
     *
434
     * @throws \Ely\Mojang\Exception\NoContentException
435
     * @throws GuzzleException
436
     *
437
     * @url https://wiki.vg/Protocol_Encryption#Server
438
     */
439 2
    public function hasJoinedServer(string $username, string $serverId): Response\ProfileResponse {
440 2
        $uri = (new Uri('https://sessionserver.mojang.com/session/minecraft/hasJoined'))
441 2
            ->withQuery(http_build_query([
442 2
                'username' => $username,
443 2
                'serverId' => $serverId,
444 2
            ], '', '&', PHP_QUERY_RFC3986));
445 2
        $request = new Request('GET', $uri);
446 2
        $response = $this->getClient()->send($request);
447 2
        $rawBody = $response->getBody()->getContents();
448 2
        if (empty($rawBody)) {
449 1
            throw new Exception\NoContentException($request, $response);
450
        }
451
452 1
        $body = $this->decode($rawBody);
453
454 1
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
455
    }
456
457
    /**
458
     * @param string $accessToken
459
     * @throws GuzzleException
460
     *
461
     * @url https://wiki.vg/Mojang_API#Check_if_security_questions_are_needed
462
     */
463 3
    public function isSecurityQuestionsNeeded(string $accessToken): void {
464 3
        $uri = new Uri('https://api.mojang.com/user/security/location');
465 3
        $request = new Request('GET', $uri, ['Authorization' => 'Bearer ' . $accessToken]);
466 3
        $response = $this->getClient()->send($request);
467 1
        $rawBody = $response->getBody()->getContents();
468 1
        if (!empty($rawBody)) {
469 1
            $body = $this->decode($rawBody);
470 1
            throw new Exception\OperationException($body['errorMessage'], $request, $response);
471
        }
472
    }
473
474
    /**
475
     * @param string $accessToken
476
     * @return array
477
     * @throws GuzzleException
478
     *
479
     * @url https://wiki.vg/Mojang_API#Get_list_of_questions
480
     */
481 1
    public function questions(string $accessToken): array {
482 1
        $uri = new Uri('https://api.mojang.com/user/security/challenges');
483 1
        $request = new Request('GET', $uri, ['Authorization' => 'Bearer ' . $accessToken]);
484 1
        $response = $this->getClient()->send($request);
485 1
        $rawBody = $response->getBody()->getContents();
486 1
        if (empty($rawBody)) {
487
            throw new Exception\NoContentException($request, $response);
488
        }
489
490 1
        $result = [];
491 1
        $body = $this->decode($rawBody);
492 1
        foreach ($body as $question) {
493 1
            $result[] = [
494 1
                'answer' => new AnswerResponse($question['answer']['id']),
495 1
                'question' => new QuestionResponse($question['question']['id'], $question['question']['question']),
496
            ];
497
        }
498
499 1
        return $result;
500
    }
501
502
    /**
503
     * @param string $accessToken
504
     * @param array $answers
505
     * @throws GuzzleException
506
     *
507
     * @url https://wiki.vg/Mojang_API#Send_back_the_answers
508
     */
509 1
    public function answer(string $accessToken, array $answers): void {
510 1
        $uri = new Uri('https://api.mojang.com/user/security/location');
511 1
        $request = new Request('POST', $uri, ['Authorization' => 'Bearer ' . $accessToken], json_encode($answers));
512 1
        $response = $this->getClient()->send($request);
513 1
        $rawBody = $response->getBody()->getContents();
514 1
        if (!empty($rawBody)) {
515 1
            $body = $this->decode($rawBody);
516 1
            throw new Exception\OperationException($body['errorMessage'], $request, $response);
517
        }
518
    }
519
520
    /**
521
     * @param array $metricKeys
522
     * @return Response\StatisticsResponse
523
     * @throws GuzzleException
524
     *
525
     * @url https://wiki.vg/Mojang_API#Statistics
526
     */
527 1
    public function statistics(array $metricKeys) {
528 1
        $response = $this->getClient()->request('POST', 'https://api.mojang.com/orders/statistics', [
529 1
            'json' => $metricKeys,
530
        ]);
531 1
        $body = $this->decode($response->getBody()->getContents());
532
533 1
        return new Response\StatisticsResponse($body['total'], $body['last24h'], $body['saleVelocityPerSeconds']);
534
    }
535
536
    /**
537
     * @return ClientInterface
538
     */
539 33
    protected function getClient(): ClientInterface {
540 33
        if ($this->client === null) {
541 1
            $this->client = $this->createDefaultClient();
542
        }
543
544 33
        return $this->client;
545
    }
546
547 1
    private function createDefaultClient(): ClientInterface {
548 1
        $stack = HandlerStack::create();
549
        // use after method because middleware executes in reverse order
550 1
        $stack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_response_converter');
551 1
        $stack->push(RetryMiddleware::create(), 'retry');
552
553 1
        return new GuzzleClient([
554 1
            'handler' => $stack,
555 1
            'timeout' => 10,
556
        ]);
557
    }
558
559 15
    private function decode(string $response): array {
560 15
        return json_decode($response, true);
561
    }
562
563
}
564