Passed
Push — master ( cf4a17...0b0ca2 )
by
01:59
created

Api::setClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
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 Ramsey\Uuid\Uuid;
16
17
class Api {
18
19
    /**
20
     * @var ClientInterface
21
     */
22
    private $client;
23
24
    public function setClient(ClientInterface $client): void {
25
        $this->client = $client;
26
    }
27
28
    /**
29
     * @return \Ely\Mojang\Response\ApiStatus[]
30
     *
31
     * @throws GuzzleException
32
     *
33
     * @url https://wiki.vg/Mojang_API#API_Status
34
     */
35
    public function apiStatus(): array {
36
        $response = $this->getClient()->request('GET', 'https://status.mojang.com/check');
37
        $body = $this->decode($response->getBody()->getContents());
38
39
        $result = [];
40
        foreach ($body as $serviceDeclaration) {
41
            $serviceName = array_keys($serviceDeclaration)[0];
42
            $result[$serviceName] = new Response\ApiStatus($serviceName, $serviceDeclaration[$serviceName]);
43
        }
44
45
        return $result;
46
    }
47
48
    /**
49
     * @param string $username
50
     * @param int    $atTime
51
     *
52
     * @return \Ely\Mojang\Response\ProfileInfo
53
     *
54
     * @throws \Ely\Mojang\Exception\MojangApiException
55
     * @throws \GuzzleHttp\Exception\GuzzleException
56
     *
57
     * @url http://wiki.vg/Mojang_API#Username_-.3E_UUID_at_time
58
     */
59
    public function usernameToUUID(string $username, int $atTime = null): Response\ProfileInfo {
60
        $query = [];
61
        if ($atTime !== null) {
62
            $query['atTime'] = $atTime;
63
        }
64
65
        $response = $this->getClient()->request('GET', "https://api.mojang.com/users/profiles/minecraft/{$username}", [
66
            'query' => $query,
67
        ]);
68
69
        $data = $this->decode($response->getBody()->getContents());
70
71
        return Response\ProfileInfo::createFromResponse($data);
72
    }
73
74
    /**
75
     * @param string $uuid
76
     *
77
     * @return \Ely\Mojang\Response\NameHistoryItem[]
78
     *
79
     * @throws GuzzleException
80
     *
81
     * @url https://wiki.vg/Mojang_API#UUID_-.3E_Name_history
82
     */
83
    public function uuidToNameHistory(string $uuid): array {
84
        $response = $this->getClient()->request('GET', "https://api.mojang.com/user/profiles/{$uuid}/names");
85
        $data = $this->decode($response->getBody()->getContents());
86
87
        $result = [];
88
        foreach ($data as $record) {
89
            $date = null;
90
            if (isset($record['changedToAt'])) {
91
                $date = new DateTime('@' . ($record['changedToAt'] / 1000));
92
            }
93
94
            $result[] = new Response\NameHistoryItem($record['name'], $date);
95
        }
96
97
        return $result;
98
    }
99
100
    /**
101
     * @param string $uuid
102
     *
103
     * @return \Ely\Mojang\Response\ProfileResponse
104
     *
105
     * @throws \Ely\Mojang\Exception\MojangApiException
106
     * @throws \GuzzleHttp\Exception\GuzzleException
107
     *
108
     * @url http://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
109
     */
110
    public function uuidToTextures(string $uuid): Response\ProfileResponse {
111
        $response = $this->getClient()->request('GET', "https://sessionserver.mojang.com/session/minecraft/profile/{$uuid}", [
112
            'query' => [
113
                'unsigned' => false,
114
            ],
115
        ]);
116
        $body = $this->decode($response->getBody()->getContents());
117
118
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
119
    }
120
121
    /**
122
     * Helper method to exchange username to the corresponding textures.
123
     *
124
     * @param string $username
125
     *
126
     * @return \Ely\Mojang\Response\ProfileResponse
127
     *
128
     * @throws GuzzleException
129
     * @throws \Ely\Mojang\Exception\MojangApiException
130
     */
131
    public function usernameToTextures(string $username): Response\ProfileResponse {
132
        return $this->uuidToTextures($this->usernameToUUID($username)->getId());
133
    }
134
135
    /**
136
     * @param string $login
137
     * @param string $password
138
     * @param string $clientToken
139
     *
140
     * @return \Ely\Mojang\Response\AuthenticateResponse
141
     *
142
     * @throws \GuzzleHttp\Exception\GuzzleException
143
     *
144
     * @url https://wiki.vg/Authentication#Authenticate
145
     */
146
    public function authenticate(
147
        string $login,
148
        string $password,
149
        string $clientToken = null
150
    ): Response\AuthenticateResponse {
151
        if ($clientToken === null) {
152
            /** @noinspection PhpUnhandledExceptionInspection */
153
            $clientToken = Uuid::uuid4()->toString();
154
        }
155
156
        $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
157
            'json' => [
158
                'username' => $login,
159
                'password' => $password,
160
                'clientToken' => $clientToken,
161
                'requestUser' => true,
162
                'agent' => [
163
                    'name' => 'Minecraft',
164
                    'version' => 1,
165
                ],
166
            ],
167
        ]);
168
        $body = $this->decode($response->getBody()->getContents());
169
170
        return new Response\AuthenticateResponse(
171
            $body['accessToken'],
172
            $body['clientToken'],
173
            $body['availableProfiles'],
174
            $body['selectedProfile'],
175
            $body['user']
176
        );
177
    }
178
179
    /**
180
     * @param string $accessToken
181
     *
182
     * @return bool
183
     *
184
     * @throws \GuzzleHttp\Exception\GuzzleException
185
     *
186
     * @url https://wiki.vg/Authentication#Validate
187
     */
188
    public function validate(string $accessToken): bool {
189
        try {
190
            $response = $this->getClient()->request('POST', 'https://authserver.mojang.com/authenticate', [
191
                'json' => [
192
                    'accessToken' => $accessToken,
193
                ],
194
            ]);
195
            if ($response->getStatusCode() === 204) {
196
                return true;
197
            }
198
        } catch (Exception\ForbiddenException $e) {
199
            // Suppress exception and let it just exit below
200
        }
201
202
        return false;
203
    }
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
    public function uploadSkin(string $accessToken, string $accountUuid, $skinContents, bool $isSlim): void {
216
        $this->getClient()->request('PUT', "https://api.mojang.com/user/profile/{$accountUuid}/skin", [
217
            'multipart' => [
218
                [
219
                    'name' => 'file',
220
                    'contents' => $skinContents,
221
                    'filename' => 'char.png',
222
                ],
223
                [
224
                    'name' => 'model',
225
                    'contents' => $isSlim ? 'slim' : '',
226
                ],
227
            ],
228
            'headers' => [
229
                'Authorization' => 'Bearer ' . $accessToken,
230
            ],
231
        ]);
232
    }
233
234
    /**
235
     * @param string $accessToken
236
     * @param string $accountUuid
237
     * @param string $serverId
238
     *
239
     * @throws GuzzleException
240
     *
241
     * @url https://wiki.vg/Protocol_Encryption#Client
242
     */
243
    public function joinServer(string $accessToken, string $accountUuid, string $serverId): void {
244
        $this->getClient()->request('POST', 'https://sessionserver.mojang.com/session/minecraft/join', [
245
            'json' => [
246
                'accessToken' => $accessToken,
247
                'selectedProfile' => $accountUuid,
248
                'serverId' => $serverId,
249
            ],
250
        ]);
251
    }
252
253
    /**
254
     * @param string $username
255
     * @param string $serverId
256
     *
257
     * @return \Ely\Mojang\Response\ProfileResponse
258
     *
259
     * @throws \Ely\Mojang\Exception\NoContentException
260
     * @throws GuzzleException
261
     *
262
     * @url https://wiki.vg/Protocol_Encryption#Server
263
     */
264
    public function hasJoinedServer(string $username, string $serverId): Response\ProfileResponse {
265
        $uri = (new Uri('https://sessionserver.mojang.com/session/minecraft/hasJoined'))
266
            ->withQuery(http_build_query([
267
                'username' => $username,
268
                'serverId' => $serverId,
269
            ], '', '&', PHP_QUERY_RFC3986));
270
        $request = new Request('GET', $uri);
271
        $response = $this->getClient()->send($request);
272
        $rawBody = $response->getBody()->getContents();
273
        if (empty($rawBody)) {
274
            throw new Exception\NoContentException($request, $response);
275
        }
276
277
        $body = $this->decode($rawBody);
278
279
        return new Response\ProfileResponse($body['id'], $body['name'], $body['properties']);
280
    }
281
282
    /**
283
     * @return ClientInterface
284
     */
285
    protected function getClient(): ClientInterface {
286
        if ($this->client === null) {
287
            $this->client = $this->createDefaultClient();
288
        }
289
290
        return $this->client;
291
    }
292
293
    private function createDefaultClient(): ClientInterface {
294
        $stack = HandlerStack::create();
295
        // use after method because middleware executes in reverse order
296
        $stack->after('http_errors', ResponseConverterMiddleware::create(), 'mojang_response_converter');
297
        $stack->push(RetryMiddleware::create(), 'retry');
298
299
        return new GuzzleClient([
300
            'handler' => $stack,
301
            'timeout' => 10,
302
        ]);
303
    }
304
305
    private function decode(string $response): array {
306
        return json_decode($response, true);
307
    }
308
309
}
310