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
.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);
// Get IDs of already subscribed feeds
var subscribedFeedIds = await db.WebFeedSubscriptions
.Where(s => s.AccountId == accountId)
.Select(s => s.FeedId)
.ToListAsync();
var feedsQuery = db.WebFeeds
.Include(f => f.Publisher)
.Where(f => !subscribedFeedIds.Contains(f.Id))
.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))
);
}
// Order by most recently created first
feedsQuery = feedsQuery.OrderByDescending(f => f.CreatedAt);
var totalCount = await feedsQuery.CountAsync();
var feeds = await feedsQuery
.Skip(offset)
.Take(take)
.ToListAsync();
Response.Headers["X-Total"] = totalCount.ToString();
return Ok(feeds);
}
}