Passed
Push — master ( df9d50...7f420f )
by Mattia
03:11
created

UuidResolver::getAccount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Resolvers;
6
7
use App\Cache\UserNotFoundCache;
8
use App\Events\Account\UsernameChangeEvent;
9
use App\Helpers\Storage\Files\SkinsStorage;
10
use App\Minecraft\MinecraftDefaults;
11
use App\Minecraft\MojangAccount;
12
use App\Minecraft\MojangClient;
13
use App\Models\Account;
14
use App\Repositories\AccountRepository;
15
use App\Repositories\AccountStatsRepository;
16
use Illuminate\Support\Facades\Event;
17
use Illuminate\Support\Facades\Log;
18
19
class UuidResolver
20
{
21
    /**
22
     * Requested string.
23
     *
24
     * @var string
25
     */
26
    private string $request = '';
27
    /**
28
     * @var string
29
     */
30
    private string $uuid = MinecraftDefaults::UUID;
31
    /**
32
     * Userdata from/to DB.
33
     *
34
     * @var Account
35
     */
36
    private ?Account $account;
37
    /**
38
     * Full userdata.
39
     *
40
     * @var MojangAccount
41
     */
42
    private ?MojangAccount $mojangAccount;
43
    /**
44
     * User data has been updated?
45
     *
46
     * @var bool
47
     */
48
    private bool $dataUpdated = false;
49
    /**
50
     * Set force update.
51
     *
52
     * @var bool
53
     */
54
    private bool $forceUpdate = false;
55
    /**
56
     * @var AccountRepository
57
     */
58
    private AccountRepository $accountRepository;
59
    /**
60
     * @var AccountStatsRepository
61
     */
62
    private AccountStatsRepository $accountStatsRepository;
63
64
    /**
65
     * @var MojangClient
66
     */
67
    private MojangClient $mojangClient;
68
69
    /**
70
     * Core constructor.
71
     *
72
     * @param AccountRepository      $accountRepository      Where user data is stored
73
     * @param AccountStatsRepository $accountStatsRepository
74
     * @param MojangClient           $mojangClient           Client for Mojang API
75
     */
76
    public function __construct(
77
        AccountRepository $accountRepository,
78
        AccountStatsRepository $accountStatsRepository,
79
        MojangClient $mojangClient
80
    ) {
81
        $this->accountRepository = $accountRepository;
82
        $this->accountStatsRepository = $accountStatsRepository;
83
        $this->mojangClient = $mojangClient;
84
    }
85
86
    /**
87
     * @return string
88
     */
89
    public function getUuid(): string
90
    {
91
        return $this->uuid;
92
    }
93
94
    /**
95
     * Check if cache is still valid.
96
     *
97
     * @param int
98
     *
99
     * @return bool
100
     */
101
    private function checkDbCache(): bool
102
    {
103
        $accountUpdatedAtTimestamp = $this->account->updated_at->timestamp ?? 0;
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
104
105
        return (\time() - $accountUpdatedAtTimestamp) < env('USERDATA_CACHE_TIME');
106
    }
107
108
    /**
109
     * Return loaded user data.
110
     *
111
     * @return Account
112
     */
113
    public function getAccount(): Account
114
    {
115
        return $this->account ?? new Account();
116
    }
117
118
    /**
119
     * Check if an UUID is in the database.
120
     *
121
     * @return bool Returns true/false
122
     */
123
    private function requestedUuidInDb(): bool
124
    {
125
        $account = $this->accountRepository->findByUuid($this->request);
126
127
        if ($account === null) {
128
            return false;
129
        }
130
131
        $this->account = $account;
132
        $this->uuid = $account->uuid;
133
134
        return true;
135
    }
136
137
    /**
138
     * Insert user data in database.
139
     *
140
     * @param void
141
     *
142
     * @return bool
143
     */
144
    public function insertNewUuid(): bool
145
    {
146
        if (UserNotFoundCache::has($this->request)) {
147
            Log::debug('Cache Hit Not Found', ['request' => $this->request]);
148
149
            return false;
150
        }
151
152
        if ($this->getFullUserdataApi()) {
153
            $this->account = $this->accountRepository->create([
154
                'username' => $this->mojangAccount->getUsername(),
155
                'uuid' => $this->mojangAccount->getUuid(),
156
                'skin' => $this->mojangAccount->getSkin(),
157
                'cape' => $this->mojangAccount->getCape(),
158
            ]);
159
160
            $this->saveRemoteSkin();
161
162
            $this->accountStatsRepository->create([
163
                'uuid' => $this->account->uuid,
164
                'count_search' => 0,
165
                'count_request' => 0,
166
                'time_search' => 0,
167
                'time_request' => 0,
168
            ]);
169
170
            return true;
171
        }
172
173
        UserNotFoundCache::add($this->request);
174
175
        return false;
176
    }
177
178
    /**
179
     * Check requested string and initialize objects.
180
     *
181
     * @param string
182
     *
183
     * @throws \Exception
184
     *
185
     * @return bool
186
     */
187
    public function resolve(string $string): bool
188
    {
189
        $this->dataUpdated = false;
190
        $this->request = $string;
191
192
        if ($this->initializeUuidRequest()) {
193
            return true;
194
        }
195
196
        $this->setFailedRequest('Account not found');
197
198
        return false;
199
    }
200
201
    /**
202
     * Update current user fail count.
203
     */
204
    private function updateUserFailUpdate(): bool
205
    {
206
        if (isset($this->account->uuid)) {
207
            ++$this->account->fail_count;
208
209
            return $this->account->save();
0 ignored issues
show
Bug introduced by
The method save() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

209
            return $this->account->/** @scrutinizer ignore-call */ save();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
210
        }
211
212
        return false;
213
    }
214
215
    /**
216
     * Update db user data.
217
     */
218
    private function updateDbUser(): bool
219
    {
220
        if (isset($this->account->username) && $this->account->uuid !== '') {
221
            // Get data from API
222
            if ($this->getFullUserdataApi()) {
223
                $originalUsername = $this->account->username;
224
                // Update database
225
                $this->accountRepository->update([
226
                    'username' => $this->mojangAccount->getUsername(),
227
                    'skin' => $this->mojangAccount->getSkin(),
228
                    'cape' => $this->mojangAccount->getCape(),
229
                    'fail_count' => 0,
230
                ], $this->account->id);
231
232
                $this->account->touch();
233
                $this->account->refresh();
234
235
                // Update skin
236
                $this->saveRemoteSkin();
237
238
                // Log username change
239
                if ($this->account->username !== $originalUsername && $originalUsername !== '') {
240
                    $this->logUsernameChange($this->account->uuid, $originalUsername, $this->account->username);
241
                }
242
                $this->dataUpdated = true;
243
244
                return true;
245
            }
246
247
            $this->updateUserFailUpdate();
248
249
            if (!SkinsStorage::exists($this->account->uuid)) {
250
                SkinsStorage::copyAsSteve($this->account->uuid);
251
            }
252
        }
253
        $this->dataUpdated = false;
254
255
        return false;
256
    }
257
258
    /**
259
     * Return if data has been updated.
260
     */
261
    public function userDataUpdated(): bool
262
    {
263
        return $this->dataUpdated;
264
    }
265
266
    /**
267
     * Log the username change.
268
     *
269
     * @param $uuid string User UUID
270
     * @param $prev string Previous username
271
     * @param $new string New username
272
     */
273
    private function logUsernameChange(string $uuid, string $prev, string $new): void
274
    {
275
        Event::dispatch(new UsernameChangeEvent($uuid, $prev, $new));
276
    }
277
278
    /**
279
     * Get userdata from Mojang API.
280
     *
281
     * @param mixed
282
     *
283
     * @throws \Throwable
284
     *
285
     * @return bool
286
     */
287
    private function getFullUserdataApi(): bool
288
    {
289
        try {
290
            $this->mojangAccount = $this->mojangClient->getUuidInfo($this->request);
291
292
            return true;
293
        } catch (\Exception $e) {
294
            Log::error($e->getTraceAsString(), ['request' => $this->request]);
295
            $this->mojangAccount = null;
296
297
            return false;
298
        }
299
    }
300
301
    /**
302
     * Save skin image.
303
     *
304
     * @param mixed
305
     *
306
     * @return bool
307
     */
308
    public function saveRemoteSkin(): bool
309
    {
310
        if (!empty($this->account->skin) && $this->account->skin !== '') {
311
            try {
312
                $skinData = $this->mojangClient->getSkin($this->account->skin);
313
314
                return SkinsStorage::save($this->account->uuid, $skinData);
315
            } catch (\Exception $e) {
316
                Log::error($e->getTraceAsString());
317
            }
318
        }
319
320
        return SkinsStorage::copyAsSteve($this->account->uuid);
321
    }
322
323
    /**
324
     * Set force update.
325
     *
326
     * @param bool $forceUpdate
327
     */
328
    public function setForceUpdate(bool $forceUpdate): void
329
    {
330
        $this->forceUpdate = $forceUpdate;
331
    }
332
333
    /**
334
     * Can I exec force update?
335
     */
336
    private function forceUpdatePossible(): bool
337
    {
338
        return ($this->forceUpdate) &&
339
            ((\time() - $this->account->updated_at->timestamp) > env('MIN_USERDATA_UPDATE_INTERVAL'));
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
340
    }
341
342
    /**
343
     * Use steve skin for given username.
344
     *
345
     * @param string
346
     */
347
    public function updateStats(): void
348
    {
349
        if (!empty($this->account->uuid) && $this->account->uuid !== MinecraftDefaults::UUID && env('STATS_ENABLED')) {
350
            $this->accountStatsRepository->incrementRequestCounter($this->account->uuid);
351
        }
352
    }
353
354
    /**
355
     * @return bool
356
     */
357
    private function initializeUuidRequest(): bool
358
    {
359
        if ($this->requestedUuidInDb()) {
360
            // Check if UUID is in my database
361
            // Data cache still valid?
362
            if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
363
                Log::debug('Refreshing User DB Data');
364
                // Nope, updating data
365
                $this->updateDbUser();
366
            }
367
368
            if (!SkinsStorage::exists($this->request)) {
369
                $this->saveRemoteSkin();
370
            }
371
372
            return true;
373
        }
374
375
        if ($this->insertNewUuid()) {
376
            return true;
377
        }
378
379
        return false;
380
    }
381
382
    /**
383
     * Set failed request.
384
     *
385
     * @param string $errorMessage
386
     */
387
    private function setFailedRequest(string $errorMessage = ''): void
388
    {
389
        Log::notice($errorMessage, ['request' => $this->request]);
390
        $this->account = null;
391
        $this->request = '';
392
    }
393
}
394