Files
animexe/app/Models/User.php
T
2026-07-14 00:01:48 +03:00

265 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name', 'username', 'email', 'password', 'avatar',
'social_provider', 'social_id',
'bio', 'banner_image', 'website', 'twitter', 'instagram', 'discord',
'profile_color', 'show_watchlist', 'show_activity',
'role', 'membership', 'premium_expires_at',
'is_banned', 'ban_reason', 'banned_at',
'gif_avatar', 'comment_bg', 'comment_glow', 'username_color', 'username_effect',
'profile_frame', 'profile_badge', 'admin_badge', 'profile_bg', 'profile_theme',
'profile_music_url', 'comment_signature', 'entry_effect', 'animated_banner',
];
protected $hidden = ['password', 'remember_token'];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'premium_expires_at' => 'datetime',
'banned_at' => 'datetime',
'is_banned' => 'boolean',
'show_watchlist' => 'boolean',
'show_activity' => 'boolean',
'animated_banner' => 'boolean',
'password' => 'hashed',
];
}
/**
* /u/{username} gibi URL'lerde username veya ID ile çözümleme.
* custom_profile_url perki olan kullanıcılar /u/kullanici-adi şeklinde erişilebilir.
*/
public function resolveRouteBinding($value, $field = null): ?self
{
if (is_numeric($value)) {
return static::find($value);
}
return static::where('username', $value)
->whereNotNull('username')
->first();
}
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isModerator(): bool
{
return in_array($this->role, ['admin', 'moderator']);
}
public function moderatorPermissions()
{
return $this->hasMany(ModeratorPermission::class);
}
public function activityLogs()
{
return $this->hasMany(UserActivityLog::class);
}
/** Returns cached permission set for this user. Admins have all permissions. */
public function can_mod(string $permission): bool
{
if ($this->isAdmin()) return true;
if ($this->role !== 'moderator') return false;
$key = "mod_perms_{$this->id}";
$perms = cache()->remember($key, 300, fn() =>
ModeratorPermission::where('user_id', $this->id)->pluck('permission')->all()
);
return in_array($permission, $perms);
}
/** Flush cached permissions (call after saving changes). */
public function flushPermCache(): void
{
cache()->forget("mod_perms_{$this->id}");
}
/** isPremium() sonucunu istek başına önbellekle — sayfa başına onlarca kez çağrılıyor */
private ?bool $_isPremiumCache = null;
public function isPremium(): bool
{
if ($this->_isPremiumCache !== null) {
return $this->_isPremiumCache;
}
// 1. yol: membership alanı 'premium' ve süresi dolmamış
$viaMembership = $this->membership === 'premium'
&& ($this->premium_expires_at === null || $this->premium_expires_at->isFuture());
if ($viaMembership) {
return $this->_isPremiumCache = true;
}
// 2. yol: aktif abonelik var ama membership alanı senkronize değil
// (2 farklı premium yolu — biri güncellenmezse kullanıcı yine premium sayılır)
try {
$viaSubscription = $this->subscriptions()
->where('status', 'active')
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
->exists();
} catch (\Throwable) {
$viaSubscription = false;
}
return $this->_isPremiumCache = $viaSubscription;
}
/**
* Kullanıcının aktif planında belirli bir perk var mı?
* Ücretsiz kullanıcılarda her zaman false döner.
*/
public function hasPerk(string $key): bool
{
if (!$this->isPremium()) return false;
// Ücretsiz mod: tüm perkler herkese açık
if (self::freeModeActive()) return true;
$sub = $this->subscriptions()
->where('status', 'active')
->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
->latest()
->with('plan')
->first();
if (!$sub?->plan) return false;
$perks = $sub->plan->perks ?? [];
return !empty($perks[$key]);
}
/** premium_free_mode ayarını 5dk cache'leyerek okur */
public static function freeModeActive(): bool
{
return cache()->remember('premium_free_mode', 300, fn() =>
\App\Models\Setting::get('premium_free_mode', '0')
) === '1';
}
/**
* Kullanıcının aktif avatar URL'si: GIF avatar varsa ve hasPerk('gif_avatar') ise döner.
*/
public function effectiveAvatar(): ?string
{
if ($this->gif_avatar && $this->hasPerk('gif_avatar')) {
return $this->gif_avatar;
}
return $this->avatar;
}
public function subscriptions()
{
return $this->hasMany(Subscription::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
// ── Social ────────────────────────────────────────────────────────────────
public function followers()
{
return $this->belongsToMany(User::class, 'user_follows', 'following_id', 'follower_id')
->withPivot('created_at');
}
public function following()
{
return $this->belongsToMany(User::class, 'user_follows', 'follower_id', 'following_id')
->withPivot('created_at');
}
public function isFollowing(int $userId): bool
{
return \App\Models\UserFollow::where('follower_id', $this->id)
->where('following_id', $userId)
->exists();
}
public function conversations()
{
return $this->belongsToMany(Conversation::class, 'conversation_participants')
->withPivot('last_read_at');
}
public function totalUnreadMessages(): int
{
return $this->conversations()
->with(['messages' => fn($q) => $q->where('user_id', '!=', $this->id)])
->get()
->sum(fn($c) => $c->unreadCountFor($this->id));
}
// Anime zevk uyum skoru (0-100)
public function compatibilityWith(User $other): int
{
$myIds = \App\Models\Watchlist::where('user_id', $this->id)->pluck('anime_id');
$theirIds = \App\Models\Watchlist::where('user_id', $other->id)->pluck('anime_id');
if ($myIds->isEmpty() || $theirIds->isEmpty()) return 0;
$mySet = $myIds->unique()->values();
$theirSet = $theirIds->unique()->values();
$intersection = $mySet->intersect($theirSet)->count();
$union = $mySet->merge($theirSet)->unique()->count();
$jaccard = $union > 0 ? $intersection / $union : 0;
// Rating similarity bonus
$myRatings = \DB::table('anime_ratings')->where('user_id', $this->id)->pluck('rating', 'anime_id');
$theirRatings = \DB::table('anime_ratings')->where('user_id', $other->id)->pluck('rating', 'anime_id');
$commonAnimes = $myRatings->keys()->intersect($theirRatings->keys());
$ratingScore = 0;
if ($commonAnimes->count() > 0) {
$diffs = $commonAnimes->map(fn($id) => abs($myRatings[$id] - $theirRatings[$id]) / 10);
$ratingScore = 1 - $diffs->avg();
}
$score = $jaccard * 0.6 + $ratingScore * 0.4;
return (int) round(min($score * 100, 100));
}
/**
* İzleme saatine göre rank bilgisi döner.
* watch_rank perki yoksa null döner.
*/
public function watchRank(): ?array
{
if (!$this->hasPerk('watch_rank')) return null;
$totalSeconds = \App\Models\ContinueWatching::where('user_id', $this->id)
->sum('seconds_watched');
$hours = $totalSeconds / 3600;
return match(true) {
$hours >= 500 => ['label' => 'Efsane', 'color' => '#ff2d7d', 'icon' => 'bi-trophy-fill'],
$hours >= 250 => ['label' => 'Usta', 'color' => '#ffd700', 'icon' => 'bi-star-fill'],
$hours >= 100 => ['label' => 'Bağımlı', 'color' => '#b84dff', 'icon' => 'bi-heart-fill'],
$hours >= 50 => ['label' => 'Hayran', 'color' => '#00f5ff', 'icon' => 'bi-eye-fill'],
$hours >= 20 => ['label' => 'İzleyici', 'color' => '#00d4a4', 'icon' => 'bi-play-circle-fill'],
default => ['label' => 'Acemi', 'color' => '#9ca3af', 'icon' => 'bi-controller'],
};
}
}