Passed
Push — master ( 6f7c46...5194c3 )
by Mattia
03:37
created

MojangClient::getCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Minepic\Minecraft;
6
7
use GuzzleHttp\Client as HttpClient;
8
use GuzzleHttp\Exception\BadResponseException;
9
use Illuminate\Support\Facades\Log;
10
use Minepic\Minecraft\Exceptions\UserNotFoundException;
11
use Psr\Http\Message\ResponseInterface;
12
13
class MojangClient
14
{
15
    /**
16
     * API response TTL.
17
     */
18
    private const CACHE_TTL = 120;
19
    /**
20
     * User Agent used for requests.
21
     */
22
    private const USER_AGENT = 'Minepic/2.0 (minepic.org)';
23
24
    /**
25
     * HTTP Client for requests.
26
     *
27
     * @var \GuzzleHttp\Client
28
     */
29
    private \GuzzleHttp\Client $httpClient;
30
31
    /**
32
     * MojangClient constructor.
33
     */
34
    public function __construct()
35
    {
36
        $this->httpClient = new HttpClient(
37
            [
38
                'headers' => [
39
                    'User-Agent' => self::USER_AGENT,
40
                ],
41
            ]
42
        );
43
    }
44
45
    private function handleGuzzleBadResponseException(
46
        BadResponseException $badResponseException
47
    ): void {
48
        Log::error(
49
            'Error from Minecraft API',
50
            [
51
                'status_code' => $badResponseException->getResponse()->getStatusCode(),
52
                'content_type' => $badResponseException->getResponse()->getHeader('content-type')[0] ?? '',
53
                'content' => $badResponseException->getResponse()->getBody()->getContents(),
54
            ]
55
        );
56
    }
57
58
    /**
59
     * @param \Throwable $exception
60
     */
61
    private function handleThrowable(\Throwable $exception): void
62
    {
63
        Log::error($exception->getFile().':'.$exception->getLine().' - '.$exception->getMessage());
64
        Log::error($exception->getTraceAsString());
65
    }
66
67
    /**
68
     * @param string $method
69
     * @param string $url
70
     *
71
     * @return string
72
     */
73
    private function getCacheKey(string $method, string $url): string
74
    {
75
        return 'minecraft_api_response_'.\md5($method.'_'.$url);
76
    }
77
78
    /**
79
     * Send new request.
80
     *
81
     * @param string $method HTTP Verb
82
     * @param string $url    API Endpoint
83
     *
84
     * @throws \Throwable
85
     *
86
     * @return array
87
     */
88
    private function sendApiRequest(string $method, string $url): ?array
89
    {
90
        try {
91
            return \Cache::remember($this->getCacheKey($method, $url), self::CACHE_TTL, function () use ($method, $url) {
92
                $response = $this->httpClient->request($method, $url);
93
                // No Content
94
                if ($response->getStatusCode() === 204) {
95
                    return null;
96
                }
97
98
                $responseContents = $response->getBody()->getContents();
99
                Log::debug('Minecraft API Response: '.$responseContents, ['method' => $method, 'url' => $url]);
100
101
                return \json_decode($responseContents, true, 512, JSON_THROW_ON_ERROR);
102
            });
103
        } catch (BadResponseException $exception) {
104
            $this->handleGuzzleBadResponseException($exception);
105
106
            throw $exception;
107
        } catch (\Throwable $exception) {
108
            $this->handleThrowable($exception);
109
110
            throw $exception;
111
        }
112
    }
113
114
    /**
115
     * Generic request.
116
     *
117
     * @param string $method
118
     * @param string $url
119
     *
120
     * @throws \Throwable
121
     *
122
     * @return \Psr\Http\Message\ResponseInterface
123
     */
124
    private function sendRequest(string $method, string $url): ResponseInterface
125
    {
126
        try {
127
            return $this->httpClient->request($method, $url);
128
        } catch (BadResponseException $exception) {
129
            $this->handleGuzzleBadResponseException($exception);
130
131
            throw $exception;
132
        } catch (\Throwable $exception) {
133
            $this->handleThrowable($exception);
134
135
            throw $exception;
136
        }
137
    }
138
139
    /**
140
     * Account info from username.
141
     *
142
     * @param string $username
143
     *
144
     * @throws \Throwable
145
     *
146
     * @return MojangAccount
147
     */
148
    public function sendUsernameInfoRequest(string $username): MojangAccount
149
    {
150
        $response = $this->sendApiRequest('GET', env('MINECRAFT_PROFILE_URL').$username);
151
152
        if ($response !== null) {
153
            return new MojangAccount($response['id'], $response['name']);
154
        }
155
156
        throw new UserNotFoundException("Unknown user {$username}");
157
    }
158
159
    /**
160
     * Account info from UUID.
161
     *
162
     * @param string $uuid User UUID
163
     *
164
     * @throws \Throwable
165
     *
166
     * @return MojangAccount
167
     */
168
    public function getUuidInfo(string $uuid): MojangAccount
169
    {
170
        $response = $this->sendApiRequest('GET', env('MINECRAFT_SESSION_URL').$uuid);
171
172
        if ($response === null) {
173
            throw new \Exception('Cannot create data account');
174
        }
175
176
        return MojangAccountFactory::makeFromApiResponse($response);
177
    }
178
179
    /**
180
     * Get Skin.
181
     *
182
     * @param string $skin Skin uuid
183
     *
184
     * @throws \Exception|\Throwable
185
     *
186
     * @return string
187
     */
188
    public function getSkin(string $skin): string
189
    {
190
        $response = $this->sendRequest('GET', env('MINECRAFT_TEXTURE_URL').$skin);
191
192
        if ($response->getHeader('content-type')[0] === 'image/png') {
193
            return $response->getBody()->getContents();
194
        }
195
196
        throw new \Exception('Invalid Response content type: '.$response->getHeader('content-type')[0]);
197
    }
198
}
199