From 03c54249a3036920b8039c596402a4e8a3cdb20a Mon Sep 17 00:00:00 2001 From: Mia Winter Date: Sat, 3 Feb 2024 15:54:16 +0100 Subject: [PATCH] Implemented simple rss support --- Wave/Controllers/RssController.cs | 83 ++++++++++++++++++++++ Wave/Program.cs | 5 +- Wave/Utilities/SyndicationFeedFormatter.cs | 46 ++++++++++++ Wave/Wave.csproj | 1 + 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 Wave/Controllers/RssController.cs create mode 100644 Wave/Utilities/SyndicationFeedFormatter.cs diff --git a/Wave/Controllers/RssController.cs b/Wave/Controllers/RssController.cs new file mode 100644 index 0000000..ea29069 --- /dev/null +++ b/Wave/Controllers/RssController.cs @@ -0,0 +1,83 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.ServiceModel.Syndication; +using System.Xml; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Wave.Data; +using Wave.Data.Migrations.postgres; + +namespace Wave.Controllers; + + +[ApiController] +[Route("/[controller]")] +public class RssController(IOptions customizations, ApplicationDbContext context) : ControllerBase { + private ApplicationDbContext Context { get; } = context; + private IOptions Customizations { get; } = customizations; + + [HttpGet("rss.xml", Name = "RssFeed")] + [Produces("application/rss+xml")] + [ResponseCache(Duration = 60*15, Location = ResponseCacheLocation.Any)] + public async Task GetRssFeedAsync() { + return Ok(await CreateFeedAll("RssFeed")); + } + [HttpGet("atom.xml", Name = "AtomFeed")] + [Produces("application/atom+xml")] + [ResponseCache(Duration = 60*15, Location = ResponseCacheLocation.Any)] + public async Task GetAtomFeedAsync() { + return Ok(await CreateFeedAll("AtomFeed")); + } + + private async Task CreateFeedAll(string? routeName) { + var query = Context.Set
() + .Include(a => a.Author). + Include(a => a.Categories) + .OrderByDescending(a => a.PublishDate) + .Take(15); + + var articles = await query.ToListAsync(); + var date = query.Max(a => a.PublishDate); + + return CreateFeedAsync(articles, date, routeName); + } + + private SyndicationFeed CreateFeedAsync(IEnumerable
articles, DateTimeOffset date, string? routeName) { + string appName = Customizations.Value.AppName; + var host = new Uri($"{Request.Scheme}://{Request.Host}{Request.PathBase}", UriKind.Absolute); + var link = new Uri(Url.RouteUrl(routeName, null, Request.Scheme, host.Host) ?? host.AbsoluteUri); + + return new SyndicationFeed(appName, "Feed on " + appName, link, articles + .Select(article => { + var item = new SyndicationItem( + article.Title, + new TextSyndicationContent(article.BodyHtml, TextSyndicationContentKind.Html), + new Uri(host, + $"/{article.PublishDate.Year}/{article.PublishDate.Month:D2}/{article.PublishDate.Day:D2}/{Uri.EscapeDataString(article.Title.ToLowerInvariant()).Replace("-", "+").Replace("%20", "-")}"), + new Uri(host, "article/" + article.Id).AbsoluteUri, + article.PublishDate) { + Authors = { + new SyndicationPerson { Name = article.Author.FullName } + }, + LastUpdatedTime = article.LastModified ?? article.PublishDate, + PublishDate = article.PublishDate + }; + + foreach (var category in article.Categories.OrderBy(c => c.Color)) { + item.Categories.Add(new SyndicationCategory(category.Name)); + } + return item; + }) + .ToList()) { + TimeToLive = TimeSpan.FromMinutes(15), + LastUpdatedTime = date, + Generator = "Wave", + Links = { + new SyndicationLink(link) { RelationshipType = "self" } + } + }; + } +} \ No newline at end of file diff --git a/Wave/Program.cs b/Wave/Program.cs index 5e69144..17d483f 100644 --- a/Wave/Program.cs +++ b/Wave/Program.cs @@ -15,6 +15,7 @@ using Wave.Components.Account; using Wave.Data; using Wave.Services; +using Wave.Utilities; var builder = WebApplication.CreateBuilder(args); builder.Configuration @@ -26,7 +27,9 @@ .AddEnvironmentVariables("WAVE_"); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); -builder.Services.AddControllers(); +builder.Services.AddControllers(options => { + options.OutputFormatters.Add(new SyndicationFeedFormatter()); +}); #region Data Protection & Redis diff --git a/Wave/Utilities/SyndicationFeedFormatter.cs b/Wave/Utilities/SyndicationFeedFormatter.cs new file mode 100644 index 0000000..b3ea1bf --- /dev/null +++ b/Wave/Utilities/SyndicationFeedFormatter.cs @@ -0,0 +1,46 @@ +using System.ServiceModel.Syndication; +using System.Text; +using System.Xml; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; + +namespace Wave.Utilities; + +public class SyndicationFeedFormatter : TextOutputFormatter { + + public SyndicationFeedFormatter() { + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/rss+xml")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/atom+xml")); + + SupportedEncodings.Add(Encoding.UTF8); + } + + protected override bool CanWriteType(Type? type) + => typeof(SyndicationFeed).IsAssignableFrom(type); + + public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { + var httpContext = context.HttpContext; + httpContext.Response.Headers.ContentDisposition = "inline"; + + var feed = context.Object as SyndicationFeed; + + await using var stringWriter = new StringWriter(); + await using var rssWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { + Async = true + }); + System.ServiceModel.Syndication.SyndicationFeedFormatter formatter; + if (context.ContentType.Value?.StartsWith("application/rss+xml") is true) { + formatter = new Rss20FeedFormatter(feed) { + SerializeExtensionsAsAtom = false + }; + } else if (context.ContentType.Value?.StartsWith("application/atom+xml") is true) { + formatter = new Atom10FeedFormatter(feed); + } else { + throw new FormatException($"The format {context.ContentType.Value} is not supported."); + } + formatter.WriteTo(rssWriter); + rssWriter.Close(); + + await httpContext.Response.WriteAsync(stringWriter.ToString(), selectedEncoding); + } +} \ No newline at end of file diff --git a/Wave/Wave.csproj b/Wave/Wave.csproj index 487349d..024b1cd 100644 --- a/Wave/Wave.csproj +++ b/Wave/Wave.csproj @@ -26,6 +26,7 @@ +