using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore; using T120B165_ImgBoard.Data; using T120B165_ImgBoard.Models; using T120B165_ImgBoard.Utils; using File = T120B165_ImgBoard.Models.File; namespace T120B165_ImgBoard.Services; public interface IPostService { Task Create( string title, string description, List tags, User author, string fileName, string fileContentType, long fileSize); Task GetById(int postId, bool includeUnfinished = false); public Task> FindAll(string? query, int pageNumber = 1); Task Delete(Post post); Task Update(Post post); } public record CreatedPost(Post Post, File File); public class PostService(ImgBoardContext context): IPostService { private const int PageSize = 8; public async Task Create( string title, string description, List tags, User author, string fileName, string fileContentType, long fileSize ) { var file = new File { OriginalFileName = fileName, Size = fileSize, ContentType = fileContentType, FilePath = Path.GetTempFileName(), CreatedDate = DateTime.Now, }; await context.Files.AddAsync(file); var entry = new Post { Title = title, Description = description, Author = author, Created = DateTime.Now, Tags = tags, File = file }; await context.Posts.AddAsync(entry); await context.SaveChangesAsync(); return new CreatedPost(entry, file); } public async Task GetById(int postId, bool includeUnfinished = false) { var query = context.Posts.Where(t => t.Id == postId); if (!includeUnfinished) { query = query.Where(t => t.File.FinishedDate != null); } return await query.Include(b => b.Author) .Include(b => b.Tags) .Include(b => b.File) .FirstOrDefaultAsync(); } public async Task> FindAll(string? query, int pageNumber = 1) { var postsQuery = context.Posts .Where(p => p.File.FinishedDate != null); // Apply query filtering if the query string is not null or empty if (!string.IsNullOrWhiteSpace(query)) { // Parse the query string into required and excluded tags var (requiredTags, excludedTags) = ParseTagQuery(query); // Filter for required tags (Post must have ALL of these tags) foreach (var requiredTag in requiredTags) { // Where the post's Tags collection contains ANY tag whose name matches the requiredTag. postsQuery = postsQuery.Where(p => p.Tags.Any(t => t.Name == requiredTag)); } // Filter out excluded tags (Post must NOT have ANY of these tags) foreach (var excludedTag in excludedTags) { // Where the post's Tags collection does NOT contain ANY tag whose name matches the excludedTag. postsQuery = postsQuery.Where(p => p.Tags.All(t => t.Name != excludedTag)); } } var totalCount = await postsQuery.CountAsync(); var items = await postsQuery .Skip((pageNumber - 1) * PageSize) .Take(PageSize) .Include(b => b.Author) .Include(b => b.Tags) .Include(b => b.File) .ToListAsync(); return new PagedList(items, pageNumber, PageSize, totalCount); } private static (List requiredTags, List excludedTags) ParseTagQuery(string query) { var requiredTags = new List(); var excludedTags = new List(); // Regex to find all "+tag" or "-tag" segments var matches = Regex.Matches(query, @"([+-])([\w-]+)"); foreach (Match match in matches) { var type = match.Groups[1].Value; // "+" or "-" var tag = match.Groups[2].Value; // The tag name switch (type) { case "+": requiredTags.Add(tag); break; case "-": excludedTags.Add(tag); break; } } return (requiredTags, excludedTags); } public async Task Delete(Post post) { context.Posts.Remove(post); await context.SaveChangesAsync(); return true; } public async Task Update(Post post) { context.Posts.Update(post); await context.SaveChangesAsync(); return post; } }