Files
animexe/app/Http/Controllers/Frontend/SocialController.php
T
2026-07-14 00:01:48 +03:00

570 lines
22 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\Http\Controllers\Frontend;
use App\Http\Controllers\Controller;
use App\Models\Anime;
use App\Models\Episode;
use App\Models\EpisodePrediction;
use App\Models\EpisodeTimestampComment;
use App\Models\FirstWatchSession;
use App\Models\PredictionVote;
use App\Models\SpoilerBox;
use App\Models\SpoilerBoxLike;
use App\Models\TimeCapsule;
use App\Models\User;
use App\Models\UserFollow;
use App\Models\WatchParty;
use App\Models\WatchPartyMember;
use App\Services\DeepSeekService;
use App\Support\MediaUrl;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class SocialController extends Controller
{
// ─────────────────────────────────────────────────────────
// Kullanıcı takip
// ─────────────────────────────────────────────────────────
public function followToggle(User $user)
{
$me = Auth::user();
if ($me->id === $user->id) {
return response()->json(['error' => 'Kendinizi takip edemezsiniz.'], 422);
}
$existing = UserFollow::where('follower_id', $me->id)
->where('following_id', $user->id)
->first();
if ($existing) {
$existing->delete();
$following = false;
} else {
UserFollow::create(['follower_id' => $me->id, 'following_id' => $user->id]);
$following = true;
}
return response()->json([
'following' => $following,
'followers_count' => UserFollow::where('following_id', $user->id)->count(),
]);
}
public function card(User $user)
{
$me = Auth::user();
$isFollowing = $me
? UserFollow::where('follower_id', $me->id)->where('following_id', $user->id)->exists()
: false;
return response()->json([
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'avatar' => $user->avatar ? MediaUrl::fromStoragePath($user->avatar) : null,
'followers' => UserFollow::where('following_id', $user->id)->count(),
'following' => UserFollow::where('follower_id', $user->id)->count(),
'is_following' => $isFollowing,
'profile_url' => route('user.profile', $user),
'follow_url' => ($me && $me->id !== $user->id) ? route('user.follow', $user) : null,
'msg_url' => ($me && $me->id !== $user->id) ? route('messages.start', $user) : null,
'is_me' => $me && $me->id === $user->id,
]);
}
public function compatibility(User $user)
{
$me = Auth::user();
if (!$me) return response()->json(['score' => 0]);
return response()->json([
'score' => $me->compatibilityWith($user),
]);
}
// ─────────────────────────────────────────────────────────
// NicoNico — Timestamp Yorumları
// ─────────────────────────────────────────────────────────
public function timestampComments(Episode $episode)
{
$comments = EpisodeTimestampComment::with('user:id,name,username')
->where('episode_id', $episode->id)
->where('is_hidden', false)
->orderBy('timestamp_sec')
->get()
->map(fn($c) => [
'id' => $c->id,
'user_id' => $c->user_id,
'timestamp_sec' => $c->timestamp_sec,
'body' => $c->body,
'color' => $c->color,
'username' => $c->user?->username ?? 'misafir',
'created_at' => $c->created_at,
]);
return response()->json(['comments' => $comments]);
}
public function timestampCommentStore(Request $request, Episode $episode)
{
$data = $request->validate([
'timestamp_sec' => 'required|integer|min:0|max:86400',
'body' => 'required|string|max:100',
'color' => 'nullable|regex:/^#[0-9a-fA-F]{6}$/',
]);
$me = Auth::user();
// Flood koruması: aynı kullanıcı 5 saniye içinde 2+ yorum atmasın
$recent = EpisodeTimestampComment::where('user_id', $me->id)
->where('episode_id', $episode->id)
->where('created_at', '>=', now()->subSeconds(5))
->count();
if ($recent >= 2) {
return response()->json(['error' => 'Çok hızlı yorum yapıyorsunuz.'], 429);
}
$comment = EpisodeTimestampComment::create([
'episode_id' => $episode->id,
'user_id' => $me->id,
'timestamp_sec' => $data['timestamp_sec'],
'body' => $data['body'],
'color' => $data['color'] ?? '#ffffff',
]);
return response()->json(['ok' => true, 'id' => $comment->id]);
}
// ─────────────────────────────────────────────────────────
// Tahmin Oyunu
// ─────────────────────────────────────────────────────────
public function predictions(Episode $episode)
{
$me = Auth::id();
$predictions = EpisodePrediction::with('user:id,name,username')
->where('episode_id', $episode->id)
->orderByDesc('vote_count')
->get()
->map(fn($p) => [
'id' => $p->id,
'body' => $p->body,
'is_correct' => $p->is_correct,
'vote_count' => $p->vote_count,
'username' => $p->user?->username,
'is_mine' => $me && $p->user_id === $me,
'voted' => $me
? PredictionVote::where('prediction_id', $p->id)->where('user_id', $me)->exists()
: false,
'created_at' => $p->created_at->diffForHumans(),
]);
$myPrediction = $me
? EpisodePrediction::where('episode_id', $episode->id)->where('user_id', $me)->first()?->id
: null;
return response()->json([
'predictions' => $predictions,
'my_prediction' => $myPrediction,
]);
}
public function predictionStore(Request $request, Episode $episode)
{
$me = Auth::user();
$data = $request->validate([
'body' => 'required|string|min:5|max:280',
]);
$existing = EpisodePrediction::where('episode_id', $episode->id)
->where('user_id', $me->id)
->first();
if ($existing) {
return response()->json(['error' => 'Bu bölüm için zaten bir tahmininiz var.'], 422);
}
$prediction = EpisodePrediction::create([
'episode_id' => $episode->id,
'user_id' => $me->id,
'body' => $data['body'],
]);
return response()->json(['ok' => true, 'id' => $prediction->id]);
}
public function predictionVote(Request $request, EpisodePrediction $prediction)
{
$me = Auth::user();
$existing = PredictionVote::where('prediction_id', $prediction->id)
->where('user_id', $me->id)
->first();
if ($existing) {
$existing->delete();
$prediction->decrement('vote_count');
return response()->json(['voted' => false, 'vote_count' => $prediction->fresh()->vote_count]);
}
PredictionVote::create(['prediction_id' => $prediction->id, 'user_id' => $me->id]);
$prediction->increment('vote_count');
return response()->json(['voted' => true, 'vote_count' => $prediction->fresh()->vote_count]);
}
// ─────────────────────────────────────────────────────────
// Watch Party
// ─────────────────────────────────────────────────────────
public function partyCreate(Request $request)
{
$me = Auth::user();
$data = $request->validate([
'episode_id' => 'required|exists:episodes,id',
'is_private' => 'boolean',
'password' => 'nullable|string|max:30',
'max_members'=> 'nullable|integer|min:2|max:20',
]);
// Kullanıcının zaten aktif bir odası varsa sil
WatchParty::where('host_user_id', $me->id)->delete();
$party = WatchParty::create([
'room_code' => WatchParty::generateCode(),
'host_user_id' => $me->id,
'episode_id' => $data['episode_id'],
'is_private' => $data['is_private'] ?? false,
'password' => isset($data['password']) ? Hash::make($data['password']) : null,
'max_members' => $data['max_members'] ?? 10,
]);
WatchPartyMember::create([
'party_id' => $party->id,
'user_id' => $me->id,
]);
return response()->json([
'ok' => true,
'room_code' => $party->room_code,
'party_url' => route('watch.party', $party->room_code),
]);
}
public function partyJoin(Request $request, string $roomCode)
{
$party = WatchParty::where('room_code', $roomCode)->firstOrFail();
$me = Auth::user();
// Şifre kontrolü
if ($party->is_private && $party->password) {
$pw = $request->input('password', '');
if (!Hash::check($pw, $party->password)) {
return response()->json(['error' => 'Yanlış şifre.'], 403);
}
}
// Kapasite
$activeCount = $party->activeMembers()->count();
if ($activeCount >= $party->max_members) {
return response()->json(['error' => 'Oda dolu.'], 403);
}
WatchPartyMember::updateOrCreate(
['party_id' => $party->id, 'user_id' => $me->id],
['last_ping' => now()]
);
return response()->json([
'ok' => true,
'current_sec' => $party->current_sec,
'is_playing' => $party->is_playing,
'host_id' => $party->host_user_id,
'members' => $this->partyMemberList($party),
]);
}
public function partySync(Request $request, string $roomCode)
{
$party = WatchParty::where('room_code', $roomCode)->firstOrFail();
$me = Auth::user();
// Sadece host senkron durumu güncelleyebilir
if ($party->host_user_id === $me->id) {
$data = $request->validate([
'current_sec' => 'required|integer|min:0',
'is_playing' => 'required|boolean',
]);
$party->update([
'current_sec' => $data['current_sec'],
'is_playing' => $data['is_playing'],
]);
}
// Herkes ping atar
WatchPartyMember::where('party_id', $party->id)
->where('user_id', $me->id)
->update(['last_ping' => now()]);
return response()->json([
'current_sec' => $party->fresh()->current_sec,
'is_playing' => $party->fresh()->is_playing,
'members' => $this->partyMemberList($party),
]);
}
public function partyLeave(string $roomCode)
{
$party = WatchParty::where('room_code', $roomCode)->firstOrFail();
$me = Auth::user();
WatchPartyMember::where('party_id', $party->id)->where('user_id', $me->id)->delete();
if ($party->host_user_id === $me->id) {
$party->delete();
return response()->json(['ok' => true, 'dissolved' => true]);
}
return response()->json(['ok' => true, 'dissolved' => false]);
}
public function partyShow(string $roomCode)
{
$party = WatchParty::with(['episode.anime', 'host'])->where('room_code', $roomCode)->firstOrFail();
return view('frontend.watch-party', compact('party'));
}
private function partyMemberList(WatchParty $party): array
{
return $party->activeMembers()->with('user:id,name,username')->get()
->map(fn($m) => [
'id' => $m->user_id,
'name' => $m->user?->name,
'username' => $m->user?->username,
'is_host' => $m->user_id === $party->host_user_id,
])->toArray();
}
// ─────────────────────────────────────────────────────────
// İlk Kez İzleyenler
// ─────────────────────────────────────────────────────────
public function firstWatchRegister(Request $request, Episode $episode)
{
$me = Auth::user();
$sessionId = $request->header('X-Session-ID') ?? session()->getId();
FirstWatchSession::updateOrCreate(
[
'episode_id' => $episode->id,
'user_id' => $me?->id,
'session_id' => $me ? null : $sessionId,
],
[
'is_first_time' => (bool)$request->input('is_first_time', true),
'last_seen' => now(),
]
);
$count = FirstWatchSession::where('episode_id', $episode->id)
->where('is_first_time', true)
->where('last_seen', '>=', now()->subMinutes(10))
->count();
return response()->json(['ok' => true, 'first_watch_count' => $count]);
}
public function firstWatchCount(Episode $episode)
{
$count = FirstWatchSession::where('episode_id', $episode->id)
->where('is_first_time', true)
->where('last_seen', '>=', now()->subMinutes(10))
->count();
return response()->json(['count' => $count]);
}
// ─────────────────────────────────────────────────────────
// Ruh Hali Motoru
// ─────────────────────────────────────────────────────────
private static array $moodGenres = [
'sad' => ['Drama', 'Romantizm'],
'funny' => ['Komedi', 'Slice of Life'],
'hype' => ['Aksiyon', 'Shounen', 'Spor'],
'think' => ['Bilim Kurgu', 'Gerilim', 'Supernatural'],
'romance' => ['Romantizm', 'Shoujo'],
'scary' => ['Korku', 'Supernatural', 'Gerilim'],
];
public function moodRecommend(Request $request)
{
$mood = $request->validate(['mood' => 'required|in:sad,funny,hype,think,romance,scary'])['mood'];
$genres = self::$moodGenres[$mood] ?? [];
$animes = Anime::whereHas('genres', fn($q) => $q->whereIn('name', $genres))
->where('is_published', true)
->inRandomOrder()
->limit(6)
->get(['id', 'title', 'cover_image', 'slug', 'rating']);
return response()->json([
'animes' => $animes->map(fn($a) => [
'id' => $a->id,
'title' => $a->title,
'cover' => $a->cover_image ? \App\Support\MediaUrl::fromStoragePath($a->cover_image) : null,
'url' => route('anime.show', $a->slug),
'rating'=> $a->rating,
]),
]);
}
// ─────────────────────────────────────────────────────────
// Zaman Kapsülü
// ─────────────────────────────────────────────────────────
public function capsuleStore(Request $request)
{
$me = Auth::user();
$data = $request->validate([
'anime_id' => 'required|exists:animes,id',
'message' => 'required|string|min:5|max:1000',
'unlock_at' => 'required|date|after:' . now()->addDays(30)->toDateString(),
]);
$data['user_id'] = $me->id;
$capsule = TimeCapsule::create($data);
return response()->json(['ok' => true, 'id' => $capsule->id]);
}
public function capsuleIndex()
{
$capsules = TimeCapsule::with('anime:id,title,slug,cover_image')
->where('user_id', Auth::id())
->orderBy('unlock_at')
->get()
->map(fn($c) => [
'id' => $c->id,
'anime' => $c->anime?->title,
'anime_url' => $c->anime ? route('anime.show', $c->anime->slug) : null,
'cover' => $c->anime?->cover_image ? MediaUrl::fromStoragePath($c->anime->cover_image) : null,
'unlock_at' => $c->unlock_at->format('d.m.Y'),
'unlocked' => $c->isUnlocked(),
'opened' => $c->isOpened(),
'message' => $c->isOpened() || $c->isUnlocked() ? $c->message : null,
'created_at' => $c->created_at->format('d.m.Y'),
]);
return view('frontend.capsules', compact('capsules'));
}
public function capsuleOpen(TimeCapsule $capsule)
{
if ($capsule->user_id !== Auth::id()) {
return response()->json(['error' => 'Yetkisiz.'], 403);
}
if (!$capsule->isUnlocked()) {
return response()->json(['error' => 'Kapsül henüz açılamaz.'], 422);
}
$capsule->update(['opened_at' => now()]);
return response()->json(['ok' => true, 'message' => $capsule->message]);
}
// ─────────────────────────────────────────────────────────
// Spoiler Kilitli Kutu
// ─────────────────────────────────────────────────────────
public function spoilerBoxes(Episode $episode)
{
$me = Auth::id();
$boxes = SpoilerBox::with('user:id,name,username')
->where('episode_id', $episode->id)
->orderByDesc('likes')
->orderByDesc('created_at')
->get()
->map(fn($b) => [
'id' => $b->id,
'body' => $b->body,
'is_spoiler' => $b->is_spoiler,
'spoiler_score' => $b->spoiler_score,
'likes' => $b->likes,
'username' => $b->user?->username,
'name' => $b->user?->name,
'is_mine' => $me && $b->user_id === $me,
'liked' => $me ? SpoilerBoxLike::where('box_id', $b->id)->where('user_id', $me)->exists() : false,
'created_at' => $b->created_at->diffForHumans(),
]);
return response()->json(['boxes' => $boxes]);
}
public function spoilerBoxStore(Request $request, Episode $episode)
{
$me = Auth::user();
$data = $request->validate([
'body' => 'required|string|min:3|max:600',
]);
// AI spoiler tespiti
$isSpoiler = false;
$spoilerScore = 0;
$ai = new DeepSeekService();
if ($ai->isConfigured()) {
$prompt = "Aşağıdaki metin bir anime bölümü hakkında yazılmış. Bu metin spoiler içeriyor mu? "
. "Sadece JSON döndür: {\"is_spoiler\": true/false, \"score\": 0-100}\n\nMetin: " . $data['body'];
try {
$raw = $ai->checkSpoiler($data['body']);
if ($raw) {
$isSpoiler = $raw['is_spoiler'] ?? false;
$spoilerScore = $raw['score'] ?? 0;
}
} catch (\Throwable $e) {}
}
$box = SpoilerBox::create([
'episode_id' => $episode->id,
'user_id' => $me->id,
'body' => $data['body'],
'is_spoiler' => $isSpoiler,
'spoiler_score' => $spoilerScore,
]);
return response()->json([
'ok' => true,
'id' => $box->id,
'is_spoiler' => $isSpoiler,
]);
}
public function spoilerBoxLike(SpoilerBox $box)
{
$me = Auth::id();
$existing = SpoilerBoxLike::where('box_id', $box->id)->where('user_id', $me)->first();
if ($existing) {
$existing->delete();
$box->decrement('likes');
return response()->json(['liked' => false, 'likes' => $box->fresh()->likes]);
}
SpoilerBoxLike::create(['box_id' => $box->id, 'user_id' => $me, 'created_at' => now()]);
$box->increment('likes');
return response()->json(['liked' => true, 'likes' => $box->fresh()->likes]);
}
}