✨ Web articles and feed
This commit is contained in:
		
							
								
								
									
										39
									
								
								DysonNetwork.Sphere/Connection/WebReader/WebArticle.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								DysonNetwork.Sphere/Connection/WebReader/WebArticle.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Connection.WebReader; | ||||
|  | ||||
| public class WebArticle : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|  | ||||
|     [MaxLength(4096)] public string Title { get; set; } | ||||
|     [MaxLength(8192)] public string Url { get; set; } | ||||
|     [MaxLength(4096)] public string? Author { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public LinkEmbed? Preview { get; set; } | ||||
|  | ||||
|     // ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength | ||||
|     public string? Content { get; set; } | ||||
|  | ||||
|     public DateTime? PublishedAt { get; set; } | ||||
|  | ||||
|     public Guid FeedId { get; set; } | ||||
|     public WebFeed Feed { get; set; } = null!; | ||||
| } | ||||
|  | ||||
| public class WebFeed : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(8192)] public string Url { get; set; } | ||||
|     [MaxLength(4096)] public string Title { get; set; } | ||||
|     [MaxLength(8192)] public string? Description { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] public LinkEmbed? Preview { get; set; } | ||||
|  | ||||
|     public Guid PublisherId { get; set; } | ||||
|     public Publisher.Publisher Publisher { get; set; } = null!; | ||||
|  | ||||
|     public ICollection<WebArticle> Articles { get; set; } = new List<WebArticle>(); | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Connection.WebReader; | ||||
|  | ||||
| [Authorize] | ||||
| [ApiController] | ||||
| [Route("feeds")] | ||||
| public class WebFeedController(WebFeedService webFeedService) : ControllerBase | ||||
| { | ||||
|     public class CreateWebFeedRequest | ||||
|     { | ||||
|         [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; } | ||||
|     } | ||||
|  | ||||
|      | ||||
|     [HttpPost] | ||||
|     public async Task<IActionResult> CreateWebFeed([FromBody] CreateWebFeedRequest request) | ||||
|     { | ||||
|         var feed = await webFeedService.CreateWebFeedAsync(request, User); | ||||
|         return Ok(feed); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,35 @@ | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Quartz; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Connection.WebReader; | ||||
|  | ||||
| [DisallowConcurrentExecution] | ||||
| public class WebFeedScraperJob( | ||||
|     AppDatabase database, | ||||
|     WebFeedService webFeedService, | ||||
|     ILogger<WebFeedScraperJob> logger | ||||
| ) | ||||
|     : IJob | ||||
| { | ||||
|     public async Task Execute(IJobExecutionContext context) | ||||
|     { | ||||
|         logger.LogInformation("Starting web feed scraper job."); | ||||
|  | ||||
|         var feeds = await database.Set<WebFeed>().ToListAsync(context.CancellationToken); | ||||
|  | ||||
|         foreach (var feed in feeds) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 await webFeedService.ScrapeFeedAsync(feed, context.CancellationToken); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Failed to scrape web feed {FeedId}", feed.Id); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         logger.LogInformation("Web feed scraper job finished."); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										91
									
								
								DysonNetwork.Sphere/Connection/WebReader/WebFeedService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								DysonNetwork.Sphere/Connection/WebReader/WebFeedService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| 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; | ||||
|  | ||||
| public class WebFeedService( | ||||
|     AppDatabase database, | ||||
|     IHttpClientFactory httpClientFactory, | ||||
|     ILogger<WebFeedService> logger, | ||||
|     AccountService accountService | ||||
| ) | ||||
| { | ||||
|     public async Task<WebFeed> CreateWebFeedAsync(WebFeedController.CreateWebFeedRequest dto, 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 | ||||
|         { | ||||
|             Url = dto.Url, | ||||
|             Title = dto.Title, | ||||
|             Description = dto.Description, | ||||
|             PublisherId = account.Id, | ||||
|         }; | ||||
|  | ||||
|         database.Set<WebFeed>().Add(feed); | ||||
|         await database.SaveChangesAsync(); | ||||
|  | ||||
|         return feed; | ||||
|     } | ||||
|  | ||||
|     public async Task ScrapeFeedAsync(WebFeed feed, CancellationToken cancellationToken = default) | ||||
|     { | ||||
|         var httpClient = httpClientFactory.CreateClient(); | ||||
|         var response = await httpClient.GetAsync(feed.Url, cancellationToken); | ||||
|         response.EnsureSuccessStatusCode(); | ||||
|  | ||||
|         await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); | ||||
|         using var reader = XmlReader.Create(stream); | ||||
|         var syndicationFeed = SyndicationFeed.Load(reader); | ||||
|  | ||||
|         if (syndicationFeed == null) | ||||
|         { | ||||
|             logger.LogWarning("Could not parse syndication feed for {FeedUrl}", feed.Url); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         foreach (var item in syndicationFeed.Items) | ||||
|         { | ||||
|             var itemUrl = item.Links.FirstOrDefault()?.Uri.ToString(); | ||||
|             if (string.IsNullOrEmpty(itemUrl)) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var articleExists = await database.Set<WebArticle>() | ||||
|                 .AnyAsync(a => a.FeedId == feed.Id && a.Url == itemUrl, cancellationToken); | ||||
|  | ||||
|             if (articleExists) | ||||
|             { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var newArticle = new WebArticle | ||||
|             { | ||||
|                 FeedId = feed.Id, | ||||
|                 Title = item.Title.Text, | ||||
|                 Url = itemUrl, | ||||
|                 Author = item.Authors.FirstOrDefault()?.Name, | ||||
|                 Content = (item.Content as TextSyndicationContent)?.Text ?? item.Summary.Text, | ||||
|                 PublishedAt = item.PublishDate.UtcDateTime, | ||||
|             }; | ||||
|  | ||||
|             database.Set<WebArticle>().Add(newArticle); | ||||
|         } | ||||
|  | ||||
|         await database.SaveChangesAsync(cancellationToken); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user