Passed
Push — master ( bebbb3...38c082 )
by Mattia
03:32
created

Core::initialize()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 12
rs 10
c 3
b 0
f 0
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\ImageSection;
11
use App\Image\IsometricAvatar;
12
use App\Image\Sections\Avatar;
13
use App\Image\Sections\Skin;
14
use App\Minecraft\MojangAccount;
15
use App\Minecraft\MojangClient;
16
use App\Models\Account;
17
use App\Repositories\AccountRepository;
18
use App\Repositories\AccountStatsRepository;
19
use Illuminate\Support\Facades\Event;
20
use Illuminate\Support\Facades\Log;
21
22
/**
23
 * Class Core.
24
 */
25
class Core
26
{
27
    /**
28
     * Requested string.
29
     *
30
     * @var string
31
     */
32
    private string $request = '';
33
    /**
34
     * Userdata from/to DB.
35
     *
36
     * @var Account
37
     */
38
    private $userdata;
39
40
    /**
41
     * Full userdata.
42
     *
43
     * @var MojangAccount
44
     */
45
    private ?MojangAccount $apiUserdata;
46
47
    /**
48
     * User data has been updated?
49
     *
50
     * @var bool
51
     */
52
    private bool $dataUpdated = false;
53
54
    /**
55
     * Set force update.
56
     *
57
     * @var bool
58
     */
59
    private bool $forceUpdate = false;
60
61
    /**
62
     * Current image path.
63
     *
64
     * @var string
65
     */
66
    private string $currentUserSkinImage;
67
68
    /**
69
     * @var AccountRepository
70
     */
71
    private AccountRepository $accountRepository;
72
73
    /**
74
     * @var AccountStatsRepository
75
     */
76
    private AccountStatsRepository $accountStatsRepository;
77
78
    /**
79
     * @var MojangClient
80
     */
81
    private MojangClient $mojangClient;
82
83
    /**
84
     * Core constructor.
85
     *
86
     * @param AccountRepository      $accountRepository      Where user data is stored
87
     * @param AccountStatsRepository $accountStatsRepository
88
     * @param MojangClient           $mojangClient           Client for Mojang API
89
     */
90
    public function __construct(
91
        AccountRepository $accountRepository,
92
        AccountStatsRepository $accountStatsRepository,
93
        MojangClient $mojangClient
94
    ) {
95
        $this->accountRepository = $accountRepository;
96
        $this->accountStatsRepository = $accountStatsRepository;
97
        $this->mojangClient = $mojangClient;
98
    }
99
100
    /**
101
     * Check if cache is still valid.
102
     *
103
     * @param int
104
     *
105
     * @return bool
106
     */
107
    private function checkDbCache(): bool
108
    {
109
        $accountUpdatedAtTimestamp = $this->userdata->updated_at->timestamp ?? 0;
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
110
111
        return (\time() - $accountUpdatedAtTimestamp) < env('USERDATA_CACHE_TIME');
112
    }
113
114
    /**
115
     * Load saved Account information.
116
     *
117
     * @param Account|null $account
118
     *
119
     * @return bool
120
     */
121
    private function loadAccountData(?Account $account): bool
122
    {
123
        if ($account !== null) {
124
            $this->userdata = $account;
125
            $this->currentUserSkinImage = SkinsStorage::getPath($this->userdata->uuid);
126
127
            return true;
128
        }
129
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
130
131
        return false;
132
    }
133
134
    /**
135
     * Return loaded user data.
136
     *
137
     * @return Account
138
     */
139
    public function getUserdata(): Account
140
    {
141
        return $this->userdata ?? new Account();
142
    }
143
144
    /**
145
     * Check if an UUID is in the database.
146
     *
147
     * @return bool Returns true/false
148
     */
149
    private function requestedUuidInDb(): bool
150
    {
151
        $account = $this->accountRepository->findByUuid($this->request);
152
153
        return $this->loadAccountData($account);
154
    }
155
156
    /**
157
     * Insert user data in database.
158
     *
159
     * @param void
160
     *
161
     * @return bool
162
     */
163
    public function insertNewUuid(): bool
164
    {
165
        if (UserNotFoundCache::has($this->request)) {
166
            Log::debug('Cache Hit Not Found', ['request' => $this->request]);
167
168
            return false;
169
        }
170
171
        if ($this->getFullUserdataApi()) {
172
            $this->userdata = $this->accountRepository->create([
173
                'username' => $this->apiUserdata->getUsername(),
174
                'uuid' => $this->apiUserdata->getUuid(),
175
                'skin' => $this->apiUserdata->getSkin(),
176
                'cape' => $this->apiUserdata->getCape(),
177
            ]);
178
179
            $this->saveRemoteSkin();
180
            $this->currentUserSkinImage = SkinsStorage::getPath($this->apiUserdata->getUuid());
181
182
            $this->accountStatsRepository->create([
183
                'uuid' => $this->userdata->uuid,
184
                'count_search' => 0,
185
                'count_request' => 0,
186
                'time_search' => 0,
187
                'time_request' => 0,
188
            ]);
189
190
            return true;
191
        }
192
193
        UserNotFoundCache::add($this->request);
194
195
        return false;
196
    }
197
198
    /**
199
     * Check requested string and initialize objects.
200
     *
201
     * @param string
202
     *
203
     * @throws \Exception
204
     *
205
     * @return bool
206
     */
207
    public function initialize(string $string): bool
208
    {
209
        $this->dataUpdated = false;
210
        $this->request = $string;
211
212
        if ($this->initializeUuidRequest()) {
213
            return true;
214
        }
215
216
        $this->setFailedRequest('Account not found');
217
218
        return false;
219
    }
220
221
    /**
222
     * Update current user fail count.
223
     */
224
    private function updateUserFailUpdate(): bool
225
    {
226
        if (isset($this->userdata->uuid)) {
227
            ++$this->userdata->fail_count;
228
229
            return $this->userdata->save();
230
        }
231
232
        return false;
233
    }
234
235
    /**
236
     * Update db user data.
237
     */
238
    private function updateDbUser(): bool
239
    {
240
        if (isset($this->userdata->username) && $this->userdata->uuid !== '') {
241
            // Get data from API
242
            if ($this->getFullUserdataApi()) {
243
                $originalUsername = $this->userdata->username;
244
                // Update database
245
                $this->accountRepository->update([
246
                    'username' => $this->apiUserdata->getUsername(),
247
                    'skin' => $this->apiUserdata->getSkin(),
248
                    'cape' => $this->apiUserdata->getCape(),
249
                    'fail_count' => 0,
250
                ], $this->userdata->id);
251
252
                $this->userdata->touch();
253
                $this->userdata->refresh();
254
255
                // Update skin
256
                $this->saveRemoteSkin();
257
258
                // Log username change
259
                if ($this->userdata->username !== $originalUsername && $originalUsername !== '') {
260
                    $this->logUsernameChange($this->userdata->uuid, $originalUsername, $this->userdata->username);
261
                }
262
                $this->dataUpdated = true;
263
264
                return true;
265
            }
266
267
            $this->updateUserFailUpdate();
268
269
            if (!SkinsStorage::exists($this->userdata->uuid)) {
270
                SkinsStorage::copyAsSteve($this->userdata->uuid);
271
            }
272
        }
273
        $this->dataUpdated = false;
274
275
        return false;
276
    }
277
278
    /**
279
     * Return if data has been updated.
280
     */
281
    public function userDataUpdated(): bool
282
    {
283
        return $this->dataUpdated;
284
    }
285
286
    /**
287
     * Log the username change.
288
     *
289
     * @param $uuid string User UUID
290
     * @param $prev string Previous username
291
     * @param $new string New username
292
     */
293
    private function logUsernameChange(string $uuid, string $prev, string $new): void
294
    {
295
        Event::dispatch(new UsernameChangeEvent($uuid, $prev, $new));
296
    }
297
298
    /**
299
     * Get userdata from Mojang API.
300
     *
301
     * @param mixed
302
     *
303
     * @return bool
304
     */
305
    private function getFullUserdataApi(): bool
306
    {
307
        try {
308
            $this->apiUserdata = $this->mojangClient->getUuidInfo($this->request);
309
310
            return true;
311
        } catch (\Exception $e) {
312
            Log::error($e->getTraceAsString(), ['request' => $this->request]);
313
            $this->apiUserdata = null;
314
315
            return false;
316
        }
317
    }
318
319
    /**
320
     * Show rendered avatar.
321
     *
322
     * @param int
323
     * @param mixed
324
     *
325
     * @throws \Throwable
326
     *
327
     * @return Avatar
328
     */
329
    public function avatarCurrentUser(int $size = 0): Avatar
330
    {
331
        $avatar = new Avatar($this->currentUserSkinImage);
332
        $avatar->renderAvatar($size);
333
334
        return $avatar;
335
    }
336
337
    /**
338
     * Default Avatar Isometric.
339
     *
340
     * @param int $size
341
     *
342
     * @throws \Throwable
343
     *
344
     * @return IsometricAvatar
345
     */
346
    public function isometricAvatarCurrentUser(int $size = 0): IsometricAvatar
347
    {
348
        $uuid = $this->userdata->uuid ?? env('DEFAULT_UUID');
349
        $timestamp = $this->userdata->updated_at->timestamp ?? \time();
0 ignored issues
show
Bug introduced by
The property timestamp does not exist on string.
Loading history...
350
        $isometricAvatar = new IsometricAvatar(
351
            $uuid,
352
            $timestamp
353
        );
354
        $isometricAvatar->render($size);
355
356
        return $isometricAvatar;
357
    }
358
359
    /**
360
     * Save skin image.
361
     *
362
     * @param mixed
363
     *
364
     * @return bool
365
     */
366
    public function saveRemoteSkin(): bool
367
    {
368
        if (!empty($this->userdata->skin) && $this->userdata->skin !== '') {
369
            try {
370
                $skinData = $this->mojangClient->getSkin($this->userdata->skin);
371
372
                return SkinsStorage::save($this->userdata->uuid, $skinData);
373
            } catch (\Exception $e) {
374
                Log::error($e->getTraceAsString());
375
            }
376
        }
377
378
        return SkinsStorage::copyAsSteve($this->userdata->uuid);
379
    }
380
381
    /**
382
     * Return rendered skin.
383
     *
384
     * @param int
385
     * @param string
386
     *
387
     * @throws \Throwable
388
     *
389
     * @return Skin
390
     */
391
    public function renderSkinCurrentUser(int $size = 0, string $type = ImageSection::FRONT): Skin
392
    {
393
        $skin = new Skin($this->currentUserSkinImage);
394
        $skin->renderSkin($size, $type);
395
396
        return $skin;
397
    }
398
399
    /**
400
     * Return a Skin object of the current user.
401
     */
402
    public function skinCurrentUser(): Skin
403
    {
404
        return new Skin($this->currentUserSkinImage);
405
    }
406
407
    /**
408
     * Set force update.
409
     *
410
     * @param bool $forceUpdate
411
     */
412
    public function setForceUpdate(bool $forceUpdate): void
413
    {
414
        $this->forceUpdate = $forceUpdate;
415
    }
416
417
    /**
418
     * Can I exec force update?
419
     */
420
    private function forceUpdatePossible(): bool
421
    {
422
        return ($this->forceUpdate) &&
423
            ((\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...
424
    }
425
426
    /**
427
     * Use steve skin for given username.
428
     *
429
     * @param string
430
     */
431
    public function updateStats($type = 'request'): void
432
    {
433
        if (!empty($this->userdata->uuid) && env('STATS_ENABLED') && $this->userdata->uuid !== env('DEFAULT_UUID')) {
434
            if ($type === 'request') {
435
                $this->accountStatsRepository->incrementRequestCounter($this->userdata->uuid);
436
            } elseif ($type === 'search') {
437
                $this->accountStatsRepository->incrementSearchCounter($this->userdata->uuid);
438
            }
439
        }
440
    }
441
442
    /**
443
     * @return bool
444
     */
445
    private function initializeUuidRequest(): bool
446
    {
447
        if ($this->requestedUuidInDb()) {
448
            // Check if UUID is in my database
449
            // Data cache still valid?
450
            if (!$this->checkDbCache() || $this->forceUpdatePossible()) {
451
                Log::debug('Refreshing User DB Data');
452
                // Nope, updating data
453
                $this->updateDbUser();
454
            }
455
456
            if (!SkinsStorage::exists($this->request)) {
457
                $this->saveRemoteSkin();
458
            }
459
460
            return true;
461
        }
462
463
        if ($this->insertNewUuid()) {
464
            return true;
465
        }
466
467
        return false;
468
    }
469
470
    /**
471
     * Set failed request.
472
     *
473
     * @param string $errorMessage
474
     */
475
    private function setFailedRequest(string $errorMessage = ''): void
476
    {
477
        Log::notice($errorMessage, ['request' => $this->request]);
478
        $this->userdata = null;
479
        $this->currentUserSkinImage = SkinsStorage::getPath(env('DEFAULT_USERNAME'));
480
        $this->request = '';
481
    }
482
}
483