Passed
Push — master ( 8a9e6c...ec351d )
by
03:16
created

Api::blockedServers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
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 22
    public function setClient(ClientInterface $client): void {
26 22
        $this->client = $client;
27 22
    }
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://authserver.mojang.com/authenticate', [
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 2
    public function authenticate(
275
        string $login,
276
        string $password,
277
        string $clientToken = null
278
    ): Response\AuthenticateResponse {
279 2
        if ($clientToken === null) {
280
            /** @noinspection PhpUnhandledExceptionInspection */
281 1
            $clientToken = Uuid::uuid4()->toString();
282
        }
283
284 2
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
285 2
            'json' => [
286 2
                'username' => $login,
287 2
                'password' => $password,
288 2
                '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
     *
310
     * @return bool
311
     *
312
     * @throws \GuzzleHttp\Exception\GuzzleException
313
     *
314
     * @url https://wiki.vg/Authentication#Validate
315
     */
316 2
    public function validate(string $accessToken): bool {
317
        try {
318 2
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
319
                'json' => [
320 2
                    'accessToken' => $accessToken,
321
                ],
322
            ]);
323 1
            if ($response->getStatusCode() === 204) {
324 1
                return true;
325
            }
326 1
        } catch (Exception\ForbiddenException $e) {
327
            // Suppress exception and let it just exit below
328
        }
329
330 1
        return false;
331
    }
332
333
    /**
334
     * @param string $accessToken
335
     * @param string $accountUuid
336
     * @param string $serverId
337
     *
338
     * @throws GuzzleException
339
     *
340
     * @url https://wiki.vg/Protocol_Encryption#Client
341
     */
342 1
    public function joinServer(string $accessToken, string $accountUuid, string $serverId): void {
343 1
        $this->getClient()->request('POST', 'https://sessionserver.mojang.com/session/minecraft/join', [
344
            'json' => [
345 1
                'accessToken' => $accessToken,
346 1
                'selectedProfile' => $accountUuid,
347 1
                'serverId' => $serverId,
348
            ],
349
        ]);
350 1
    }
351
352
    /**
353
     * @param string $username
354
     * @param string $serverId
355
     *
356
     * @return \Ely\Mojang\Response\ProfileResponse
357
     *
358
     * @throws \Ely\Mojang\Exception\NoContentException
359
     * @throws GuzzleException
360
     *
361
     * @url https://wiki.vg/Protocol_Encryption#Server
362
     */
363 2
    public function hasJoinedServer(string $username, string $serverId): Response\ProfileResponse {
364 2
        $uri = (new Uri('https://sessionserver.mojang.com/session/minecraft/hasJoined'))
365 2
            ->withQuery(http_build_query([
366 2
                'username' => $username,
367 2
                'serverId' => $serverId,
368 2
            ], '', '&', PHP_QUERY_RFC3986));
369 2
        $request = new Request('GET', $uri);
370 2
        $response = $this->getClient()->send($request);
371 2
        $rawBody = $response->getBody()->getContents();
372 2
        if (empty($rawBody)) {
373 1
            throw new Exception\NoContentException($request, $response);
374
        }
375
376 1
        $body = $this->decode($rawBody);
377
378 1
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
379
    }
380
381
    /**
382
     * @return ClientInterface
383
     */
384 21
    protected function getClient(): ClientInterface {
385 21
        if ($this->client === null) {
386 1
            $this->client = $this->createDefaultClient();
387
        }
388
389 21
        return $this->client;
390
    }
391
392 1
    private function createDefaultClient(): ClientInterface {
393 1
        $stack = HandlerStack::create();
394
        // use after method because middleware executes in reverse order
395 1
        $stack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_response_converter');
396 1
        $stack->push(RetryMiddleware::create(), 'retry');
397
398 1
        return new GuzzleClient([
399 1
            'handler' => $stack,
400 1
            'timeout' => 10,
401
        ]);
402
    }
403
404 10
    private function decode(string $response): array {
405 10
        return json_decode($response, true);
406
    }
407
408
}
409