Passed
Push — master ( 6ffe87...662eee )
by Mattia
03:26
created

Core::skinCurrentUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

234
            return $this->userdata->/** @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...
235
        }
236
237
        return false;
238
    }
239
240
    /**
241
     * Update db user data.
242
     */
243
    private function updateDbUser(): bool
244
    {
245
        if (isset($this->userdata->username) && $this->userdata->uuid !== '') {
246
            // Get data from API
247
            if ($this->getFullUserdataApi()) {
248
                $originalUsername = $this->userdata->username;
249
                // Update database
250
                $this->accountRepository->update([
251
                    'username' => $this->apiUserdata->getUsername(),
252
                    'skin' => $this->apiUserdata->getSkin(),
253
                    'cape' => $this->apiUserdata->getCape(),
254
                    'fail_count' => 0,
255
                ], $this->userdata->id);
256
257
                $this->userdata->touch();
258
                $this->userdata->refresh();
259
260
                // Update skin
261
                $this->saveRemoteSkin();
262
263
                // Log username change
264
                if ($this->userdata->username !== $originalUsername && $originalUsername !== '') {
265
                    $this->logUsernameChange($this->userdata->uuid, $originalUsername, $this->userdata->username);
266
                }
267
                $this->dataUpdated = true;
268
269
                return true;
270
            }
271
272
            $this->updateUserFailUpdate();
273
274
            if (!SkinsStorage::exists($this->userdata->uuid)) {
275
                SkinsStorage::copyAsSteve($this->userdata->uuid);
276
            }
277
        }
278
        $this->dataUpdated = false;
279
280
        return false;
281
    }
282
283
    /**
284
     * Return if data has been updated.
285
     */
286
    public function userDataUpdated(): bool
287
    {
288
        return $this->dataUpdated;
289
    }
290
291
    /**
292
     * Log the username change.
293
     *
294
     * @param $uuid string User UUID
295
     * @param $prev string Previous username
296
     * @param $new string New username
297
     */
298
    private function logUsernameChange(string $uuid, string $prev, string $new): void
299
    {
300
        Event::dispatch(new UsernameChangeEvent($uuid, $prev, $new));
301
    }
302
303
    /**
304
     * Get userdata from Mojang API.
305
     *
306
     * @param mixed
307
     *
308
     * @throws \Throwable
309
     *
310
     * @return bool
311
     */
312
    private function getFullUserdataApi(): bool
313
    {
314
        try {
315
            $this->apiUserdata = $this->mojangClient->getUuidInfo($this->request);
316
317
            return true;
318
        } catch (\Exception $e) {
319
            Log::error($e->getTraceAsString(), ['request' => $this->request]);
320
            $this->apiUserdata = null;
321
322
            return false;
323
        }
324
    }
325
326
    /**
327
     * Default Avatar Isometric.
328
     *
329
     * @param int $size
330
     *
331
     * @throws \Throwable
332
     *
333
     * @return IsometricAvatar
334
     */
335
    public function isometricAvatarCurrentUser(int $size = 0): IsometricAvatar
336
    {
337
        $uuid = $this->userdata->uuid ?? env('DEFAULT_UUID');
338
        $timestamp = $this->userdata->updated_at->timestamp ?? \time();
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
339
        $isometricAvatar = new IsometricAvatar(
340
            $uuid,
341
            $timestamp
342
        );
343
        $isometricAvatar->render($size);
344
345
        return $isometricAvatar;
346
    }
347
348
    /**
349
     * Save skin image.
350
     *
351
     * @param mixed
352
     *
353
     * @return bool
354
     */
355
    public function saveRemoteSkin(): bool
356
    {
357
        if (!empty($this->userdata->skin) && $this->userdata->skin !== '') {
358
            try {
359
                $skinData = $this->mojangClient->getSkin($this->userdata->skin);
360
361
                return SkinsStorage::save($this->userdata->uuid, $skinData);
362
            } catch (\Exception $e) {
363
                Log::error($e->getTraceAsString());
364
            }
365
        }
366
367
        return SkinsStorage::copyAsSteve($this->userdata->uuid);
368
    }
369
370
    /**
371
     * Set force update.
372
     *
373
     * @param bool $forceUpdate
374
     */
375
    public function setForceUpdate(bool $forceUpdate): void
376
    {
377
        $this->forceUpdate = $forceUpdate;
378
    }
379
380
    /**
381
     * Can I exec force update?
382
     */
383
    private function forceUpdatePossible(): bool
384
    {
385
        return ($this->forceUpdate) &&
386
            ((\time() - $this->userdata->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...
387
    }
388
389
    /**
390
     * Use steve skin for given username.
391
     *
392
     * @param string
393
     */
394
    public function updateStats(): void
395
    {
396
        if (!empty($this->userdata->uuid) && env('STATS_ENABLED') && $this->userdata->uuid !== env('DEFAULT_UUID')) {
397
            $this->accountStatsRepository->incrementRequestCounter($this->userdata->uuid);
398
        }
399
    }
400
401
    /**
402
     * @return bool
403
     */
404
    private function initializeUuidRequest(): bool
405
    {
406
        if ($this->requestedUuidInDb()) {
407
            // Check if UUID is in my database
408
            // Data cache still valid?
409
            if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
410
                Log::debug('Refreshing User DB Data');
411
                // Nope, updating data
412
                $this->updateDbUser();
413
            }
414
415
            if (!SkinsStorage::exists($this->request)) {
416
                $this->saveRemoteSkin();
417
            }
418
419
            return true;
420
        }
421
422
        if ($this->insertNewUuid()) {
423
            return true;
424
        }
425
426
        return false;
427
    }
428
429
    /**
430
     * Set failed request.
431
     *
432
     * @param string $errorMessage
433
     */
434
    private function setFailedRequest(string $errorMessage = ''): void
435
    {
436
        Log::notice($errorMessage, ['request' => $this->request]);
437
        $this->userdata = null;
438
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
439
        $this->request = '';
440
    }
441
}
442