288 lines
10 KiB
PHP
288 lines
10 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Frontend;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\Comment;
|
||
use App\Models\CommentLike;
|
||
use App\Models\Setting;
|
||
use App\Services\DeepSeekService;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\Support\Facades\Http;
|
||
|
||
class CommentController extends Controller
|
||
{
|
||
/**
|
||
* POST /comments — yorum gönder
|
||
*/
|
||
public function store(Request $request)
|
||
{
|
||
$user = Auth::user();
|
||
$maxLength = ($user && $user->hasPerk('extended_comments')) ? 1000 : 500;
|
||
|
||
$request->validate([
|
||
'commentable_type' => 'required|in:episode,anime',
|
||
'commentable_id' => 'required|integer',
|
||
'content' => 'nullable|string|max:' . $maxLength,
|
||
'gif_url' => 'nullable|url|max:500',
|
||
'parent_id' => 'nullable|integer|exists:comments,id',
|
||
]);
|
||
|
||
if (empty(trim($request->content ?? '')) && empty($request->gif_url)) {
|
||
return response()->json(['error' => 'Yorum boş olamaz.'], 422);
|
||
}
|
||
|
||
if (!empty($request->gif_url) && (!$user || !$user->hasPerk('comment_gif'))) {
|
||
return response()->json(['error' => 'GIF eklemek için premium üyelik gerekiyor.'], 403);
|
||
}
|
||
|
||
$commentsEnabled = Setting::get('comments_enabled', '1') === '1';
|
||
|
||
if (!$commentsEnabled) {
|
||
return response()->json(['error' => 'Yorumlar şu an kapalı.'], 403);
|
||
}
|
||
|
||
$content = trim($request->content ?? '');
|
||
|
||
// AI moderasyon (sadece metin içeren yorumlar için, GIF yorumları direkt onaylanır)
|
||
$aiService = new DeepSeekService();
|
||
$pendingReason = null;
|
||
$status = 'approved';
|
||
|
||
if (!empty($content) && $aiService->isConfigured()) {
|
||
$mod = $aiService->moderateComment($content);
|
||
|
||
if ($mod['is_rude']) {
|
||
$status = 'pending';
|
||
$pendingReason = 'rude';
|
||
} elseif ($mod['is_spoiler']) {
|
||
$status = 'pending';
|
||
$pendingReason = 'spoiler';
|
||
}
|
||
} elseif (Setting::get('comments_require_approval', '0') === '1') {
|
||
$status = 'pending';
|
||
$pendingReason = 'manual';
|
||
}
|
||
|
||
$comment = Comment::create([
|
||
'user_id' => Auth::id(),
|
||
'commentable_type' => $request->commentable_type,
|
||
'commentable_id' => $request->commentable_id,
|
||
'parent_id' => $request->parent_id ?: null,
|
||
'content' => $content,
|
||
'gif_url' => $request->gif_url ?: null,
|
||
'status' => $status,
|
||
'like_count' => 0,
|
||
]);
|
||
|
||
$comment->load('user');
|
||
|
||
if ($status !== 'approved') {
|
||
return response()->json([
|
||
'ok' => true,
|
||
'pending' => true,
|
||
'pending_reason' => $pendingReason,
|
||
]);
|
||
}
|
||
|
||
return response()->json([
|
||
'ok' => true,
|
||
'pending' => false,
|
||
'comment' => $this->formatComment($comment, Auth::id()),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST /comments/{comment}/like — beğen/beğenmekten vazgeç (toggle)
|
||
*/
|
||
public function like(Comment $comment)
|
||
{
|
||
$userId = Auth::id();
|
||
|
||
$existing = CommentLike::where('user_id', $userId)
|
||
->where('comment_id', $comment->id)
|
||
->first();
|
||
|
||
if ($existing) {
|
||
$existing->delete();
|
||
$comment->decrement('like_count');
|
||
$liked = false;
|
||
} else {
|
||
CommentLike::create(['user_id' => $userId, 'comment_id' => $comment->id]);
|
||
$comment->increment('like_count');
|
||
$liked = true;
|
||
}
|
||
|
||
return response()->json([
|
||
'ok' => true,
|
||
'liked' => $liked,
|
||
'like_count' => $comment->fresh()->like_count,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* GET /comments/gif-search — GIF arama (Giphy öncelikli, Tenor fallback)
|
||
*/
|
||
public function gifSearch(Request $request)
|
||
{
|
||
$query = $request->query('q', 'anime reaction');
|
||
$giphyKey = Setting::get('giphy_api_key', '');
|
||
$tenorKey = Setting::get('tenor_api_key', '');
|
||
|
||
// Giphy
|
||
if (!empty($giphyKey)) {
|
||
return $this->searchGiphy($query, $giphyKey);
|
||
}
|
||
|
||
// Tenor
|
||
if (!empty($tenorKey)) {
|
||
return $this->searchTenor($query, $tenorKey);
|
||
}
|
||
|
||
return response()->json(['results' => [], 'error' => 'no_key']);
|
||
}
|
||
|
||
private function searchGiphy(string $query, string $apiKey)
|
||
{
|
||
try {
|
||
$res = Http::timeout(8)->get('https://api.giphy.com/v1/gifs/search', [
|
||
'api_key' => $apiKey,
|
||
'q' => $query,
|
||
'limit' => 24,
|
||
'rating' => 'pg-13',
|
||
'lang' => 'en',
|
||
]);
|
||
|
||
if (!$res->successful()) {
|
||
\Log::warning('Giphy API failed', ['status' => $res->status()]);
|
||
return response()->json(['results' => [], 'error' => 'giphy_fail']);
|
||
}
|
||
|
||
$gifs = collect($res->json('data', []))->map(function ($r) {
|
||
$images = $r['images'] ?? [];
|
||
$preview = $images['fixed_height_small']['url']
|
||
?? $images['fixed_height']['url']
|
||
?? $images['downsized']['url']
|
||
?? null;
|
||
$full = $images['downsized_medium']['url']
|
||
?? $images['fixed_height']['url']
|
||
?? $images['original']['url']
|
||
?? $preview;
|
||
if (!$preview || !$full) return null;
|
||
return [
|
||
'id' => $r['id'],
|
||
'preview' => $preview,
|
||
'url' => $full,
|
||
'title' => $r['title'] ?? '',
|
||
];
|
||
})->filter()->values();
|
||
|
||
return response()->json(['results' => $gifs]);
|
||
} catch (\Exception $e) {
|
||
\Log::error('Giphy error: ' . $e->getMessage());
|
||
return response()->json(['results' => [], 'error' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
private function searchTenor(string $query, string $apiKey)
|
||
{
|
||
try {
|
||
$res = Http::timeout(8)->get('https://tenor.googleapis.com/v2/search', [
|
||
'q' => $query,
|
||
'key' => $apiKey,
|
||
'limit' => 24,
|
||
'media_filter' => 'tinygif,gif',
|
||
'contentfilter' => 'medium',
|
||
]);
|
||
|
||
if (!$res->successful()) {
|
||
return response()->json(['results' => [], 'error' => 'tenor_fail']);
|
||
}
|
||
|
||
$gifs = collect($res->json('results', []))->map(function ($r) {
|
||
$formats = $r['media_formats'] ?? [];
|
||
$preview = $formats['tinygif']['url'] ?? $formats['mediumgif']['url'] ?? $formats['gif']['url'] ?? null;
|
||
$full = $formats['gif']['url'] ?? $formats['mediumgif']['url'] ?? $preview ?? null;
|
||
if (!$preview || !$full) return null;
|
||
return [
|
||
'id' => $r['id'],
|
||
'preview' => $preview,
|
||
'url' => $full,
|
||
'title' => $r['content_description'] ?? '',
|
||
];
|
||
})->filter()->values();
|
||
|
||
return response()->json(['results' => $gifs]);
|
||
} catch (\Exception $e) {
|
||
return response()->json(['results' => [], 'error' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GET /comments — bölüm yorumlarını getir (AJAX sayfalama)
|
||
*/
|
||
public function index(Request $request)
|
||
{
|
||
$userId = Auth::id();
|
||
|
||
$query = Comment::where('commentable_type', $request->type)
|
||
->where('commentable_id', $request->id)
|
||
->whereNull('parent_id')
|
||
->where('status', 'approved')
|
||
->with(['user', 'replies' => fn($q) => $q->where('status', 'approved')->with('user')->orderBy('created_at')])
|
||
->orderByDesc('is_pinned')
|
||
->orderByDesc('like_count')
|
||
->orderByDesc('created_at');
|
||
|
||
$comments = $query->paginate(20);
|
||
|
||
return response()->json([
|
||
'data' => $comments->map(fn($c) => $this->formatComment($c, $userId, true)),
|
||
'has_more' => $comments->hasMorePages(),
|
||
'next_page'=> $comments->currentPage() + 1,
|
||
]);
|
||
}
|
||
|
||
private function formatComment(Comment $c, ?int $userId, bool $withReplies = false): array
|
||
{
|
||
$data = [
|
||
'id' => $c->id,
|
||
'content' => $c->content,
|
||
'gif_url' => $c->gif_url,
|
||
'like_count' => $c->like_count,
|
||
'is_liked' => $userId ? $c->likes()->where('user_id', $userId)->exists() : false,
|
||
'is_pinned' => $c->is_pinned,
|
||
'parent_id' => $c->parent_id,
|
||
'created_at' => $c->created_at?->diffForHumans(),
|
||
'user' => $c->user ? [
|
||
'id' => $c->user->id,
|
||
'name' => $c->user->name,
|
||
'username' => $c->user->username,
|
||
'avatar' => $c->user->gif_avatar && $c->user->hasPerk('gif_avatar')
|
||
? $c->user->gif_avatar
|
||
: ($c->user->avatar ? \App\Support\MediaUrl::fromStoragePath($c->user->avatar) : null),
|
||
'role' => $c->user->role,
|
||
'is_following' => $userId && $userId !== $c->user->id
|
||
? \App\Models\UserFollow::where('follower_id', $userId)->where('following_id', $c->user->id)->exists()
|
||
: false,
|
||
'comment_bg' => $c->user->comment_bg,
|
||
'comment_glow' => $c->user->comment_glow,
|
||
'comment_signature' => $c->user->comment_signature,
|
||
'username_color' => $c->user->username_color,
|
||
'username_effect' => $c->user->username_effect,
|
||
'profile_frame' => $c->user->profile_frame,
|
||
'profile_badge' => $c->user->profile_badge,
|
||
'admin_badge' => $c->user->admin_badge,
|
||
'watch_rank' => $c->user->watchRank(),
|
||
] : null,
|
||
];
|
||
|
||
if ($withReplies && $c->relationLoaded('replies')) {
|
||
$data['replies'] = $c->replies->map(fn($r) => $this->formatComment($r, $userId))->toArray();
|
||
}
|
||
|
||
return $data;
|
||
}
|
||
}
|