Passed
Push — master ( 1bb436...987afb )
by
05:45 queued 12s
created

Api::questions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

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