using Microsoft.Extensions.Caching.Memory; using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.Models.Booru; namespace Nuuru.Server.Services.Search; public static class SearchDefaults { public const string DefaultQuery = "-764 -764_(cult) -bbw -child_sexual_abuse_material -cum -daisys_destruction -diaper -dox -femboy -femdom -femquote -fetish -flag:minor_attracted_person -furfag -gangrape -gore -mass_shooter -mass_shooting -meta:flagspam -meta:pornographic_content -nsfw -o9a -order_of_nine_angles -pedophilia -pee -penis -peter_scully -porn -pussy -rape -school_shooter -school_shooting -self_harm -sex -sexy {-shroom ~ shroomcuck} {-soyjak_forum ~ shroomcuck} {-subnas:shrooma ~ shroomcuck} -total_nigger_death -vagina -zoophile -subvariant:shoyta -subvariant:soylita -meta:traced_from_literal_fetish_art -rating:explicit -rating:questionable"; } public interface IDefaultQueryFilterService { Task> ApplyDefaultFiltersAsync(IQueryable query); Task IsPostVisibleAsync(int postId); Task> GetVisiblePostIdsAsync(IEnumerable postIds); } public class DefaultQueryFilterService : IDefaultQueryFilterService { private readonly ApplicationDbContext _context; private readonly ICurrentUserContext _userContext; private readonly IUserSettingsService _userSettingsService; private readonly IMemoryCache _cache; private readonly ILogger _logger; // Per-request cache: avoids re-fetching + re-parsing when called multiple times per scope private string? _cachedQueryString; private SearchParseResult? _cachedParseResult; private bool _queryResolved; public DefaultQueryFilterService( ApplicationDbContext context, ICurrentUserContext userContext, IUserSettingsService userSettingsService, IMemoryCache cache, ILogger logger) { _context = context; _userContext = userContext; _userSettingsService = userSettingsService; _cache = cache; _logger = logger; } public async Task> ApplyDefaultFiltersAsync(IQueryable query) { var parseResult = await GetParseResultAsync(); var builder = new SearchQueryBuilder(_context, _userContext, _cache); // Always apply visibility filters (trash and approval) based on user context. // Even if there's no default tag query, we still want to filter out trashed/pending posts. query = builder.ApplyVisibilityFilters(query, parseResult); if (parseResult != null) { query = builder.ApplyFilterNodes(query, parseResult); } return query; } public async Task IsPostVisibleAsync(int postId) { var query = await ApplyDefaultFiltersAsync( _context.BooruPosts .AsNoTracking() .Where(p => p.Id == postId)); return await query.AnyAsync(); } public async Task> GetVisiblePostIdsAsync(IEnumerable postIds) { var ids = postIds .Distinct() .ToList(); if (ids.Count == 0) { return []; } var query = await ApplyDefaultFiltersAsync( _context.BooruPosts .AsNoTracking() .Where(p => ids.Contains(p.Id))); var visibleIds = await query .Select(p => p.Id) .ToListAsync(); return visibleIds.ToHashSet(); } private async Task GetParseResultAsync() { // Return cached result if we've already resolved this request if (_queryResolved) return _cachedParseResult; _queryResolved = true; string? defaultQuery; if (_userContext.UserId.HasValue) { defaultQuery = await _userSettingsService.GetDefaultSearchQueryAsync(_userContext.UserId.Value); } else { defaultQuery = SearchDefaults.DefaultQuery; } if (string.IsNullOrWhiteSpace(defaultQuery)) return null; _cachedQueryString = defaultQuery; try { // Use IMemoryCache to avoid re-tokenizing/parsing the same default query string across requests var cacheKey = $"default_filter_parse:{defaultQuery}"; if (!_cache.TryGetValue(cacheKey, out SearchParseResult? parseResult)) { var tokenizer = new SearchTokenizer(defaultQuery); var tokens = tokenizer.Tokenize(); var parser = new SearchParser(tokens); parseResult = parser.Parse(); if (parseResult.Errors.Count == 0) { _cache.Set(cacheKey, parseResult, TimeSpan.FromMinutes(10)); } } if (parseResult!.Errors.Count > 0) { _logger.LogWarning("Failed to parse default search query '{Query}': {Errors}", defaultQuery, string.Join(", ", parseResult.Errors)); return null; } _cachedParseResult = parseResult; return parseResult; } catch (Exception ex) { _logger.LogWarning(ex, "Error applying default search query '{Query}'", defaultQuery); return null; } } }