Passed
Push — master ( 1d57f8...c9eed8 )
by
02:02
created

Api::playernamesToUuids()   B

Complexity

Conditions 7
Paths 15

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 15
nop 1
dl 0
loc 32
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
    public function setClient(ClientInterface $client): void {
26
        $this->client = $client;
27
    }
28
29
    /**
30
     * @return \Ely\Mojang\Response\ApiStatus[]
31
     *
32
     * @throws GuzzleException
33
     *
34
     * @url https://wiki.vg/Mojang_API#API_Status
35
     */
36
    public function apiStatus(): array {
37
        $response = $this->getClient()->request('GET', 'https://status.mojang.com/check');
38
        $body = $this->decode($response->getBody()->getContents());
39
40
        $result = [];
41
        foreach ($body as $serviceDeclaration) {
42
            $serviceName = array_keys($serviceDeclaration)[0];
43
            $result[$serviceName] = new Response\ApiStatus($serviceName, $serviceDeclaration[$serviceName]);
44
        }
45
46
        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
    public function usernameToUUID(string $username, int $atTime = null): Response\ProfileInfo {
61
        $query = [];
62
        if ($atTime !== null) {
63
            $query['atTime'] = $atTime;
64
        }
65
66
        $response = $this->getClient()->request('GET', "https://api.mojang.com/users/profiles/minecraft/{$username}", [
67
            'query' => $query,
68
        ]);
69
70
        $data = $this->decode($response->getBody()->getContents());
71
72
        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
    public function uuidToNameHistory(string $uuid): array {
85
        $response = $this->getClient()->request('GET', "https://api.mojang.com/user/profiles/{$uuid}/names");
86
        $data = $this->decode($response->getBody()->getContents());
87
88
        $result = [];
89
        foreach ($data as $record) {
90
            $date = null;
91
            if (isset($record['changedToAt'])) {
92
                $date = new DateTime('@' . ($record['changedToAt'] / 1000));
93
            }
94
95
            $result[] = new Response\NameHistoryItem($record['name'], $date);
96
        }
97
98
        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
    public function uuidToTextures(string $uuid): Response\ProfileResponse {
112
        $response = $this->getClient()->request('GET', "https://sessionserver.mojang.com/session/minecraft/profile/{$uuid}", [
113
            'query' => [
114
                'unsigned' => false,
115
            ],
116
        ]);
117
        $body = $this->decode($response->getBody()->getContents());
118
119
        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
    public function usernameToTextures(string $username): Response\ProfileResponse {
133
        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
    public function playernamesToUuids(array $names): array {
147
        foreach ($names as $i => $name) {
148
            if (empty($name)) {
149
                unset($names[$i]);
150
            }
151
        }
152
153
        if (count($names) > 100) {
154
            throw new InvalidArgumentException('You cannot request more than 100 names per request');
155
        }
156
157
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
158
            'json' => array_values($names),
159
        ]);
160
        $body = $this->decode($response->getBody()->getContents());
161
162
        $result = [];
163
        foreach ($body as $record) {
164
            $object = Response\ProfileInfo::createFromResponse($record);
165
            $key = $object->getName();
166
            foreach ($names as $i => $name) {
167
                if (mb_strtolower($name) === mb_strtolower($object->getName())) {
168
                    unset($names[$i]);
169
                    $key = $name;
170
                    break;
171
                }
172
            }
173
174
            $result[$key] = $object;
175
        }
176
177
        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
    public function uploadSkin(string $accessToken, string $accountUuid, $skinContents, bool $isSlim): void {
191
        $this->getClient()->request('PUT', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
192
            'multipart' => [
193
                [
194
                    'name' => 'file',
195
                    'contents' => $skinContents,
196
                    'filename' => 'char.png',
197
                ],
198
                [
199
                    'name' => 'model',
200
                    'contents' => $isSlim ? 'slim' : '',
201
                ],
202
            ],
203
            'headers' => [
204
                'Authorization' => 'Bearer ' . $accessToken,
205
            ],
206
        ]);
207
    }
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
    public function authenticate(
221
        string $login,
222
        string $password,
223
        string $clientToken = null
224
    ): Response\AuthenticateResponse {
225
        if ($clientToken === null) {
226
            /** @noinspection PhpUnhandledExceptionInspection */
227
            $clientToken = Uuid::uuid4()->toString();
228
        }
229
230
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
231
            'json' => [
232
                'username' => $login,
233
                'password' => $password,
234
                'clientToken' => $clientToken,
235
                'requestUser' => true,
236
                'agent' => [
237
                    'name' => 'Minecraft',
238
                    'version' => 1,
239
                ],
240
            ],
241
        ]);
242
        $body = $this->decode($response->getBody()->getContents());
243
244
        return new Response\AuthenticateResponse(
245
            $body['accessToken'],
246
            $body['clientToken'],
247
            $body['availableProfiles'],
248
            $body['selectedProfile'],
249
            $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
    public function validate(string $accessToken): bool {
263
        try {
264
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
265
                'json' => [
266
                    'accessToken' => $accessToken,
267
                ],
268
            ]);
269
            if ($response->getStatusCode() === 204) {
270
                return true;
271
            }
272
        } catch (Exception\ForbiddenException $e) {
273
            // Suppress exception and let it just exit below
274
        }
275
276
        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
    public function joinServer(string $accessToken, string $accountUuid, string $serverId): void {
289
        $this->getClient()->request('POST', 'https://sessionserver.mojang.com/session/minecraft/join', [
290
            'json' => [
291
                'accessToken' => $accessToken,
292
                'selectedProfile' => $accountUuid,
293
                'serverId' => $serverId,
294
            ],
295
        ]);
296
    }
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
    public function hasJoinedServer(string $username, string $serverId): Response\ProfileResponse {
310
        $uri = (new Uri('https://sessionserver.mojang.com/session/minecraft/hasJoined'))
311
            ->withQuery(http_build_query([
312
                'username' => $username,
313
                'serverId' => $serverId,
314
            ], '', '&', PHP_QUERY_RFC3986));
315
        $request = new Request('GET', $uri);
316
        $response = $this->getClient()->send($request);
317
        $rawBody = $response->getBody()->getContents();
318
        if (empty($rawBody)) {
319
            throw new Exception\NoContentException($request, $response);
320
        }
321
322
        $body = $this->decode($rawBody);
323
324
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
325
    }
326
327
    /**
328
     * @return ClientInterface
329
     */
330
    protected function getClient(): ClientInterface {
331
        if ($this->client === null) {
332
            $this->client = $this->createDefaultClient();
333
        }
334
335
        return $this->client;
336
    }
337
338
    private function createDefaultClient(): ClientInterface {
339
        $stack = HandlerStack::create();
340
        // use after method because middleware executes in reverse order
341
        $stack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_response_converter');
342
        $stack->push(RetryMiddleware::create(), 'retry');
343
344
        return new GuzzleClient([
345
            'handler' => $stack,
346
            'timeout' => 10,
347
        ]);
348
    }
349
350
    private function decode(string $response): array {
351
        return json_decode($response, true);
352
    }
353
354
}
355