Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
<?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'],
};
}
}