♻️ 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 System.ComponentModel.DataAnnotations;
using DysonNetwork.Sphere.Permission; using DysonNetwork.Sphere.Publisher;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Connection.WebReader; namespace DysonNetwork.Sphere.Connection.WebReader;
[Authorize] [Authorize]
[ApiController] [ApiController]
[Route("/feeds")] [Route("/publishers/{pubName}/feeds")]
public class WebFeedController(WebFeedService webFeedService, AppDatabase database) : ControllerBase 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] var publisher = await ps.GetPublisherByName(pubName);
[MaxLength(8192)] if (publisher is null) return NotFound();
public required string Url { get; set; } var feeds = await webFeed.GetFeedsByPublisherAsync(publisher.Id);
return Ok(feeds);
[Required]
[MaxLength(4096)]
public required string Title { get; set; }
[MaxLength(8192)]
public string? Description { get; set; }
} }
[HttpGet("{id:guid}")]
[HttpPost] public async Task<IActionResult> GetFeed([FromRoute] string pubName, Guid id)
public async Task<IActionResult> CreateWebFeed([FromBody] CreateWebFeedRequest request)
{ {
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); return Ok(feed);
} }
[HttpPost("scrape/{feedId}")] [HttpPost]
[RequiredPermission("maintenance", "web-feeds")] [Authorize]
public async Task<ActionResult> ScrapeFeed(Guid feedId) 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) if (feed == null)
{ {
return NotFound(); return NotFound();
} }
await webFeedService.ScrapeFeedAsync(feed); await webFeed.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);
}
return Ok(); return Ok();
} }
} }

View File

@ -1,9 +1,6 @@
using System.Security.Claims;
using System.ServiceModel.Syndication; using System.ServiceModel.Syndication;
using System.Xml; using System.Xml;
using DysonNetwork.Sphere.Account;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace DysonNetwork.Sphere.Connection.WebReader; namespace DysonNetwork.Sphere.Connection.WebReader;
@ -11,30 +8,17 @@ public class WebFeedService(
AppDatabase database, AppDatabase database,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
ILogger<WebFeedService> logger, ILogger<WebFeedService> logger,
AccountService accountService,
WebReaderService webReaderService WebReaderService webReaderService
) )
{ {
public async Task<WebFeed> CreateWebFeedAsync(WebFeedController.CreateWebFeedRequest request, public async Task<WebFeed> CreateWebFeedAsync(Publisher.Publisher publisher, WebFeedController.WebFeedRequest request)
ClaimsPrincipal claims)
{ {
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 var feed = new WebFeed
{ {
Url = request.Url, Url = request.Url!,
Title = request.Title, Title = request.Title!,
Description = request.Description, Description = request.Description,
PublisherId = account.Id, PublisherId = publisher.Id,
}; };
database.Set<WebFeed>().Add(feed); database.Set<WebFeed>().Add(feed);
@ -43,6 +27,48 @@ public class WebFeedService(
return feed; 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) public async Task ScrapeFeedAsync(WebFeed feed, CancellationToken cancellationToken = default)
{ {
var httpClient = httpClientFactory.CreateClient(); var httpClient = httpClientFactory.CreateClient();