Social interactions with annotations (#4068)

Co-authored-by: Joe Milazzo <josephmajora@gmail.com>
This commit is contained in:
Fesaa
2025-10-04 22:11:06 +02:00
committed by GitHub
parent d4e3a2de3e
commit b40734265b
107 changed files with 7615 additions and 1402 deletions
@@ -35,10 +35,25 @@ public static class AnnotationFilter
return comparison switch
{
FilterComparison.Equal => queryable.Where(a => a.Series.LibraryId == libraryIds[0]),
FilterComparison.Contains => queryable.Where(a => libraryIds.Contains(a.Series.LibraryId)),
FilterComparison.NotContains => queryable.Where(a => !libraryIds.Contains(a.Series.LibraryId)),
FilterComparison.NotEqual => queryable.Where(a => a.Series.LibraryId != libraryIds[0]),
FilterComparison.Equal => queryable.Where(a => a.LibraryId == libraryIds[0]),
FilterComparison.Contains => queryable.Where(a => libraryIds.Contains(a.LibraryId)),
FilterComparison.NotContains => queryable.Where(a => !libraryIds.Contains(a.LibraryId)),
FilterComparison.NotEqual => queryable.Where(a => a.LibraryId != libraryIds[0]),
_ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null),
};
}
public static IQueryable<AppUserAnnotation> HasSeries(this IQueryable<AppUserAnnotation> queryable, bool condition,
FilterComparison comparison, IList<int> seriesIds)
{
if (seriesIds.Count == 0 || !condition) return queryable;
return comparison switch
{
FilterComparison.Equal => queryable.Where(a => a.SeriesId == seriesIds[0]),
FilterComparison.Contains => queryable.Where(a => seriesIds.Contains(a.SeriesId)),
FilterComparison.NotContains => queryable.Where(a => !seriesIds.Contains(a.SeriesId)),
FilterComparison.NotEqual => queryable.Where(a => a.SeriesId != seriesIds[0]),
_ => throw new ArgumentOutOfRangeException(nameof(comparison), comparison, null),
};
}
@@ -50,7 +65,7 @@ public static class AnnotationFilter
return comparison switch
{
FilterComparison.Equal => queryable.Where(a => a.SelectedSlotIndex== highlightSlotIdxs[0]),
FilterComparison.Equal => queryable.Where(a => a.SelectedSlotIndex == highlightSlotIdxs[0]),
FilterComparison.Contains => queryable.Where(a => highlightSlotIdxs.Contains(a.SelectedSlotIndex)),
FilterComparison.NotContains => queryable.Where(a => !highlightSlotIdxs.Contains(a.SelectedSlotIndex)),
FilterComparison.NotEqual => queryable.Where(a => a.SelectedSlotIndex != highlightSlotIdxs[0]),
@@ -14,6 +14,7 @@ using API.Entities;
using API.Entities.Enums;
using API.Entities.Person;
using API.Entities.Scrobble;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
namespace API.Extensions.QueryExtensions;
@@ -90,6 +91,34 @@ public static class QueryableExtensions
.Select(lib => lib.Id);
}
/// <summary>
/// Returns all library ids for a user
/// </summary>
/// <param name="userId"></param>
/// <param name="libraryId">0 for no library filter</param>
/// <param name="queryContext">Defaults to None - The context behind this query, so appropriate restrictions can be placed</param>
/// <returns></returns>
public static IQueryable<int> GetLibraryIdsForUser(this DbSet<AppUser> query, int userId, int libraryId = 0, QueryContext queryContext = QueryContext.None)
{
var user = query
.AsSplitQuery()
.AsNoTracking()
.Where(u => u.Id == userId)
.AsSingleQuery();
if (libraryId == 0)
{
return user.SelectMany(l => l.Libraries)
.IsRestricted(queryContext)
.Select(lib => lib.Id);
}
return user.SelectMany(l => l.Libraries)
.Where(lib => lib.Id == libraryId)
.IsRestricted(queryContext)
.Select(lib => lib.Id);
}
/// <summary>
/// Returns all libraries for a given user and library type
/// </summary>
@@ -346,31 +375,9 @@ public static class QueryableExtensions
};
}
public static IQueryable<FullAnnotationDto> SelectFullAnnotation(this IQueryable<AppUserAnnotation> query)
public static IQueryable<FullAnnotationDto> OrderFullAnnotation(this IQueryable<FullAnnotationDto> query)
{
return query.Select(a => new FullAnnotationDto
{
Id = a.Id,
UserId = a.AppUserId,
SelectedText = a.SelectedText,
Comment = a.Comment,
CommentHtml = a.CommentHtml,
CommentPlainText = a.CommentPlainText,
Context = a.Context,
ChapterTitle = a.ChapterTitle,
PageNumber = a.PageNumber,
SelectedSlotIndex = a.SelectedSlotIndex,
ContainsSpoiler = a.ContainsSpoiler,
CreatedUtc = a.CreatedUtc,
LastModifiedUtc = a.LastModifiedUtc,
LibraryId = a.LibraryId,
LibraryName = a.Chapter.Volume.Series.Library.Name,
SeriesId = a.SeriesId,
SeriesName = a.Chapter.Volume.Series.Name,
VolumeId = a.VolumeId,
VolumeName = a.Chapter.Volume.Name,
ChapterId = a.ChapterId,
})
return query
.OrderBy(a => a.SeriesId)
.ThenBy(a => a.VolumeId)
.ThenBy(a => a.ChapterId)
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using API.Data.Misc;
using API.Entities;
@@ -151,4 +152,199 @@ public static class RestrictByAgeExtensions
return q;
}
private static IQueryable<AppUserRating> RestrictAgainstAgeRestriction(this IQueryable<AppUserRating> queryable, AgeRestriction restriction, int userId)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
var q = queryable.Where(r => r.Series.Metadata.AgeRating <= restriction.AgeRating || r.AppUserId == userId);
if (!restriction.IncludeUnknowns)
{
return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId);
}
return q;
}
private static IQueryable<AppUserChapterRating> RestrictAgainstAgeRestriction(this IQueryable<AppUserChapterRating> queryable, AgeRestriction restriction, int userId)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
var q = queryable.Where(r => r.Series.Metadata.AgeRating <= restriction.AgeRating || r.AppUserId == userId);
if (!restriction.IncludeUnknowns)
{
return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId);
}
return q;
}
private static IQueryable<AppUserAnnotation> RestrictAgainstAgeRestriction(this IQueryable<AppUserAnnotation> queryable, AgeRestriction restriction, int userId)
{
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
var q = queryable.Where(a => a.Series.Metadata.AgeRating <= restriction.AgeRating || a.AppUserId == userId);
if (!restriction.IncludeUnknowns)
{
return q.Where(a => a.Series.Metadata.AgeRating != AgeRating.Unknown || a.AppUserId == userId);
}
return q;
}
// TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here
/// <summary>
/// Filter annotations by social preferences of users
/// </summary>
/// <param name="queryable"></param>
/// <param name="userId"></param>
/// <param name="userPreferences">List of user preferences for every user on the server</param>
/// <returns></returns>
public static IQueryable<AppUserAnnotation> RestrictBySocialPreferences(this IQueryable<AppUserAnnotation> queryable, int userId, IList<AppUserPreferences> userPreferences)
{
var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences);
var socialPreferences = preferencesById[userId];
if (socialPreferences.ViewOtherAnnotations)
{
// We are unable to do dictionary lookups in Sqlite; This means we need to translate them to X IN Y.
var sharingUserIds = userPreferences
.Where(p => p.SocialPreferences.ShareAnnotations)
.Select(p => p.AppUserId)
.ToHashSet();
// Only include the users' annotations, or those of users that are sharing
queryable = queryable.Where(a => a.AppUserId == userId || sharingUserIds.Contains(a.AppUserId));
// For other users' annotation
foreach (var sharingUserId in sharingUserIds.Where(id => id != userId))
{
// Filter out libs if enabled
var libs = preferencesById[sharingUserId].SocialLibraries;
if (libs.Count > 0)
{
queryable = queryable.Where(a => a.AppUserId != sharingUserId || libs.Contains(a.LibraryId));
}
// Filter on age rating
var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating;
var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns;
if (ageRating != AgeRating.NotApplicable)
{
queryable = queryable.Where(a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating <= ageRating)
.WhereIf(!includeUnknowns,
a => a.AppUserId != sharingUserId || a.Series.Metadata.AgeRating != AgeRating.Unknown);
}
}
}
else
{
queryable = queryable.Where(a => a.AppUserId == userId);
}
return queryable
.WhereIf(socialPreferences.SocialLibraries.Count > 0,
a => a.AppUserId == userId || socialPreferences.SocialLibraries.Contains(a.LibraryId))
.RestrictAgainstAgeRestriction(new AgeRestriction
{
AgeRating = socialPreferences.SocialMaxAgeRating,
IncludeUnknowns = socialPreferences.SocialIncludeUnknowns,
}, userId);
}
// TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here
/// <summary>
/// Filter user reviews social preferences of users
/// </summary>
/// <param name="queryable"></param>
/// <param name="userId"></param>
/// <param name="userPreferences">List of user preferences for every user on the server</param>
/// <returns></returns>
public static IQueryable<AppUserRating> RestrictBySocialPreferences(this IQueryable<AppUserRating> queryable, int userId, IList<AppUserPreferences> userPreferences)
{
var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences);
var socialPreferences = preferencesById[userId];
var sharingUserIds = userPreferences
.Where(p => p.SocialPreferences.ShareReviews)
.Select(p => p.AppUserId)
.ToHashSet();
queryable = queryable.Where(r => r.AppUserId == userId || sharingUserIds.Contains(r.AppUserId));
foreach (var sharingUserId in sharingUserIds.Where(id => id != userId))
{
var libs = preferencesById[sharingUserId].SocialLibraries;
if (libs.Count > 0)
{
queryable = queryable.Where(r => r.AppUserId != sharingUserId || libs.Contains(r.Series.LibraryId));
}
var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating;
var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns;
if (ageRating != AgeRating.NotApplicable)
{
queryable = queryable.Where(r => r.AppUserId != sharingUserId || r.Series.Metadata.AgeRating <= ageRating)
.WhereIf(!includeUnknowns,
r => r.AppUserId != sharingUserId || r.Series.Metadata.AgeRating != AgeRating.Unknown);
}
}
return queryable
.WhereIf(socialPreferences.SocialLibraries.Count > 0,
r => r.AppUserId == userId || socialPreferences.SocialLibraries.Contains(r.Series.LibraryId))
.RestrictAgainstAgeRestriction(new AgeRestriction
{
AgeRating = socialPreferences.SocialMaxAgeRating,
IncludeUnknowns = socialPreferences.SocialIncludeUnknowns,
}, userId);
}
// TODO: After updating to .net 10, leverage new Complex Data type queries to inline all db operations here
/// <summary>
/// Filter user chapter reviews social preferences of users
/// </summary>
/// <param name="queryable"></param>
/// <param name="userId"></param>
/// <param name="userPreferences">List of user preferences for every user on the server</param>
/// <returns></returns>
public static IQueryable<AppUserChapterRating> RestrictBySocialPreferences(this IQueryable<AppUserChapterRating> queryable, int userId, IList<AppUserPreferences> userPreferences)
{
var preferencesById = userPreferences.ToDictionary(p => p.AppUserId, p => p.SocialPreferences);
var socialPreferences = preferencesById[userId];
var sharingUserIds = userPreferences
.Where(p => p.SocialPreferences.ShareReviews)
.Select(p => p.AppUserId)
.ToHashSet();
queryable = queryable.Where(r => r.AppUserId == userId || sharingUserIds.Contains(r.AppUserId));
foreach (var sharingUserId in sharingUserIds.Where(id => id != userId))
{
var libs = preferencesById[sharingUserId].SocialLibraries;
if (libs.Count > 0)
{
queryable = queryable.Where(r => r.AppUserId != sharingUserId || libs.Contains(r.Series.LibraryId));
}
var ageRating = preferencesById[sharingUserId].SocialMaxAgeRating;
var includeUnknowns = preferencesById[sharingUserId].SocialIncludeUnknowns;
if (ageRating != AgeRating.NotApplicable)
{
queryable = queryable.Where(r => r.AppUserId != sharingUserId || r.Series.Metadata.AgeRating <= ageRating)
.WhereIf(!includeUnknowns,
r => r.AppUserId != sharingUserId || r.Series.Metadata.AgeRating != AgeRating.Unknown);
}
}
return queryable
.WhereIf(socialPreferences.SocialLibraries.Count > 0,
r => r.AppUserId == userId || socialPreferences.SocialLibraries.Contains(r.Series.LibraryId))
.RestrictAgainstAgeRestriction(new AgeRestriction
{
AgeRating = socialPreferences.SocialMaxAgeRating,
IncludeUnknowns = socialPreferences.SocialIncludeUnknowns,
}, userId);
}
}