using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; using Hangfire; using Kavita.API.Database; using Kavita.API.Repositories; using Kavita.API.Services.Plus; using Kavita.Models.Builders; using Kavita.Models.Constants; using Kavita.Models.DTOs.SeriesDetail; using Kavita.Server.Attributes; using Microsoft.AspNetCore.Mvc; namespace Kavita.Server.Controllers; public class ReviewController( IUnitOfWork unitOfWork, IMapper mapper, IScrobblingService scrobblingService) : BaseApiController { /// /// Updates the user's review for a given series /// /// /// [HttpPost("series")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateSeriesReview(UpdateUserReviewDto dto) { var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); if (user == null) return Unauthorized(); if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, dto.SeriesId)) return NotFound(); var ratingBuilder = new RatingBuilder(await unitOfWork.UserRepository.GetUserRatingAsync(dto.SeriesId, user.Id)); var rating = ratingBuilder .WithBody(dto.Body) .WithSeriesId(dto.SeriesId) .Build(); if (rating.Id == 0) { user.Ratings.Add(rating); } unitOfWork.UserRepository.Update(user); await unitOfWork.CommitAsync(); BackgroundJob.Enqueue(() => scrobblingService.ScrobbleReviewUpdate(user.Id, dto.SeriesId, string.Empty, dto.Body)); return Ok(mapper.Map(rating)); } /// /// Update the user's review for a given chapter /// /// chapterId must be set /// [HttpPost("chapter")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task> UpdateChapterReview(UpdateUserReviewDto dto) { var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); if (user == null) return Unauthorized(); if (dto.ChapterId == null) return BadRequest(); if (!await unitOfWork.UserRepository.HasAccessToSeries(UserId, dto.SeriesId)) return NotFound(); var chapterId = dto.ChapterId.Value; var ratingBuilder = new ChapterRatingBuilder(await unitOfWork.UserRepository.GetUserChapterRatingAsync(user.Id, chapterId)); var rating = ratingBuilder .WithBody(dto.Body) .WithSeriesId(dto.SeriesId) .WithChapterId(chapterId) .Build(); if (rating.Id == 0) { user.ChapterRatings.Add(rating); } unitOfWork.UserRepository.Update(user); await unitOfWork.CommitAsync(); return Ok(mapper.Map(rating)); } /// /// Deletes the user's review for the given series /// /// [HttpDelete("series")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteSeriesReview([FromQuery] int seriesId) { var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.Ratings); if (user == null) return Unauthorized(); user.Ratings = user.Ratings.Where(r => r.SeriesId != seriesId).ToList(); unitOfWork.UserRepository.Update(user); await unitOfWork.CommitAsync(); return Ok(); } /// /// Deletes the user's review for the given chapter /// /// [HttpDelete("chapter")] [DisallowRole(PolicyConstants.ReadOnlyRole)] public async Task DeleteChapterReview([FromQuery] int chapterId) { var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId, AppUserIncludes.ChapterRatings); if (user == null) return Unauthorized(); user.ChapterRatings = user.ChapterRatings.Where(r => r.ChapterId != chapterId).ToList(); unitOfWork.UserRepository.Update(user); await unitOfWork.CommitAsync(); return Ok(); } /// /// Returns all reviews for the user. If you are authenticated as the user, will always return data, regardless of ShareReviews setting /// /// User to load, if your own, will bypass RBS and ShareReviews restrictions /// Null to ignore filtering. >= rating /// Null to ignore filtering on Series name /// [HttpGet("all")] public async Task>> GetAllReviewsForUser(int userId, float? rating = null, string? filterQuery = null) { return Ok(await unitOfWork.UserRepository.GetAllReviewsForUser(userId, UserId, filterQuery, rating)); } }