Implemented simple rss support
This commit is contained in:
parent
d96f946592
commit
03c54249a3
83
Wave/Controllers/RssController.cs
Normal file
83
Wave/Controllers/RssController.cs
Normal 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" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
using Wave.Components.Account;
|
using Wave.Components.Account;
|
||||||
using Wave.Data;
|
using Wave.Data;
|
||||||
using Wave.Services;
|
using Wave.Services;
|
||||||
|
using Wave.Utilities;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Configuration
|
builder.Configuration
|
||||||
|
@ -26,7 +27,9 @@
|
||||||
.AddEnvironmentVariables("WAVE_");
|
.AddEnvironmentVariables("WAVE_");
|
||||||
|
|
||||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers(options => {
|
||||||
|
options.OutputFormatters.Add(new SyndicationFeedFormatter());
|
||||||
|
});
|
||||||
|
|
||||||
#region Data Protection & Redis
|
#region Data Protection & Redis
|
||||||
|
|
||||||
|
|
46
Wave/Utilities/SyndicationFeedFormatter.cs
Normal file
46
Wave/Utilities/SyndicationFeedFormatter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
|
||||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
|
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.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" />
|
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue