♻️ Refined web feed APIs

This commit is contained in:
LittleSheep 2025-06-29 22:02:26 +08:00
parent c4ea15097e
commit ca5be5a01c
2 changed files with 145 additions and 58 deletions

View File

@ -1,62 +1,123 @@
using System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Publisher;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Connection.WebReader;
[Authorize]
[ApiController]
[Route("/feeds")]
public class WebFeedController(WebFeedService webFeedService, AppDatabase database) : ControllerBase
[Route("/publishers/{pubName}/feeds")]
public class WebFeedController(WebFeedService webFeed, PublisherService ps) : ControllerBase
{
public class CreateWebFeedRequest
public record WebFeedRequest(
[MaxLength(8192)] string? Url,
[MaxLength(4096)] string? Title,
[MaxLength(8192)] string? Description
);
[HttpGet]
public async Task<IActionResult> ListFeeds([FromRoute] string pubName)
{
[Required]
[MaxLength(8192)]
public required string Url { get; set; }
[Required]
[MaxLength(4096)]
public required string Title { get; set; }
[MaxLength(8192)]
public string? Description { get; set; }
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
var feeds = await webFeed.GetFeedsByPublisherAsync(publisher.Id);
return Ok(feeds);
}
[HttpPost]
public async Task<IActionResult> CreateWebFeed([FromBody] CreateWebFeedRequest request)
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetFeed([FromRoute] string pubName, Guid id)
{
var feed = await webFeedService.CreateWebFeedAsync(request, User);
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
if (feed == null)
return NotFound();
return Ok(feed);
}
[HttpPost("scrape/{feedId}")]
[RequiredPermission("maintenance", "web-feeds")]
public async Task<ActionResult> ScrapeFeed(Guid feedId)
[HttpPost]
[Authorize]
public async Task<IActionResult> CreateWebFeed([FromRoute] string pubName, [FromBody] WebFeedRequest request)
{
var feed = await database.Set<WebFeed>().FindAsync(feedId);
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
if (string.IsNullOrWhiteSpace(request.Url) || string.IsNullOrWhiteSpace(request.Title))
return BadRequest("Url and title are required");
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the publisher to create a web feed");
var feed = await webFeed.CreateWebFeedAsync(publisher, request);
return Ok(feed);
}
[HttpPatch("{id:guid}")]
[Authorize]
public async Task<IActionResult> UpdateFeed([FromRoute] string pubName, Guid id, [FromBody] WebFeedRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the publisher to update a web feed");
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
if (feed == null)
return NotFound();
feed = await webFeed.UpdateFeedAsync(feed, request);
return Ok(feed);
}
[HttpDelete("{id:guid}")]
[Authorize]
public async Task<IActionResult> DeleteFeed([FromRoute] string pubName, Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the publisher to delete a web feed");
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
if (feed == null)
return NotFound();
var result = await webFeed.DeleteFeedAsync(id);
if (!result)
return NotFound();
return NoContent();
}
[HttpPost("{id:guid}/scrap")]
[Authorize]
public async Task<ActionResult> Scrap([FromRoute] string pubName, Guid id)
{
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
var publisher = await ps.GetPublisherByName(pubName);
if (publisher is null) return NotFound();
if (!await ps.IsMemberWithRole(publisher.Id, currentUser.Id, PublisherMemberRole.Editor))
return StatusCode(403, "You must be an editor of the publisher to scrape a web feed");
var feed = await webFeed.GetFeedAsync(id, publisherId: publisher.Id);
if (feed == null)
{
return NotFound();
}
await webFeedService.ScrapeFeedAsync(feed);
return Ok();
}
[HttpPost("scrape-all")]
[RequiredPermission("maintenance", "web-feeds")]
public async Task<ActionResult> ScrapeAllFeeds()
{
var feeds = await database.Set<WebFeed>().ToListAsync();
foreach (var feed in feeds)
{
await webFeedService.ScrapeFeedAsync(feed);
}
await webFeed.ScrapeFeedAsync(feed);
return Ok();
}
}

View File

@ -1,9 +1,6 @@
using System.Security.Claims;
using System.ServiceModel.Syndication;
using System.Xml;
using DysonNetwork.Sphere.Account;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace DysonNetwork.Sphere.Connection.WebReader;
@ -11,30 +8,17 @@ public class WebFeedService(
AppDatabase database,
IHttpClientFactory httpClientFactory,
ILogger<WebFeedService> logger,
AccountService accountService,
WebReaderService webReaderService
)
{
public async Task<WebFeed> CreateWebFeedAsync(WebFeedController.CreateWebFeedRequest request,
ClaimsPrincipal claims)
public async Task<WebFeed> CreateWebFeedAsync(Publisher.Publisher publisher, WebFeedController.WebFeedRequest request)
{
if (claims.Identity?.Name == null)
{
throw new UnauthorizedAccessException();
}
var account = await accountService.LookupAccount(claims.Identity.Name);
if (account == null)
{
throw new UnauthorizedAccessException();
}
var feed = new WebFeed
{
Url = request.Url,
Title = request.Title,
Url = request.Url!,
Title = request.Title!,
Description = request.Description,
PublisherId = account.Id,
PublisherId = publisher.Id,
};
database.Set<WebFeed>().Add(feed);
@ -43,6 +27,48 @@ public class WebFeedService(
return feed;
}
public async Task<WebFeed?> GetFeedAsync(Guid id, Guid? publisherId = null)
{
var query = database.WebFeeds.Where(a => a.Id == id).AsQueryable();
if (publisherId.HasValue)
query = query.Where(a => a.PublisherId == publisherId.Value);
return await query.FirstOrDefaultAsync();
}
public async Task<List<WebFeed>> GetFeedsByPublisherAsync(Guid publisherId)
{
return await database.WebFeeds.Where(a => a.PublisherId == publisherId).ToListAsync();
}
public async Task<WebFeed> UpdateFeedAsync(WebFeed feed, WebFeedController.WebFeedRequest request)
{
if (request.Url is not null)
feed.Url = request.Url;
if (request.Title is not null)
feed.Title = request.Title;
if (request.Description is not null)
feed.Description = request.Description;
database.Update(feed);
await database.SaveChangesAsync();
return feed;
}
public async Task<bool> DeleteFeedAsync(Guid id)
{
var feed = await database.WebFeeds.FindAsync(id);
if (feed == null)
{
return false;
}
database.WebFeeds.Remove(feed);
await database.SaveChangesAsync();
return true;
}
public async Task ScrapeFeedAsync(WebFeed feed, CancellationToken cancellationToken = default)
{
var httpClient = httpClientFactory.CreateClient();