Issues (974)

app/Models/User.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Models;
6
7
use App\Enums\QueueType;
8
use App\Enums\SignupError;
9
use App\Enums\UserRole;
10
use App\Jobs\SendAccountExpiredEmail;
11
use App\Jobs\SendAccountWillExpireEmail;
12
use App\Rules\ValidEmailDomain;
13
use App\Services\InvitationService;
14
use Carbon\CarbonImmutable;
15
use Illuminate\Database\Eloquent\Builder;
16
use Illuminate\Database\Eloquent\Casts\Attribute;
17
use Illuminate\Database\Eloquent\Collection;
18
use Illuminate\Database\Eloquent\Factories\HasFactory;
19
use Illuminate\Database\Eloquent\Relations\BelongsTo;
20
use Illuminate\Database\Eloquent\Relations\HasMany;
21
use Illuminate\Database\Eloquent\Relations\HasOne;
22
use Illuminate\Database\Eloquent\SoftDeletes;
23
use Illuminate\Foundation\Auth\User as Authenticatable;
24
use Illuminate\Http\Request;
25
use Illuminate\Notifications\Notifiable;
26
use Illuminate\Support\Arr;
27
use Illuminate\Support\Carbon;
28
use Illuminate\Support\Facades\Hash;
29
use Illuminate\Support\Facades\Log;
30
use Illuminate\Support\Facades\Password;
31
use Illuminate\Support\Facades\Validator;
32
use Illuminate\Support\Str;
33
use Jrean\UserVerification\Traits\UserVerification;
34
use Spatie\Permission\Models\Role;
35
use Spatie\Permission\Traits\HasRoles;
36
37
/**
38
 * @property int $id
39
 * @property string $username
40
 * @property string|null $firstname
41
 * @property string|null $lastname
42
 * @property string $email
43
 * @property string $password
44
 * @property int $roles_id
45
 * @property string|null $host
46
 * @property int $grabs
47
 * @property string $api_token
48
 * @property Carbon|null $created_at
49
 * @property Carbon|null $updated_at
50
 * @property string|null $resetguid
51
 * @property Carbon|null $lastlogin
52
 * @property Carbon|null $apiaccess
53
 * @property int $invites
54
 * @property int|null $invitedby
55
 * @property bool $movieview
56
 * @property bool $xxxview
57
 * @property bool $musicview
58
 * @property bool $consoleview
59
 * @property bool $bookview
60
 * @property bool $gameview
61
 * @property string|null $saburl
62
 * @property string|null $sabapikey
63
 * @property bool|null $sabapikeytype
64
 * @property bool|null $sabpriority
65
 * @property string|null $nzbgeturl
66
 * @property string|null $nzbgetusername
67
 * @property string|null $nzbgetpassword
68
 * @property string|null $nzbvortex_api_key
69
 * @property string|null $nzbvortex_server_url
70
 * @property string $notes
71
 * @property string|null $cp_url
72
 * @property string|null $cp_api
73
 * @property string|null $style
74
 * @property Carbon|null $rolechangedate
75
 * @property Carbon|null $pending_role_start_date
76
 * @property int|null $pending_roles_id
77
 * @property string|null $remember_token
78
 * @property int $rate_limit
79
 * @property Carbon|null $email_verified_at
80
 * @property bool $verified
81
 * @property string|null $verification_token
82
 * @property string|null $timezone
83
 * @property bool $can_post
84
 * @property-read Collection<int, ReleaseComment> $comments
85
 * @property-read Collection<int, UserDownload> $downloads
86
 * @property-read Collection<int, DnzbFailure> $failedReleases
87
 * @property-read Collection<int, Invitation> $invitations
88
 * @property-read Collection<int, UsersRelease> $releases
89
 * @property-read Collection<int, UserRequest> $requests
90
 * @property-read Collection<int, UserSerie> $series
91
 * @property-read Collection<int, RolePromotionStat> $promotionStats
92
 * @property-read Collection<int, UserRoleHistory> $roleHistory
93
 * @property-read Role|null $role
94
 * @property-read PasswordSecurity|null $passwordSecurity
95
 *
96
 * @method static Builder|User whereUsername(string $value)
97
 * @method static Builder|User whereEmail(string $value)
98
 * @method static Builder|User whereApiToken(string $value)
99
 * @method static Builder|User whereResetguid(string $value)
100
 * @method static Builder|User whereVerified(int $value)
101
 * @method static Builder|User active()
102
 * @method static Builder|User verified()
103
 * @method static Builder|User withRole(int|string $role)
104
 * @method static Builder|User expiringSoon(int $days = 7)
105
 * @method static Builder|User expired()
106
 */
107
final class User extends Authenticatable
108
{
109
    use HasFactory;
110
    use HasRoles;
111
    use Notifiable;
112
    use SoftDeletes;
113
    use UserVerification;
114
115
    /**
116
     * @var list<string>
117
     */
118
    protected $hidden = [
119
        'remember_token',
120
        'password',
121
    ];
122
123
    /**
124
     * @var list<string>
125
     */
126
    protected $guarded = [];
127
128
    /**
129
     * Roles excluded from promotions.
130
     *
131
     * @var list<string>
132
     */
133
    private const PROMOTION_EXCLUDED_ROLES = [
134
        'User',
135
        'Admin',
136
        'Moderator',
137
        'Disabled',
138
        'Friend',
139
    ];
140
141
    /**
142
     * Days in a year for subscription calculations.
143
     */
144
    private const DAYS_PER_YEAR = 365;
145
146
    /**
147
     * Get the attributes that should be cast.
148
     *
149
     * @return array<string, string>
150
     */
151
    protected function casts(): array
152
    {
153
        return [
154
            'email_verified_at' => 'datetime',
155
            'lastlogin' => 'datetime',
156
            'apiaccess' => 'datetime',
157
            'rolechangedate' => 'datetime',
158
            'pending_role_start_date' => 'datetime',
159
            'created_at' => 'datetime',
160
            'updated_at' => 'datetime',
161
            'password' => 'hashed',
162
            'movieview' => 'boolean',
163
            'xxxview' => 'boolean',
164
            'musicview' => 'boolean',
165
            'consoleview' => 'boolean',
166
            'bookview' => 'boolean',
167
            'gameview' => 'boolean',
168
            'sabapikeytype' => 'boolean',
169
            'sabpriority' => 'boolean',
170
            'verified' => 'boolean',
171
            'can_post' => 'boolean',
172
            'grabs' => 'integer',
173
            'invites' => 'integer',
174
            'rate_limit' => 'integer',
175
            'roles_id' => 'integer',
176
            'pending_roles_id' => 'integer',
177
            'invitedby' => 'integer',
178
        ];
179
    }
180
181
    protected function getDefaultGuardName(): string
182
    {
183
        return 'web';
184
    }
185
186
    // ===== Relationships =====
187
188
    public function role(): BelongsTo
189
    {
190
        return $this->belongsTo(Role::class, 'roles_id');
191
    }
192
193
    public function requests(): HasMany
194
    {
195
        return $this->hasMany(UserRequest::class, 'users_id');
196
    }
197
198
    public function downloads(): HasMany
199
    {
200
        return $this->hasMany(UserDownload::class, 'users_id');
201
    }
202
203
    public function releases(): HasMany
204
    {
205
        return $this->hasMany(UsersRelease::class, 'users_id');
206
    }
207
208
    public function series(): HasMany
209
    {
210
        return $this->hasMany(UserSerie::class, 'users_id');
211
    }
212
213
    public function invitations(): HasMany
214
    {
215
        return $this->hasMany(Invitation::class, 'invited_by');
216
    }
217
218
    public function failedReleases(): HasMany
219
    {
220
        return $this->hasMany(DnzbFailure::class, 'users_id');
221
    }
222
223
    public function comments(): HasMany
224
    {
225
        return $this->hasMany(ReleaseComment::class, 'users_id');
226
    }
227
228
    public function promotionStats(): HasMany
229
    {
230
        return $this->hasMany(RolePromotionStat::class);
231
    }
232
233
    public function roleHistory(): HasMany
234
    {
235
        return $this->hasMany(UserRoleHistory::class);
236
    }
237
238
    public function passwordSecurity(): HasOne
239
    {
240
        return $this->hasOne(PasswordSecurity::class);
241
    }
242
243
    // ===== Backward Compatibility Aliases =====
244
245
    public function request(): HasMany
246
    {
247
        return $this->requests();
248
    }
249
250
    public function download(): HasMany
251
    {
252
        return $this->downloads();
253
    }
254
255
    public function release(): HasMany
256
    {
257
        return $this->releases();
258
    }
259
260
    public function invitation(): HasMany
261
    {
262
        return $this->invitations();
263
    }
264
265
    public function failedRelease(): HasMany
266
    {
267
        return $this->failedReleases();
268
    }
269
270
    public function comment(): HasMany
271
    {
272
        return $this->comments();
273
    }
274
275
    // ===== Query Scopes =====
276
277
    /**
278
     * Scope to get active (non-disabled) users.
279
     */
280
    public function scopeActive(Builder $query): Builder
281
    {
282
        return $query->where('roles_id', '!=', UserRole::DISABLED->value);
283
    }
284
285
    /**
286
     * Scope to get verified users.
287
     */
288
    public function scopeVerified(Builder $query): Builder
289
    {
290
        return $query->where('verified', true);
291
    }
292
293
    /**
294
     * Scope to filter users by role.
295
     */
296
    public function scopeWithRole(Builder $query, int|string $role): Builder
297
    {
298
        if (is_numeric($role)) {
299
            return $query->where('roles_id', (int) $role);
300
        }
301
302
        return $query->whereHas('role', fn (Builder $q) => $q->where('name', $role));
303
    }
304
305
    /**
306
     * Scope to get users with roles expiring within specified days.
307
     */
308
    public function scopeExpiringSoon(Builder $query, int $days = 7): Builder
309
    {
310
        return $query->whereNotNull('rolechangedate')
311
            ->whereBetween('rolechangedate', [now(), now()->addDays($days)]);
312
    }
313
314
    /**
315
     * Scope to get users with expired roles.
316
     */
317
    public function scopeExpired(Builder $query): Builder
318
    {
319
        return $query->whereNotNull('rolechangedate')
320
            ->where('rolechangedate', '<', now());
321
    }
322
323
    /**
324
     * Scope to exclude sharing email.
325
     */
326
    public function scopeExcludeSharing(Builder $query): Builder
327
    {
328
        return $query->where('email', '!=', '[email protected]');
329
    }
330
331
    // ===== Attribute Accessors =====
332
333
    /**
334
     * Get the user's full name.
335
     */
336
    protected function fullName(): Attribute
337
    {
338
        return Attribute::make(
339
            get: fn (): string => trim("{$this->firstname} {$this->lastname}") ?: $this->username,
340
        );
341
    }
342
343
    /**
344
     * Get the user's timezone or default.
345
     */
346
    protected function effectiveTimezone(): Attribute
347
    {
348
        return Attribute::make(
349
            get: fn (): string => $this->timezone ?? 'UTC',
350
        );
351
    }
352
353
    /**
354
     * Check if user has admin privileges.
355
     */
356
    protected function isAdmin(): Attribute
357
    {
358
        return Attribute::make(
359
            get: fn (): bool => $this->roles_id === UserRole::ADMIN->value,
360
        );
361
    }
362
363
    /**
364
     * Check if user is disabled.
365
     */
366
    protected function isDisabled(): Attribute
367
    {
368
        return Attribute::make(
369
            get: fn (): bool => $this->roles_id === UserRole::DISABLED->value,
370
        );
371
    }
372
373
    /**
374
     * Check if role is expired.
375
     */
376
    protected function isRoleExpired(): Attribute
377
    {
378
        return Attribute::make(
379
            get: fn (): bool => $this->rolechangedate?->isPast() ?? false,
380
        );
381
    }
382
383
    /**
384
     * Get days until role expires.
385
     */
386
    protected function daysUntilExpiry(): Attribute
387
    {
388
        return Attribute::make(
389
            get: fn (): ?int => $this->rolechangedate
390
                ? (int) now()->diffInDays($this->rolechangedate, false)
391
                : null,
392
        );
393
    }
394
395
    // ===== Pending Role Methods =====
396
397
    /**
398
     * Check if user has a pending stacked role.
399
     */
400
    public function hasPendingRole(): bool
401
    {
402
        return $this->pending_roles_id !== null && $this->pending_role_start_date !== null;
403
    }
404
405
    /**
406
     * Get the pending role.
407
     */
408
    public function getPendingRole(): ?Role
409
    {
410
        if (! $this->hasPendingRole()) {
411
            return null;
412
        }
413
414
        return Role::find($this->pending_roles_id);
415
    }
416
417
    /**
418
     * Cancel a pending stacked role.
419
     */
420
    public function cancelPendingRole(): bool
421
    {
422
        if (! $this->hasPendingRole()) {
423
            return false;
424
        }
425
426
        return $this->update([
427
            'pending_roles_id' => null,
428
            'pending_role_start_date' => null,
429
        ]);
430
    }
431
432
    /**
433
     * Get all pending stacked role changes for this user.
434
     * Returns an array of stacked role changes that haven't been activated yet (effective_date is in the future).
435
     *
436
     * @return \Illuminate\Support\Collection<int, array{
437
     *     role: Role|null,
438
     *     role_name: string,
439
     *     start_date: Carbon,
440
     *     end_date: Carbon,
441
     *     is_current_pending: bool
442
     * }>
443
     */
444
    public function getAllPendingStackedRoles(): \Illuminate\Support\Collection
445
    {
446
        // Get all stacked role changes from history where effective_date is in the future
447
        $stackedHistory = UserRoleHistory::where('user_id', $this->id)
448
            ->where('is_stacked', true)
449
            ->where('effective_date', '>', now())
450
            ->orderBy('effective_date', 'asc')
451
            ->get();
452
453
        return $stackedHistory->map(function ($history) {
454
            $role = Role::find($history->new_role_id);
455
            return [
456
                'role' => $role,
457
                'role_name' => $role?->name ?? 'Unknown Role',
458
                'start_date' => $history->effective_date,
459
                'end_date' => $history->new_expiry_date,
460
                'is_current_pending' => $this->pending_roles_id === $history->new_role_id
461
                    && $this->pending_role_start_date?->equalTo($history->effective_date),
462
            ];
463
        });
464
    }
465
466
    /**
467
     * Get comprehensive role expiry information.
468
     *
469
     * @return array{
470
     *     current_role: Role|null,
471
     *     current_expiry: Carbon|null,
472
     *     has_expiry: bool,
473
     *     is_expired: bool,
474
     *     days_until_expiry: int|null,
475
     *     pending_role: Role|null,
476
     *     pending_start: Carbon|null,
477
     *     has_pending_role: bool
478
     * }
479
     */
480
    public function getRoleExpiryInfo(): array
481
    {
482
        return [
483
            'current_role' => $this->role,
484
            'current_expiry' => $this->rolechangedate,
485
            'has_expiry' => $this->rolechangedate !== null,
486
            'is_expired' => $this->is_role_expired,
487
            'days_until_expiry' => $this->days_until_expiry,
488
            'pending_role' => $this->getPendingRole(),
489
            'pending_start' => $this->pending_role_start_date,
490
            'has_pending_role' => $this->hasPendingRole(),
491
        ];
492
    }
493
494
    // ===== Static Query Methods =====
495
496
    /**
497
     * Get count of users matching filters.
498
     */
499
    public static function getCount(
500
        ?string $role = null,
501
        ?string $username = '',
502
        ?string $host = '',
503
        ?string $email = '',
504
        ?string $createdFrom = '',
505
        ?string $createdTo = '',
506
    ): int {
507
        return static::query()
508
            ->withTrashed()
509
            ->excludeSharing()
510
            ->when($role, fn (Builder $q) => $q->where('roles_id', $role))
511
            ->when($username, fn (Builder $q) => $q->where('username', 'like', "%{$username}%"))
512
            ->when($host, fn (Builder $q) => $q->where('host', 'like', "%{$host}%"))
513
            ->when($email, fn (Builder $q) => $q->where('email', 'like', "%{$email}%"))
514
            ->when($createdFrom, fn (Builder $q) => $q->where('created_at', '>=', "{$createdFrom} 00:00:00"))
515
            ->when($createdTo, fn (Builder $q) => $q->where('created_at', '<=', "{$createdTo} 23:59:59"))
516
            ->count();
517
    }
518
519
    /**
520
     * Find user by username.
521
     */
522
    public static function findByUsername(string $username): ?static
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STATIC on line 522 at column 62
Loading history...
523
    {
524
        return static::whereUsername($username)->first();
525
    }
526
527
    /**
528
     * Find user by email.
529
     */
530
    public static function findByEmail(string $email): ?static
531
    {
532
        return static::whereEmail($email)->first();
533
    }
534
535
    /**
536
     * Find user by RSS token.
537
     */
538
    public static function findByRssToken(string $token): ?static
539
    {
540
        return static::whereApiToken($token)->first();
541
    }
542
543
    /**
544
     * Find user by password reset GUID.
545
     */
546
    public static function findByResetGuid(string $guid): ?static
547
    {
548
        return static::whereResetguid($guid)->first();
549
    }
550
551
    // ===== Backward Compatibility Aliases =====
552
553
    /**
554
     * @deprecated Use findByUsername() instead
555
     */
556
    public static function getByUsername(string $userName): ?static
557
    {
558
        return static::findByUsername($userName);
559
    }
560
561
    /**
562
     * @deprecated Use findByEmail() instead
563
     */
564
    public static function getByEmail(string $email): ?static
565
    {
566
        return static::findByEmail($email);
567
    }
568
569
    /**
570
     * @deprecated Use findByRssToken() instead
571
     */
572
    public static function getByRssToken(string $rssToken): ?static
573
    {
574
        return static::findByRssToken($rssToken);
575
    }
576
577
    /**
578
     * @deprecated Use findByResetGuid() instead
579
     */
580
    public static function getByPassResetGuid(string $guid): ?static
581
    {
582
        return static::findByResetGuid($guid);
583
    }
584
585
    // ===== User Management Methods =====
586
587
    /**
588
     * Delete user by ID.
589
     *
590
     * @throws \Exception
591
     */
592
    public static function deleteUser(int $id): void
593
    {
594
        static::findOrFail($id)->delete();
595
    }
596
597
    /**
598
     * Update user details.
599
     */
600
    public static function updateUser(
601
        int $id,
602
        string $userName,
603
        ?string $email,
604
        int $grabs,
605
        int $role,
606
        ?string $notes,
607
        int $invites,
608
        int $movieview,
609
        int $musicview,
610
        int $gameview,
611
        int $xxxview,
612
        int $consoleview,
613
        int $bookview,
614
        string $style = 'None',
615
    ): int {
616
        $user = static::findOrFail($id);
617
        $roleModel = Role::find($role);
618
619
        $user->update([
620
            'username' => trim($userName),
621
            'grabs' => $grabs,
622
            'roles_id' => $role,
623
            'notes' => substr($notes ?? '', 0, 255),
624
            'invites' => $invites,
625
            'movieview' => $movieview,
626
            'musicview' => $musicview,
627
            'gameview' => $gameview,
628
            'xxxview' => $xxxview,
629
            'consoleview' => $consoleview,
630
            'bookview' => $bookview,
631
            'style' => $style,
632
            'rate_limit' => $roleModel?->rate_limit ?? 60,
633
            ...($email ? ['email' => trim($email)] : []),
634
        ]);
635
636
        if ($roleModel) {
637
            $user->syncRoles([$roleModel->name]);
638
        }
639
640
        return SignupError::SUCCESS->value;
641
    }
642
643
    /**
644
     * Update user role with optional stacking and promotions.
645
     */
646
    public static function updateUserRole(
647
        int $uid,
648
        int|string $role,
649
        bool $applyPromotions = true,
650
        bool $stackRole = true,
651
        ?int $changedBy = null,
652
        ?string $originalExpiryBeforeEdits = null,
653
        bool $preserveCurrentExpiry = false,
654
        ?int $addYears = null,
655
    ): bool {
656
        Log::info('updateUserRole called', [
657
            'uid' => $uid,
658
            'role' => $role,
659
            'applyPromotions' => $applyPromotions,
660
            'stackRole' => $stackRole,
661
            'changedBy' => $changedBy,
662
            'originalExpiryBeforeEdits' => $originalExpiryBeforeEdits,
663
            'preserveCurrentExpiry' => $preserveCurrentExpiry,
664
            'addYears' => $addYears,
665
        ]);
666
667
        $roleModel = is_numeric($role)
668
            ? Role::find((int) $role)
669
            : Role::where('name', $role)->first();
670
671
        if (! $roleModel) {
672
            Log::error('Role not found', ['role' => $role]);
673
674
            return false;
675
        }
676
677
        $user = static::find($uid);
678
        if (! $user) {
679
            Log::error('User not found', ['uid' => $uid]);
680
681
            return false;
682
        }
683
684
        $currentRoleId = $user->roles_id;
685
        $oldExpiryDate = $originalExpiryBeforeEdits
686
            ? Carbon::parse($originalExpiryBeforeEdits)
687
            : $user->rolechangedate;
688
        $currentExpiryDate = $user->rolechangedate;
689
690
        Log::info('updateUserRole - expiry date values', [
691
            'originalExpiryBeforeEdits_raw' => $originalExpiryBeforeEdits,
692
            'user_rolechangedate_raw' => $user->rolechangedate,
693
            'oldExpiryDate' => $oldExpiryDate?->toDateTimeString(),
694
            'currentExpiryDate' => $currentExpiryDate?->toDateTimeString(),
695
            'currentRoleId' => $currentRoleId,
696
            'newRoleId' => $roleModel->id,
697
        ]);
698
699
        // No role change needed
700
        if ($currentRoleId === $roleModel->id) {
701
            return self::handleSameRoleUpdate(
702
                $user,
703
                $roleModel,
704
                $applyPromotions,
705
                $addYears,
706
                $currentExpiryDate
707
            );
708
        }
709
710
        // Determine if we should stack
711
        $shouldStack = $stackRole && $currentExpiryDate?->isFuture();
712
713
        if ($shouldStack) {
714
            return self::handleStackedRoleChange(
715
                $user,
716
                $roleModel,
717
                $currentRoleId,
718
                $oldExpiryDate,
719
                $currentExpiryDate,
720
                $applyPromotions,
721
                $addYears,
722
                $changedBy
723
            );
724
        }
725
726
        return self::handleImmediateRoleChange(
727
            $user,
728
            $roleModel,
729
            $currentRoleId,
730
            $oldExpiryDate,
731
            $currentExpiryDate,
732
            $applyPromotions,
733
            $addYears,
734
            $changedBy,
735
            $preserveCurrentExpiry
736
        );
737
    }
738
739
    /**
740
     * Handle role update when role is not changing.
741
     */
742
    private static function handleSameRoleUpdate(
743
        self $user,
744
        Role $roleModel,
745
        bool $applyPromotions,
746
        ?int $addYears,
747
        ?Carbon $currentExpiryDate,
748
    ): bool {
749
        if (in_array($roleModel->name, self::PROMOTION_EXCLUDED_ROLES, true)) {
750
            return true;
751
        }
752
753
        $additionalDays = ($addYears ?? 0) * self::DAYS_PER_YEAR;
754
        $promotionDays = $applyPromotions
755
            ? RolePromotion::calculateAdditionalDays($roleModel->id)
756
            : 0;
757
        $totalDays = $additionalDays + $promotionDays;
758
759
        if ($totalDays <= 0) {
760
            return true;
761
        }
762
763
        $newExpiryDate = $currentExpiryDate?->isFuture()
764
            ? $currentExpiryDate->copy()->addDays($totalDays)
765
            : now()->addDays($totalDays);
766
767
        $updated = $user->update(['rolechangedate' => $newExpiryDate]);
768
769
        if ($updated && $promotionDays > 0) {
770
            self::trackPromotionApplication($user, $roleModel->id, $currentExpiryDate, $newExpiryDate);
771
        }
772
773
        return $updated;
774
    }
775
776
    /**
777
     * Handle stacked role change.
778
     */
779
    private static function handleStackedRoleChange(
780
        self $user,
781
        Role $roleModel,
782
        int $currentRoleId,
783
        ?Carbon $oldExpiryDate,
784
        Carbon $currentExpiryDate,
785
        bool $applyPromotions,
786
        ?int $addYears,
787
        ?int $changedBy,
788
    ): bool {
789
        // Determine the stacking start date
790
        // If user already has a pending stacked role, we need to calculate when THAT role would expire
791
        // and use that as the starting point for this new stacked role
792
793
        $stackingStartDate = $currentExpiryDate;
794
795
        // Check if there's already a pending role - if so, calculate when it would expire
796
        if ($user->hasPendingRole()) {
797
            $pendingRole = $user->getPendingRole();
798
            $pendingStartDate = $user->pending_role_start_date;
799
800
            if ($pendingRole && $pendingStartDate) {
801
                // Calculate when the pending role would expire
802
                $pendingRoleBaseDays = $pendingRole->addyears * self::DAYS_PER_YEAR;
803
                $pendingRolePromotionDays = ! in_array($pendingRole->name, self::PROMOTION_EXCLUDED_ROLES, true)
804
                    ? RolePromotion::calculateAdditionalDays($pendingRole->id)
805
                    : 0;
806
                $pendingRoleTotalDays = $pendingRoleBaseDays + $pendingRolePromotionDays;
807
                $pendingRoleExpiryDate = Carbon::parse($pendingStartDate)->addDays($pendingRoleTotalDays);
808
809
                Log::info('Existing pending role detected - calculating new stacking start date', [
810
                    'pendingRoleId' => $pendingRole->id,
811
                    'pendingRoleName' => $pendingRole->name,
812
                    'pendingStartDate' => $pendingStartDate->toDateTimeString(),
813
                    'pendingRoleBaseDays' => $pendingRoleBaseDays,
814
                    'pendingRolePromotionDays' => $pendingRolePromotionDays,
815
                    'pendingRoleExpiryDate' => $pendingRoleExpiryDate->toDateTimeString(),
816
                ]);
817
818
                // Use the pending role's expiry date as the new stacking start date
819
                if ($pendingRoleExpiryDate->isFuture()) {
820
                    $stackingStartDate = $pendingRoleExpiryDate;
821
                }
822
            }
823
        } else {
824
            // No pending role - use the latest of oldExpiryDate and currentExpiryDate
825
            Log::info('No pending role - comparing old and current expiry dates', [
826
                'oldExpiryDate' => $oldExpiryDate?->toDateTimeString(),
827
                'currentExpiryDate' => $currentExpiryDate->toDateTimeString(),
828
            ]);
829
830
            // Use the greater of the two dates if old expiry exists and is in the future
831
            if ($oldExpiryDate !== null && $oldExpiryDate->isFuture() && $oldExpiryDate->gt($currentExpiryDate)) {
832
                $stackingStartDate = $oldExpiryDate;
833
            }
834
        }
835
836
        Log::info('Final stacking start date selected', [
837
            'stackingStartDate' => $stackingStartDate->toDateTimeString(),
838
            'hadPendingRole' => $user->hasPendingRole(),
839
        ]);
840
841
        $baseDays = ($addYears ?? $roleModel->addyears) * self::DAYS_PER_YEAR;
842
        $promotionDays = ! in_array($roleModel->name, self::PROMOTION_EXCLUDED_ROLES, true) && $applyPromotions
843
            ? RolePromotion::calculateAdditionalDays($roleModel->id)
844
            : 0;
845
        $totalDays = $baseDays + $promotionDays;
846
        $newExpiryDate = $stackingStartDate->copy()->addDays($totalDays);
847
848
        $user->update([
849
            'pending_roles_id' => $roleModel->id,
850
            'pending_role_start_date' => $stackingStartDate,
851
        ]);
852
853
        try {
854
            UserRoleHistory::recordRoleChange(
855
                userId: $user->id,
856
                oldRoleId: $currentRoleId,
857
                newRoleId: $roleModel->id,
858
                oldExpiryDate: $stackingStartDate,
859
                newExpiryDate: $newExpiryDate,
860
                effectiveDate: $stackingStartDate,
861
                isStacked: true,
862
                changeReason: 'stacked_role_change',
863
                changedBy: $changedBy
864
            );
865
        } catch (\Exception $e) {
866
            Log::error('Failed to record role history', ['error' => $e->getMessage()]);
867
        }
868
869
        return true;
870
    }
871
872
    /**
873
     * Handle immediate role change.
874
     */
875
    private static function handleImmediateRoleChange(
876
        self $user,
877
        Role $roleModel,
878
        int $currentRoleId,
879
        ?Carbon $oldExpiryDate,
880
        ?Carbon $currentExpiryDate,
881
        bool $applyPromotions,
882
        ?int $addYears,
883
        ?int $changedBy,
884
        bool $preserveCurrentExpiry,
885
    ): bool {
886
        $baseDays = ($addYears ?? $roleModel->addyears) * self::DAYS_PER_YEAR;
887
        $promotionDays = ! in_array($roleModel->name, self::PROMOTION_EXCLUDED_ROLES, true) && $applyPromotions
888
            ? RolePromotion::calculateAdditionalDays($roleModel->id)
889
            : 0;
890
        $totalDays = $baseDays + $promotionDays;
891
892
        $newExpiryDate = match (true) {
893
            $preserveCurrentExpiry && $currentExpiryDate !== null => $currentExpiryDate,
894
            $totalDays > 0 => now()->addDays($totalDays),
895
            default => null,
896
        };
897
898
        $updated = $user->update([
899
            'roles_id' => $roleModel->id,
900
            'rolechangedate' => $newExpiryDate,
901
        ]);
902
903
        $user->syncRoles([$roleModel->name]);
904
905
        if ($updated) {
906
            try {
907
                UserRoleHistory::recordRoleChange(
908
                    userId: $user->id,
909
                    oldRoleId: $currentRoleId,
910
                    newRoleId: $roleModel->id,
911
                    oldExpiryDate: $oldExpiryDate,
912
                    newExpiryDate: $newExpiryDate,
913
                    effectiveDate: now(),
914
                    isStacked: false,
915
                    changeReason: 'immediate_role_change',
916
                    changedBy: $changedBy
917
                );
918
            } catch (\Exception $e) {
919
                Log::error('Failed to record role history', ['error' => $e->getMessage()]);
920
            }
921
922
            if ($applyPromotions && $promotionDays > 0) {
923
                self::trackPromotionApplication($user, $roleModel->id, $oldExpiryDate, $newExpiryDate);
924
            }
925
        }
926
927
        return $updated;
928
    }
929
930
    /**
931
     * Track promotion application statistics.
932
     */
933
    private static function trackPromotionApplication(
934
        self $user,
935
        int $roleId,
936
        ?Carbon $oldExpiryDate,
937
        ?Carbon $newExpiryDate,
938
    ): void {
939
        $promotions = RolePromotion::getActivePromotions($roleId);
940
        foreach ($promotions as $promotion) {
941
            $promotion->trackApplication($user->id, $roleId, $oldExpiryDate, $newExpiryDate);
942
        }
943
    }
944
945
    /**
946
     * Process expired roles and send notifications.
947
     */
948
    public static function updateExpiredRoles(): void
949
    {
950
        $now = CarbonImmutable::now();
951
952
        // Send expiration warnings
953
        self::sendExpirationWarnings($now);
954
955
        // Process expired roles
956
        self::processExpiredRoles($now);
957
    }
958
959
    /**
960
     * Send expiration warning emails.
961
     */
962
    private static function sendExpirationWarnings(CarbonImmutable $now): void
963
    {
964
        $periods = [
965
            'day' => $now->addDay(),
966
            'week' => $now->addWeek(),
967
            'month' => $now->addMonth(),
968
        ];
969
970
        foreach ($periods as $period) {
971
            $users = static::whereDate('rolechangedate', '=', $period)->get();
972
            $days = $now->diffInDays($period, true);
973
974
            foreach ($users as $user) {
975
                SendAccountWillExpireEmail::dispatch($user, $days)->onQueue('emails');
976
            }
977
        }
978
    }
979
980
    /**
981
     * Process users with expired roles.
982
     */
983
    private static function processExpiredRoles(CarbonImmutable $now): void
984
    {
985
        static::expired()->each(function (self $user) use ($now) {
986
            $oldRoleId = $user->roles_id;
987
            $oldExpiryDate = $user->rolechangedate;
988
989
            // Check for pending stacked role
990
            if ($user->hasPendingRole() && $user->pending_role_start_date?->lte($now)) {
991
                self::activatePendingRole($user, $oldRoleId, $oldExpiryDate, $now);
992
993
                return;
994
            }
995
996
            // Downgrade to default user role
997
            self::downgradeToDefaultRole($user, $oldRoleId, $oldExpiryDate, $now);
998
        });
999
    }
1000
1001
    /**
1002
     * Activate a pending stacked role.
1003
     */
1004
    private static function activatePendingRole(
1005
        self $user,
1006
        int $oldRoleId,
1007
        ?Carbon $oldExpiryDate,
1008
        CarbonImmutable $now,
1009
    ): void {
1010
        $roleModel = Role::find($user->pending_roles_id);
1011
        if (! $roleModel) {
1012
            return;
1013
        }
1014
1015
        $baseDays = $roleModel->addyears * self::DAYS_PER_YEAR;
1016
        $promotionDays = RolePromotion::calculateAdditionalDays($roleModel->id);
1017
        $totalDays = $baseDays + $promotionDays;
1018
        $newExpiryDate = $totalDays > 0 ? $now->addDays($totalDays) : null;
1019
1020
        $user->update([
1021
            'roles_id' => $roleModel->id,
1022
            'rolechangedate' => $newExpiryDate,
1023
            'pending_roles_id' => null,
1024
            'pending_role_start_date' => null,
1025
        ]);
1026
        $user->syncRoles([$roleModel->name]);
1027
1028
        UserRoleHistory::recordRoleChange(
1029
            userId: $user->id,
1030
            oldRoleId: $oldRoleId,
1031
            newRoleId: $roleModel->id,
1032
            oldExpiryDate: $oldExpiryDate,
1033
            newExpiryDate: $newExpiryDate,
1034
            effectiveDate: Carbon::instance($now),
1035
            isStacked: true,
1036
            changeReason: 'stacked_role_activated',
1037
            changedBy: null
1038
        );
1039
    }
1040
1041
    /**
1042
     * Downgrade user to default role.
1043
     */
1044
    private static function downgradeToDefaultRole(
1045
        self $user,
1046
        int $oldRoleId,
1047
        ?Carbon $oldExpiryDate,
1048
        CarbonImmutable $now,
1049
    ): void {
1050
        $user->update([
1051
            'roles_id' => UserRole::USER->value,
1052
            'rolechangedate' => null,
1053
            'pending_roles_id' => null,
1054
            'pending_role_start_date' => null,
1055
        ]);
1056
        $user->syncRoles(['User']);
1057
1058
        UserRoleHistory::recordRoleChange(
1059
            userId: $user->id,
1060
            oldRoleId: $oldRoleId,
1061
            newRoleId: UserRole::USER->value,
1062
            oldExpiryDate: $oldExpiryDate,
1063
            newExpiryDate: null,
1064
            effectiveDate: Carbon::instance($now),
1065
            isStacked: false,
1066
            changeReason: 'role_expired',
1067
            changedBy: null
1068
        );
1069
1070
        SendAccountExpiredEmail::dispatch($user)->onQueue('emails');
1071
    }
1072
1073
    /**
1074
     * Get paginated user list with filters.
1075
     *
1076
     * @return Collection<int, static>
1077
     * @throws \Throwable
1078
     */
1079
    public static function getRange(
1080
        int|false $start,
1081
        int $offset,
1082
        string $orderBy,
1083
        ?string $userName = '',
1084
        ?string $email = '',
1085
        ?string $host = '',
1086
        ?string $role = '',
1087
        bool $apiRequests = false,
1088
        ?string $createdFrom = '',
1089
        ?string $createdTo = '',
1090
    ): Collection {
1091
        $order = self::getBrowseOrder($orderBy);
1092
1093
        if ($apiRequests) {
1094
            UserRequest::clearApiRequests(false);
1095
1096
            $query = '
1097
                SELECT users.*, roles.name AS rolename, COUNT(user_requests.id) AS apirequests
1098
                FROM users
1099
                INNER JOIN roles ON roles.id = users.roles_id
1100
                LEFT JOIN user_requests ON user_requests.users_id = users.id
1101
                WHERE users.id != 0 %s %s %s %s %s %s
1102
                AND email != \'[email protected]\'
1103
                GROUP BY users.id
1104
                ORDER BY %s %s %s';
1105
        } else {
1106
            $query = '
1107
                SELECT users.*, roles.name AS rolename
1108
                FROM users
1109
                INNER JOIN roles ON roles.id = users.roles_id
1110
                WHERE 1=1 %s %s %s %s %s %s
1111
                ORDER BY %s %s %s';
1112
        }
1113
1114
        return static::fromQuery(
1115
            sprintf(
1116
                $query,
1117
                $userName ? 'AND users.username LIKE '.escapeString("%{$userName}%") : '',
1118
                $email ? 'AND users.email LIKE '.escapeString("%{$email}%") : '',
1119
                $host ? 'AND users.host LIKE '.escapeString("%{$host}%") : '',
1120
                $role ? "AND users.roles_id = {$role}" : '',
1121
                $createdFrom ? 'AND users.created_at >= '.escapeString("{$createdFrom} 00:00:00") : '',
1122
                $createdTo ? 'AND users.created_at <= '.escapeString("{$createdTo} 23:59:59") : '',
1123
                $order[0],
1124
                $order[1],
1125
                $start === false ? '' : "LIMIT {$offset} OFFSET {$start}"
1126
            )
1127
        );
1128
    }
1129
1130
    /**
1131
     * Get sort configuration for user browsing.
1132
     *
1133
     * @return array{0: string, 1: string}
1134
     */
1135
    public static function getBrowseOrder(string $orderBy = ''): array
1136
    {
1137
        $order = $orderBy ?: 'username_desc';
1138
        $parts = explode('_', $order);
1139
1140
        $field = match ($parts[0]) {
1141
            'email' => 'email',
1142
            'host' => 'host',
1143
            'createdat' => 'created_at',
1144
            'lastlogin' => 'lastlogin',
1145
            'apiaccess' => 'apiaccess',
1146
            'grabs' => 'grabs',
1147
            'role' => 'rolename',
1148
            'rolechangedate' => 'rolechangedate',
1149
            'verification' => 'verified',
1150
            default => 'username',
1151
        };
1152
1153
        $direction = isset($parts[1]) && preg_match('/^asc|desc$/i', $parts[1])
1154
            ? $parts[1]
1155
            : 'desc';
1156
1157
        return [$field, $direction];
1158
    }
1159
1160
    // ===== Password Methods =====
1161
1162
    /**
1163
     * Verify password and rehash if needed.
1164
     */
1165
    public static function checkPassword(string $password, string $hash, int $userId = -1): bool
1166
    {
1167
        if (! Hash::check($password, $hash)) {
1168
            return false;
1169
        }
1170
1171
        if ($userId > 0 && Hash::needsRehash($hash)) {
1172
            static::find($userId)?->update(['password' => Hash::make($password)]);
1173
        }
1174
1175
        return true;
1176
    }
1177
1178
    /**
1179
     * Hash a password.
1180
     */
1181
    public static function hashPassword(string $password): string
1182
    {
1183
        return Hash::make($password);
1184
    }
1185
1186
    /**
1187
     * Generate a secure random password.
1188
     *
1189
     * @throws \Exception
1190
     */
1191
    public static function generatePassword(int $length = 15): string
1192
    {
1193
        return Str::password($length);
1194
    }
1195
1196
    // ===== Update Methods =====
1197
1198
    /**
1199
     * Regenerate RSS key.
1200
     */
1201
    public static function updateRssKey(int $uid): int
1202
    {
1203
        static::find($uid)?->update([
1204
            'api_token' => md5(Password::getRepository()->createNewToken()),
1205
        ]);
1206
1207
        return SignupError::SUCCESS->value;
1208
    }
1209
1210
    /**
1211
     * Update password reset GUID.
1212
     */
1213
    public static function updatePassResetGuid(int $id, ?string $guid): int
1214
    {
1215
        static::find($id)?->update(['resetguid' => $guid]);
1216
1217
        return SignupError::SUCCESS->value;
1218
    }
1219
1220
    /**
1221
     * Update user password.
1222
     */
1223
    public static function updatePassword(int $id, string $password): int
1224
    {
1225
        static::find($id)?->update(['password' => Hash::make($password)]);
1226
1227
        return SignupError::SUCCESS->value;
1228
    }
1229
1230
    /**
1231
     * Update user role change date.
1232
     */
1233
    public static function updateUserRoleChangeDate(int $id, string $roleChangeDate): int
1234
    {
1235
        static::find($id)?->update(['rolechangedate' => $roleChangeDate]);
1236
1237
        return SignupError::SUCCESS->value;
1238
    }
1239
1240
    /**
1241
     * Increment user's grab count.
1242
     */
1243
    public static function incrementGrabs(int $id, int $num = 1): void
1244
    {
1245
        static::find($id)?->increment('grabs', $num);
1246
    }
1247
1248
    // ===== Validation Methods =====
1249
1250
    /**
1251
     * Validate a URL format.
1252
     */
1253
    public static function isValidUrl(string $url): bool
1254
    {
1255
        return (bool) preg_match(
1256
            '/^(https?|ftp):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?/i',
1257
            $url
1258
        );
1259
    }
1260
1261
    // ===== Registration Methods =====
1262
1263
    /**
1264
     * Register a new user.
1265
     *
1266
     * @throws \Exception
1267
     */
1268
    public static function signUp(
1269
        string $userName,
1270
        string $password,
1271
        string $email,
1272
        string $host,
1273
        ?string $notes,
1274
        int $invites = Invitation::DEFAULT_INVITES,
1275
        string $inviteCode = '',
1276
        bool $forceInviteMode = false,
1277
        int $role = UserRole::USER->value,
1278
        bool $validate = true,
1279
    ): bool|int|string {
1280
        $userData = [
1281
            'username' => trim($userName),
1282
            'password' => trim($password),
1283
            'email' => trim($email),
1284
        ];
1285
1286
        if ($validate) {
1287
            $validator = Validator::make($userData, [
1288
                'username' => ['required', 'string', 'min:5', 'max:255', 'unique:users'],
1289
                'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new ValidEmailDomain],
1290
                'password' => [
1291
                    'required',
1292
                    'string',
1293
                    'min:8',
1294
                    'confirmed',
1295
                    'regex:/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/',
1296
                ],
1297
            ]);
1298
1299
            if ($validator->fails()) {
1300
                return implode('', Arr::collapse($validator->errors()->toArray()));
1301
            }
1302
        }
1303
1304
        $invitedBy = 0;
1305
        if (! $forceInviteMode && (int) Settings::settingValue('registerstatus') === Settings::REGISTER_STATUS_INVITE) {
1306
            if ($inviteCode === '') {
1307
                return SignupError::BAD_INVITE_CODE->value;
1308
            }
1309
1310
            $invitedBy = self::checkAndUseInvite($inviteCode);
1311
            if ($invitedBy < 0) {
1312
                return SignupError::BAD_INVITE_CODE->value;
1313
            }
1314
        }
1315
1316
        return self::add(
1317
            $userData['username'],
1318
            $userData['password'],
1319
            $userData['email'],
1320
            $role,
1321
            $notes,
1322
            $host,
1323
            $invites,
1324
            $invitedBy
1325
        );
1326
    }
1327
1328
    /**
1329
     * Validate and consume an invite code.
1330
     */
1331
    public static function checkAndUseInvite(string $inviteCode): int
1332
    {
1333
        $invite = Invitation::findValidByToken($inviteCode);
1334
        if (! $invite) {
1335
            return -1;
1336
        }
1337
1338
        static::where('id', $invite->invited_by)->decrement('invites');
1339
        $invite->markAsUsed(0);
1340
1341
        return $invite->invited_by;
1342
    }
1343
1344
    /**
1345
     * Create a new user.
1346
     *
1347
     * @return int|false
1348
     */
1349
    public static function add(
1350
        string $userName,
1351
        string $password,
1352
        string $email,
1353
        int $role,
1354
        ?string $notes = '',
1355
        string $host = '',
1356
        int $invites = Invitation::DEFAULT_INVITES,
1357
        int $invitedBy = 0,
1358
    ): int|false {
1359
        $hashedPassword = Hash::make($password);
1360
1361
        $storeIps = config('nntmux:settings.store_user_ip') === true ? $host : '';
1362
1363
        $user = static::create([
1364
            'username' => $userName,
1365
            'password' => $hashedPassword,
1366
            'email' => $email,
1367
            'host' => $storeIps,
1368
            'roles_id' => $role,
1369
            'invites' => $invites,
1370
            'invitedby' => $invitedBy === 0 ? null : $invitedBy,
1371
            'notes' => $notes,
1372
        ]);
1373
1374
        return $user->id;
1375
    }
1376
1377
    // ===== Category Exclusion Methods =====
1378
1379
    /**
1380
     * Get excluded category IDs for a user.
1381
     *
1382
     * @return array<int>
1383
     * @throws \Exception
1384
     */
1385
    public static function getCategoryExclusionById(int $userId): array
1386
    {
1387
        $user = static::findOrFail($userId);
1388
1389
        $userAllowed = $user->getDirectPermissions()->pluck('name')->toArray();
1390
        $roleAllowed = $user->getAllPermissions()->pluck('name')->toArray();
1391
        $allowed = array_intersect($roleAllowed, $userAllowed);
1392
1393
        $categoryPermissions = [
1394
            'view console' => 1000,
1395
            'view movies' => 2000,
1396
            'view audio' => 3000,
1397
            'view pc' => 4000,
1398
            'view tv' => 5000,
1399
            'view adult' => 6000,
1400
            'view books' => 7000,
1401
            'view other' => 1,
1402
        ];
1403
1404
        $excludedRoots = [];
1405
        foreach ($categoryPermissions as $permission => $rootId) {
1406
            if (! in_array($permission, $allowed, false)) {
1407
                $excludedRoots[] = $rootId;
1408
            }
1409
        }
1410
1411
        return Category::whereIn('root_categories_id', $excludedRoots)
1412
            ->pluck('id')
1413
            ->toArray();
1414
    }
1415
1416
    /**
1417
     * Get excluded categories for API request.
1418
     *
1419
     * @throws \Exception
1420
     */
1421
    public static function getCategoryExclusionForApi(Request $request): array
1422
    {
1423
        $apiToken = $request->input('api_token') ?? $request->input('apikey');
1424
        $user = static::findByRssToken($apiToken);
1425
1426
        return $user ? static::getCategoryExclusionById($user->id) : [];
1427
    }
1428
1429
    // ===== Invitation Methods =====
1430
1431
    /**
1432
     * Send an invitation email.
1433
     *
1434
     * @throws \Exception
1435
     */
1436
    public static function sendInvite(string $serverUrl, int $uid, string $emailTo): string
1437
    {
1438
        $user = static::findOrFail($uid);
1439
1440
        $invitation = Invitation::createInvitation($emailTo, $user->id);
1441
        $url = "{$serverUrl}/register?token={$invitation->token}";
1442
1443
        app(InvitationService::class)->sendInvitationEmail($invitation);
1444
1445
        return $url;
1446
    }
1447
1448
    // ===== Cleanup Methods =====
1449
1450
    /**
1451
     * Delete unverified users older than 3 days.
1452
     */
1453
    public static function deleteUnVerified(): void
1454
    {
1455
        static::whereVerified(0)
1456
            ->where('created_at', '<', now()->subDays(3))
1457
            ->delete();
1458
    }
1459
1460
    /**
1461
     * Check if user can post.
1462
     */
1463
    public static function canPost(int $userId): bool
1464
    {
1465
        return (bool) static::where('id', $userId)->value('can_post');
1466
    }
1467
1468
    /**
1469
     * Get the user's timezone.
1470
     */
1471
    public function getTimezone(): string
1472
    {
1473
        return $this->timezone ?? 'UTC';
1474
    }
1475
1476
    // ===== Legacy Constants (Deprecated - Use Enums) =====
1477
1478
    /** @deprecated Use SignupError::BAD_USERNAME->value instead */
1479
    public const ERR_SIGNUP_BADUNAME = SignupError::BAD_USERNAME->value;
1480
1481
    /** @deprecated Use SignupError::BAD_PASSWORD->value instead */
1482
    public const ERR_SIGNUP_BADPASS = SignupError::BAD_PASSWORD->value;
1483
1484
    /** @deprecated Use SignupError::BAD_EMAIL->value instead */
1485
    public const ERR_SIGNUP_BADEMAIL = SignupError::BAD_EMAIL->value;
1486
1487
    /** @deprecated Use SignupError::USERNAME_IN_USE->value instead */
1488
    public const ERR_SIGNUP_UNAMEINUSE = SignupError::USERNAME_IN_USE->value;
1489
1490
    /** @deprecated Use SignupError::EMAIL_IN_USE->value instead */
1491
    public const ERR_SIGNUP_EMAILINUSE = SignupError::EMAIL_IN_USE->value;
1492
1493
    /** @deprecated Use SignupError::BAD_INVITE_CODE->value instead */
1494
    public const ERR_SIGNUP_BADINVITECODE = SignupError::BAD_INVITE_CODE->value;
1495
1496
    /** @deprecated Use SignupError::SUCCESS->value instead */
1497
    public const SUCCESS = SignupError::SUCCESS->value;
1498
1499
    /** @deprecated Use UserRole::USER->value instead */
1500
    public const ROLE_USER = UserRole::USER->value;
1501
1502
    /** @deprecated Use UserRole::ADMIN->value instead */
1503
    public const ROLE_ADMIN = UserRole::ADMIN->value;
1504
1505
    /** @deprecated Use UserRole::DISABLED->value instead */
1506
    public const ROLE_DISABLED = UserRole::DISABLED->value;
1507
1508
    /** @deprecated Use UserRole::MODERATOR->value instead */
1509
    public const ROLE_MODERATOR = UserRole::MODERATOR->value;
1510
1511
    /** @deprecated Use QueueType::NONE->value instead */
1512
    public const QUEUE_NONE = QueueType::NONE->value;
1513
1514
    /** @deprecated Use QueueType::SABNZBD->value instead */
1515
    public const QUEUE_SABNZBD = QueueType::SABNZBD->value;
1516
1517
    /** @deprecated Use QueueType::NZBGET->value instead */
1518
    public const QUEUE_NZBGET = QueueType::NZBGET->value;
1519
}
1520