Implemented simple rss support

This commit is contained in:
Mia Rose Winter 2024-02-03 15:54:16 +01:00
parent d96f946592
commit 03c54249a3
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
4 changed files with 134 additions and 1 deletions

View file

@ -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<Customization> customizations, ApplicationDbContext context) : ControllerBase {
private ApplicationDbContext Context { get; } = context;
private IOptions<Customization> Customizations { get; } = customizations;
[HttpGet("rss.xml", Name = "RssFeed")]
[Produces("application/rss+xml")]
[ResponseCache(Duration = 60*15, Location = ResponseCacheLocation.Any)]
public async Task<IActionResult> 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<IActionResult> GetAtomFeedAsync() {
return Ok(await CreateFeedAll("AtomFeed"));
}
private async Task<SyndicationFeed> CreateFeedAll(string? routeName) {
var query = Context.Set<Article>()
.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<Article> 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" }
}
};
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -26,6 +26,7 @@
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="System.ServiceModel.Syndication" Version="8.0.0" />
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.5" />
</ItemGroup>