Passed
Branch master (c9eed8)
by
03:15
created

Api::playernamesToUuids()   B

Complexity

Conditions 7
Paths 15

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 15
nop 1
dl 0
loc 32
ccs 20
cts 20
cp 1
crap 7
rs 8.8333
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 18
    public function setClient(ClientInterface $client): void {
26 18
        $this->client = $client;
27 18
    }
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 \Psr\Http\Message\StreamInterface|resource|string $skinContents
184
     * @param bool $isSlim
185
     *
186
     * @throws GuzzleException
187
     *
188
     * @url https://wiki.vg/Mojang_API#Upload_Skin
189
     */
190 2
    public function uploadSkin(string $accessToken, string $accountUuid, $skinContents, bool $isSlim): void {
191 2
        $this->getClient()->request('PUT', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
192
            'multipart' => [
193
                [
194 2
                    'name' => 'file',
195 2
                    'contents' => $skinContents,
196 2
                    'filename' => 'char.png',
197
                ],
198
                [
199 2
                    'name' => 'model',
200 2
                    'contents' => $isSlim ? 'slim' : '',
201
                ],
202
            ],
203
            'headers' => [
204 2
                'Authorization' => 'Bearer ' . $accessToken,
205
            ],
206
        ]);
207 2
    }
208
209
    /**
210
     * @param string $login
211
     * @param string $password
212
     * @param string $clientToken
213
     *
214
     * @return \Ely\Mojang\Response\AuthenticateResponse
215
     *
216
     * @throws \GuzzleHttp\Exception\GuzzleException
217
     *
218
     * @url https://wiki.vg/Authentication#Authenticate
219
     */
220 2
    public function authenticate(
221
        string $login,
222
        string $password,
223
        string $clientToken = null
224
    ): Response\AuthenticateResponse {
225 2
        if ($clientToken === null) {
226
            /** @noinspection PhpUnhandledExceptionInspection */
227 1
            $clientToken = Uuid::uuid4()->toString();
228
        }
229
230 2
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
231 2
            'json' => [
232 2
                'username' => $login,
233 2
                'password' => $password,
234 2
                'clientToken' => $clientToken,
235
                'requestUser' => true,
236
                'agent' => [
237
                    'name' => 'Minecraft',
238
                    'version' => 1,
239
                ],
240
            ],
241
        ]);
242 2
        $body = $this->decode($response->getBody()->getContents());
243
244 2
        return new Response\AuthenticateResponse(
245 2
            $body['accessToken'],
246 2
            $body['clientToken'],
247 2
            $body['availableProfiles'],
248 2
            $body['selectedProfile'],
249 2
            $body['user']
250
        );
251
    }
252
253
    /**
254
     * @param string $accessToken
255
     *
256
     * @return bool
257
     *
258
     * @throws \GuzzleHttp\Exception\GuzzleException
259
     *
260
     * @url https://wiki.vg/Authentication#Validate
261
     */
262 2
    public function validate(string $accessToken): bool {
263
        try {
264 2
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
265
                'json' => [
266 2
                    'accessToken' => $accessToken,
267
                ],
268
            ]);
269 1
            if ($response->getStatusCode() === 204) {
270 1
                return true;
271
            }
272 1
        } catch (Exception\ForbiddenException $e) {
273
            // Suppress exception and let it just exit below
274
        }
275
276 1
        return false;
277
    }
278
279
    /**
280
     * @param string $accessToken
281
     * @param string $accountUuid
282
     * @param string $serverId
283
     *
284
     * @throws GuzzleException
285
     *
286
     * @url https://wiki.vg/Protocol_Encryption#Client
287
     */
288 1
    public function joinServer(string $accessToken, string $accountUuid, string $serverId): void {
289 1
        $this->getClient()->request('POST', 'https://sessionserver.mojang.com/session/minecraft/join', [
290
            'json' => [
291 1
                'accessToken' => $accessToken,
292 1
                'selectedProfile' => $accountUuid,
293 1
                'serverId' => $serverId,
294
            ],
295
        ]);
296 1
    }
297
298
    /**
299
     * @param string $username
300
     * @param string $serverId
301
     *
302
     * @return \Ely\Mojang\Response\ProfileResponse
303
     *
304
     * @throws \Ely\Mojang\Exception\NoContentException
305
     * @throws GuzzleException
306
     *
307
     * @url https://wiki.vg/Protocol_Encryption#Server
308
     */
309 2
    public function hasJoinedServer(string $username, string $serverId): Response\ProfileResponse {
310 2
        $uri = (new Uri('https://sessionserver.mojang.com/session/minecraft/hasJoined'))
311 2
            ->withQuery(http_build_query([
312 2
                'username' => $username,
313 2
                'serverId' => $serverId,
314 2
            ], '', '&', PHP_QUERY_RFC3986));
315 2
        $request = new Request('GET', $uri);
316 2
        $response = $this->getClient()->send($request);
317 2
        $rawBody = $response->getBody()->getContents();
318 2
        if (empty($rawBody)) {
319 1
            throw new Exception\NoContentException($request, $response);
320
        }
321
322 1
        $body = $this->decode($rawBody);
323
324 1
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
325
    }
326
327
    /**
328
     * @return ClientInterface
329
     */
330 17
    protected function getClient(): ClientInterface {
331 17
        if ($this->client === null) {
332 1
            $this->client = $this->createDefaultClient();
333
        }
334
335 17
        return $this->client;
336
    }
337
338 1
    private function createDefaultClient(): ClientInterface {
339 1
        $stack = HandlerStack::create();
340
        // use after method because middleware executes in reverse order
341 1
        $stack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_response_converter');
342 1
        $stack->push(RetryMiddleware::create(), 'retry');
343
344 1
        return new GuzzleClient([
345 1
            'handler' => $stack,
346 1
            'timeout' => 10,
347
        ]);
348
    }
349
350 10
    private function decode(string $response): array {
351 10
        return json_decode($response, true);
352
    }
353
354
}
355