using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Nuuru.Server.Auth; using Nuuru.Server.Data; using Nuuru.Server.DTOs.Reaction; using Nuuru.Server.Models; using Nuuru.Server.Services; using System.Security.Claims; namespace Nuuru.Server.Controllers { [ApiController] [Route("api/reactions")] public class ReactionController : ControllerBase { private readonly IReactionService _reactionService; private readonly ApplicationDbContext _context; private readonly ILogger _logger; public ReactionController(IReactionService reactionService, ApplicationDbContext context, ILogger logger) { _reactionService = reactionService; _context = context; _logger = logger; } private static ReactionEntityType? ParseEntityType(string entityType) { return entityType.ToLowerInvariant() switch { "comment" => ReactionEntityType.BooruComment, "post" => ReactionEntityType.BooruPost, "forum-post" => ReactionEntityType.ForumPost, _ => null }; } private Guid? GetUserId() { var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; return Guid.TryParse(userIdClaim, out var userId) ? userId : null; } /// /// Get the list of valid emote names for reactions /// [HttpGet("emotes")] [AllowAnonymous] [ResponseCache(Duration = 3600)] public ActionResult> GetEmotes() { return Ok(_reactionService.GetValidEmotes()); } [HttpGet("{entityType}/{entityId:int}")] public async Task> GetReactions(string entityType, int entityId) { var parsedType = ParseEntityType(entityType); if (parsedType == null) { return BadRequest(new { error = "Invalid entity type. Valid types: comment, post, forum-post" }); } var userId = GetUserId(); var response = await _reactionService.GetReactionsAsync(parsedType.Value, entityId, userId); return Ok(response); } [HttpPost("{entityType}/{entityId:int}")] [Authorize(Permissions.User.React)] public async Task> ToggleReaction( string entityType, int entityId, [FromBody] ReactRequest request) { var parsedType = ParseEntityType(entityType); if (parsedType == null) { return BadRequest(new { error = "Invalid entity type. Valid types: comment, post, forum-post" }); } var userId = GetUserId(); if (userId == null) { return Unauthorized(); } if (string.IsNullOrWhiteSpace(request.EmoteName)) { return BadRequest(new { error = "Emote name is required" }); } if (!_reactionService.IsValidEmote(request.EmoteName)) { return BadRequest(new { error = "Invalid emote name" }); } var response = await _reactionService.ToggleReactionAsync( parsedType.Value, entityId, userId.Value, request.EmoteName); if (response == null) { return BadRequest(new { error = "Failed to toggle reaction" }); } // Set target for audit log string targetId = entityId.ToString(); string targetType = "Post"; string category = "Booru"; if (parsedType == ReactionEntityType.ForumPost) { category = "Forum"; targetType = "ForumPost"; var context = await _context.ForumPosts .Where(p => p.Id == entityId) .Select(p => new { p.ThreadId, CategorySlug = p.Thread.Category.Slug }) .FirstOrDefaultAsync(); if (context != null) { targetId = $"{context.CategorySlug}:{context.ThreadId}:{entityId}"; } } else if (parsedType == ReactionEntityType.BooruComment) { targetType = "Comment"; var postContext = await _context.BooruComments .Where(c => c.Id == entityId) .Select(c => c.PostId) .FirstOrDefaultAsync(); if (postContext != 0) { targetId = $"{postContext}:{entityId}"; } } HttpContext.Items[AuditLog.TargetIdKey] = targetId; HttpContext.Items[AuditLog.TargetTypeKey] = targetType; HttpContext.Items[AuditLog.TargetCategoryKey] = category; HttpContext.Items[AuditLog.ActionKey] = $"Reaction.ToggleReaction({request.EmoteName})"; return Ok(response); } } }