using DysonNetwork.Shared.Proto; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Sphere.WebReader; [ApiController] [Route("/api/feeds")] public class WebFeedPublicController( AppDatabase db, WebFeedService webFeed ) : ControllerBase { /// /// Subscribe to a web feed /// /// The ID of the feed to subscribe to /// Subscription details [HttpPost("{feedId:guid}/subscribe")] [Authorize] public async Task Subscribe(Guid feedId) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); // Check if feed exists var feed = await db.WebFeeds.FindAsync(feedId); if (feed == null) return NotFound("Feed not found"); // Check if already subscribed var existingSubscription = await db.WebFeedSubscriptions .FirstOrDefaultAsync(s => s.FeedId == feedId && s.AccountId == accountId); if (existingSubscription != null) return Ok(existingSubscription); // Create new subscription var subscription = new WebFeedSubscription { FeedId = feedId, AccountId = accountId }; db.WebFeedSubscriptions.Add(subscription); await db.SaveChangesAsync(); return CreatedAtAction(nameof(GetSubscriptionStatus), new { feedId }, subscription); } /// /// Unsubscribe from a web feed /// /// The ID of the feed to unsubscribe from [HttpDelete("{feedId:guid}/subscribe")] [Authorize] public async Task Unsubscribe(Guid feedId) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); var subscription = await db.WebFeedSubscriptions .FirstOrDefaultAsync(s => s.FeedId == feedId && s.AccountId == accountId); if (subscription == null) return NoContent(); db.WebFeedSubscriptions.Remove(subscription); await db.SaveChangesAsync(); return NoContent(); } /// /// Get subscription status for the current user and a specific feed /// /// The ID of the feed to check /// Subscription status [HttpGet("{feedId:guid}/subscription")] [Authorize] public async Task> GetSubscriptionStatus(Guid feedId) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); var subscription = await db.WebFeedSubscriptions .Where(s => s.FeedId == feedId && s.AccountId == accountId) .FirstOrDefaultAsync(); if (subscription is null) return NotFound(); return Ok(subscription); } /// /// List all feeds the current user is subscribed to /// /// List of subscribed feeds [HttpGet("subscribed")] [Authorize] public async Task> GetSubscribedFeeds( [FromQuery] int offset = 0, [FromQuery] int take = 20 ) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); var query = db.WebFeedSubscriptions .Where(s => s.AccountId == accountId) .Include(s => s.Feed) .ThenInclude(f => f.Publisher) .OrderByDescending(s => s.CreatedAt); var totalCount = await query.CountAsync(); var subscriptions = await query .Select(q => q.Feed) .Skip(offset) .Take(take) .ToListAsync(); Response.Headers["X-Total"] = totalCount.ToString(); return Ok(subscriptions); } /// /// Get articles from subscribed feeds /// [HttpGet] [Authorize] public async Task> GetWebFeedArticles( [FromQuery] int offset = 0, [FromQuery] int take = 20 ) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); var subscribedFeedIds = await db.WebFeedSubscriptions .Where(s => s.AccountId == accountId) .Select(s => s.FeedId) .ToListAsync(); var query = db.WebFeeds .Where(f => subscribedFeedIds.Contains(f.Id)) .Include(f => f.Publisher) .OrderByDescending(f => f.CreatedAt); var totalCount = await query.CountAsync(); var feeds = await query .Skip(offset) .Take(take) .ToListAsync(); Response.Headers["X-Total"] = totalCount.ToString(); return Ok(feeds); } /// /// Get feed metadata by ID (public endpoint) /// /// The ID of the feed /// Feed metadata [AllowAnonymous] [HttpGet("{feedId:guid}")] public async Task> GetFeedById(Guid feedId) { var feed = await webFeed.GetFeedAsync(feedId); if (feed == null) return NotFound(); return Ok(feed); } /// /// Get articles from a specific feed (public endpoint) /// /// The ID of the feed /// Number of articles to skip /// Maximum number of articles to return /// List of articles from the feed [AllowAnonymous] [HttpGet("{feedId:guid}/articles")] public async Task> GetFeedArticles( [FromRoute] Guid feedId, [FromQuery] int offset = 0, [FromQuery] int take = 20 ) { // Check if feed exists var feedExists = await db.WebFeeds.AnyAsync(f => f.Id == feedId); if (!feedExists) return NotFound("Feed not found"); var query = db.WebArticles .Where(a => a.FeedId == feedId) .OrderByDescending(a => a.CreatedAt) .Include(a => a.Feed) .ThenInclude(f => f.Publisher); var totalCount = await query.CountAsync(); var articles = await query .Skip(offset) .Take(take) .ToListAsync(); Response.Headers["X-Total"] = totalCount.ToString(); return Ok(articles); } /// /// Explore available web feeds /// [HttpGet("explore")] [Authorize] public async Task> ExploreFeeds( [FromQuery] int offset = 0, [FromQuery] int take = 20, [FromQuery] string? query = null ) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); var feedsQuery = db.WebFeeds .Include(f => f.Publisher) .OrderByDescending(f => f.CreatedAt) .AsQueryable(); // Apply search filter if query is provided if (!string.IsNullOrWhiteSpace(query)) { var searchTerm = $"%{query}%"; feedsQuery = feedsQuery.Where(f => EF.Functions.ILike(f.Title, searchTerm) || (f.Description != null && EF.Functions.ILike(f.Description, searchTerm)) ); } var totalCount = await feedsQuery.CountAsync(); var feeds = await feedsQuery .Skip(offset) .Take(take) .ToListAsync(); Response.Headers["X-Total"] = totalCount.ToString(); return Ok(feeds); } }