Skip to content

Commit

Permalink
Merge pull request #207 from MartinZikmund/feature/rss-validation
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinZikmund authored Dec 1, 2023
2 parents 9b31470 + 01e33d2 commit d19054b
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 165 deletions.
226 changes: 113 additions & 113 deletions src/Directory.Packages.props

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/web/MZikmund.Web.Core/MZikmund.Web.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.AspNetCore.Rewrite" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="MediatR" />
<PackageReference Include="System.ServiceModel.Syndication" />
<PackageReference Include="WilderMinds.MetaWeblog" />
</ItemGroup>

Expand Down
4 changes: 3 additions & 1 deletion src/web/MZikmund.Web.Core/Syndication/FeedEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ public record FeedEntry
{
public string Id { get; set; } = "";

public DateTimeOffset PubDate { get; set; }
public DateTimeOffset PublishedDate { get; set; }

public DateTimeOffset UpdatedDate { get; set; }

public string Title { get; set; } = "";

Expand Down
89 changes: 61 additions & 28 deletions src/web/MZikmund.Web.Core/Syndication/FeedGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Based on https://github.com/EdiWang/Moonglade/blob/cf5571b0db09c7722b310ca9922cdcd542e93a51/src/Moonglade.Syndication/FeedGenerator.cs

using Edi.SyndicationFeed.ReaderWriter;
using Edi.SyndicationFeed.ReaderWriter.Atom;
using Edi.SyndicationFeed.ReaderWriter.Rss;
using Microsoft.AspNetCore.Http;
using System.ServiceModel.Syndication;
using System;
using System.Text;
using System.Xml;
using EwSyndicationItem = Edi.SyndicationFeed.ReaderWriter.SyndicationItem;
using EwSyndicationLink = Edi.SyndicationFeed.ReaderWriter.SyndicationLink;
using EwSyndicationCategory = Edi.SyndicationFeed.ReaderWriter.SyndicationCategory;
using EwSyndicationPerson = Edi.SyndicationFeed.ReaderWriter.SyndicationPerson;

namespace MZikmund.Web.Core.Syndication;

Expand All @@ -18,7 +22,7 @@ public FeedGenerator(IHttpContextAccessor httpContextAccessor)
HostUrl = baseUrl!;
HeadTitle = "Martin Zikmund";
HeadDescription = "Open-source enthusiast and Microsoft MVP. Passionate speaker, avid climber, and Lego aficionado.";
Copyright = "©2023 Martin Zikmund";
Copyright = ${DateTimeOffset.UtcNow.Year} Martin Zikmund";
Generator = "MZikmund";
GeneratorVersion = "0.1";
TrackBackUrl = baseUrl!;
Expand All @@ -42,28 +46,29 @@ public FeedGenerator(IHttpContextAccessor httpContextAccessor)

public string Language { get; set; }

public async Task<string> GetRssAsync(IEnumerable<FeedEntry> feedEntries)
public async Task<string> GetRssAsync(IEnumerable<FeedEntry> feedEntries, string id)
{
var feed = new SyndicationFeed(HeadTitle, HeadDescription, new Uri("https://mzikmund.dev/rss"), id, DateTimeOffset.UtcNow);
feed.Copyright = new TextSyndicationContent(Copyright);

var items = GetItemCollection(feedEntries);
feed.Items = items;
// TODO: Generate additional properties (image, etc.)
var feed = GetItemCollection(feedEntries);

var sw = new StringWriterWithEncoding(Encoding.UTF8);
await using (var xmlWriter = XmlWriter.Create(sw, new() { Async = true }))
{
var writer = new RssFeedWriter(xmlWriter);

await writer.WriteTitle(HeadTitle);
await writer.WriteDescription(HeadDescription);
await writer.Write(new SyndicationLink(new(TrackBackUrl)));
await writer.WritePubDate(DateTimeOffset.UtcNow);
await writer.WriteCopyright(Copyright);
await writer.WriteGenerator(Generator);
await writer.WriteLanguage(new(Language));
var settings = new XmlWriterSettings
{
Encoding = Encoding.UTF8,
NewLineHandling = NewLineHandling.Entitize,
NewLineOnAttributes = true,
Indent = true
};

foreach (var item in feed)
{
await writer.Write(item);
}
await using (var xmlWriter = XmlWriter.Create(sw, new() { Async = true }))
{
var rssFormatter = new Rss20FeedFormatter(feed, false);
rssFormatter.WriteTo(xmlWriter);

await xmlWriter.FlushAsync();
xmlWriter.Close();
Expand All @@ -75,7 +80,7 @@ public async Task<string> GetRssAsync(IEnumerable<FeedEntry> feedEntries)

public async Task<string> GetAtomAsync(IEnumerable<FeedEntry> feedEntries)
{
var feed = GetItemCollection(feedEntries);
var feed = GetEwItemCollection(feedEntries);

var sw = new StringWriterWithEncoding(Encoding.UTF8);
await using (var xmlWriter = XmlWriter.Create(sw, new() { Async = true }))
Expand All @@ -101,43 +106,71 @@ public async Task<string> GetAtomAsync(IEnumerable<FeedEntry> feedEntries)
return xml;
}

private static List<SyndicationItem> GetItemCollection(IEnumerable<FeedEntry> itemCollection)
private static List<EwSyndicationItem> GetEwItemCollection(IEnumerable<FeedEntry> itemCollection)
{
var synItemCollection = new List<SyndicationItem>();
var synItemCollection = new List<EwSyndicationItem>();
if (null == itemCollection) return synItemCollection;

foreach (var item in itemCollection)
{
// create rss item
var sItem = new SyndicationItem
var sItem = new EwSyndicationItem
{
Id = item.Id,
Title = item.Title,
Description = item.Description,
LastUpdated = item.PubDate.ToUniversalTime(),
Published = item.PubDate.ToUniversalTime()
LastUpdated = item.UpdatedDate.ToUniversalTime(),
Published = item.PublishedDate.ToUniversalTime()
};

sItem.AddLink(new SyndicationLink(new(item.Link)));
sItem.AddLink(new EwSyndicationLink(new(item.Link)));

// add author
if (!string.IsNullOrWhiteSpace(item.Author) && !string.IsNullOrWhiteSpace(item.AuthorEmail))
{
sItem.AddContributor(new SyndicationPerson(item.Author, item.AuthorEmail));
sItem.AddContributor(new EwSyndicationPerson(item.Author, item.AuthorEmail));
}

// add categories
if (item.Categories is { Length: > 0 })
{
foreach (var itemCategory in item.Categories)
{
sItem.AddCategory(new SyndicationCategory(itemCategory));
sItem.AddCategory(new EwSyndicationCategory(itemCategory));
}
}
synItemCollection.Add(sItem);
}
return synItemCollection;
}

private List<SyndicationItem> GetItemCollection(IEnumerable<FeedEntry> itemCollection)
{
var syndicationItems = new List<SyndicationItem>();
if (null == itemCollection)
{
return syndicationItems;
}

foreach (var item in itemCollection)
{
// create rss item
var syndicationItem = new SyndicationItem(item.Title, item.Description, new Uri(item.Link), item.Id, item.UpdatedDate);
syndicationItem.Authors.Add(new SyndicationPerson(item.AuthorEmail, item.Author, HostUrl));

// add categories
if (item.Categories is { Length: > 0 })
{
foreach (var itemCategory in item.Categories)
{
syndicationItem.Categories.Add(new SyndicationCategory(itemCategory));
}
}

syndicationItems.Add(syndicationItem);
}
return syndicationItems;
}
}

public class StringWriterWithEncoding : StringWriter
Expand Down
4 changes: 2 additions & 2 deletions src/web/MZikmund.Web.Core/Syndication/GetRssHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ public GetRssHandler(

public async Task<string?> Handle(GetRssQuery request, CancellationToken ct)
{
var data = await _syndicationDataSource.GetFeedDataAsync(request.CategoryName);
var data = await _syndicationDataSource.GetFeedDataAsync(request.CategoryName, request.TagName);
if (data is null)
{
return null;
}

return await _feedGenerator.GetRssAsync(data);
return await _feedGenerator.GetRssAsync(data, request.CategoryName ?? "Default");
}
}
2 changes: 1 addition & 1 deletion src/web/MZikmund.Web.Core/Syndication/GetRssQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

namespace MZikmund.Web.Core.Syndication;

public record GetRssQuery(string? CategoryName) : IRequest<string?>;
public record GetRssQuery(string? CategoryName, string? TagName) : IRequest<string?>;
2 changes: 1 addition & 1 deletion src/web/MZikmund.Web.Core/Syndication/IFeedGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public interface IFeedGenerator
{
Task<string> GetRssAsync(IEnumerable<FeedEntry> feedEntries);
Task<string> GetRssAsync(IEnumerable<FeedEntry> feedEntries, string id);

Task<string> GetAtomAsync(IEnumerable<FeedEntry> feedEntries);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace MZikmund.Web.Core.Syndication;

public interface ISyndicationDataSource
{
Task<IReadOnlyList<FeedEntry>?> GetFeedDataAsync(string? categoryRouteName = null);
Task<IReadOnlyList<FeedEntry>?> GetFeedDataAsync(string? categoryRouteName = null, string? tagRouteName = null);
}
32 changes: 22 additions & 10 deletions src/web/MZikmund.Web.Core/Syndication/SyndicationDataSource.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Based on https://github.com/EdiWang/Moonglade/blob/cf5571b0db09c7722b310ca9922cdcd542e93a51/src/Moonglade.Syndication/SyndicationDataSource.cs

using Microsoft.AspNetCore.Http;

using Microsoft.Extensions.Configuration;
using MZikmund.Web.Configuration;
using MZikmund.Web.Core.Services;
using MZikmund.Web.Data.Entities;
Expand All @@ -15,37 +13,50 @@ public class SyndicationDataSource : ISyndicationDataSource
{
private readonly string _baseUrl;
private readonly IRepository<CategoryEntity> _categoriesRepository;
private readonly IRepository<TagEntity> _tagsRepository;
private readonly IRepository<PostEntity> _postsRepository;
private readonly IPostContentProcessor _postContentProcessor;
private readonly ISiteConfiguration _siteConfiguration;

public SyndicationDataSource(
IHttpContextAccessor httpContextAccessor,
IRepository<CategoryEntity> categoriesRepository,
IRepository<TagEntity> tagsRepository,
IRepository<PostEntity> postsRepository,
IPostContentProcessor postContentProcessor,
ISiteConfiguration siteConfiguration)
{
_categoriesRepository = categoriesRepository;
_tagsRepository = tagsRepository;
_postsRepository = postsRepository;
_postContentProcessor = postContentProcessor;
_siteConfiguration = siteConfiguration;
var acc = httpContextAccessor;
_baseUrl = $"{acc.HttpContext.Request.Scheme}://{acc.HttpContext.Request.Host}";
}

public async Task<IReadOnlyList<FeedEntry>?> GetFeedDataAsync(string? categoryRouteName = null)
public async Task<IReadOnlyList<FeedEntry>?> GetFeedDataAsync(string? categoryRouteName = null, string? tagRouteName = null)
{
IReadOnlyList<FeedEntry> itemCollection;
if (!string.IsNullOrWhiteSpace(categoryRouteName))
{
var cat = await _categoriesRepository.GetAsync(c => c.RouteName == categoryRouteName);
if (cat is null)
var category = await _categoriesRepository.GetAsync(c => c.RouteName == categoryRouteName);
if (category is null)
{
return null;
}

itemCollection = await GetFeedEntriesAsync(category.Id);
}
else if (!string.IsNullOrWhiteSpace(tagRouteName))
{
var tag = await _tagsRepository.GetAsync(c => c.RouteName == tagRouteName);
if (tag is null)
{
return null;
}

itemCollection = await GetFeedEntriesAsync(cat.Id);
itemCollection = await GetFeedEntriesAsync(null, tag.Id);
}
else
{
Expand All @@ -55,15 +66,16 @@ public SyndicationDataSource(
return itemCollection;
}

private async Task<IReadOnlyList<FeedEntry>> GetFeedEntriesAsync(Guid? categoryId = null)
private async Task<IReadOnlyList<FeedEntry>> GetFeedEntriesAsync(Guid? categoryId = null, Guid? tagId = null)
{
var specification = new ListPostsSpecification(1, 15, categoryId, null);
var specification = new ListPostsSpecification(1, 15, categoryId, tagId);
var posts = await _postsRepository.SelectAsync(specification, post => new FeedEntry
{
Id = post.Id.ToString(),
Title = post.Title,
PubDate = post.PublishedDate ?? DateTimeOffset.UtcNow,
Description = post.Content, // TODO: Do we want full content here?
PublishedDate = post.PublishedDate ?? DateTimeOffset.UtcNow,
UpdatedDate = post.LastModifiedDate ?? DateTimeOffset.UtcNow,
Description = post.Content,
Link = $"{_baseUrl}/blog/{post.RouteName}",
Author = $"{_siteConfiguration.Author.FirstName} {_siteConfiguration.Author.LastName}",
AuthorEmail = _siteConfiguration.Author.Email,
Expand Down
26 changes: 18 additions & 8 deletions src/web/MZikmund.Web/Controllers/SyndicationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,29 @@ public async Task<IActionResult> Opml()
ContentInfo = categoryMap,
BlogUrl = $"{rootUrl}/blog",
RssUrl = $"{rootUrl}/rss",
RssCategoryTemplate = $"{rootUrl}/rss/[catTitle]",
RssCategoryTemplate = $"{rootUrl}/rss/category/[catTitle]",
BlogCategoryTemplate = $"{rootUrl}/blog/categories/[catTitle]"
};

var xml = await _mediator.Send(new GetOpmlQuery(opmlConfig));
return Content(xml, "text/xml");
}

[HttpGet("rss/{categoryName?}")]
public async Task<IActionResult> Rss([MaxLength(64)] string? categoryName = null)
[HttpGet("rss")]
public async Task<IActionResult> Rss() => await GenerateRssResponseAsync(new GetRssQuery(null, null));

[HttpGet("rss/category/{categoryName}")]
public async Task<IActionResult> RssByCategory([MaxLength(64)] string categoryName) =>
await GenerateRssResponseAsync(new GetRssQuery(categoryName, null));

[HttpGet("rss/tag/{tagName}")]
public async Task<IActionResult> RssByTag([MaxLength(64)] string tagName) =>
await GenerateRssResponseAsync(new GetRssQuery(null, tagName));

[HttpGet("atom/{categoryName?}")]
public async Task<IActionResult> Atom([MaxLength(64)] string? categoryName = null)
{
var xml = await _mediator.Send(new GetRssQuery(categoryName));
var xml = await _mediator.Send(new GetAtomQuery(categoryName));
if (string.IsNullOrWhiteSpace(xml))
{
return NotFound();
Expand All @@ -50,15 +61,14 @@ public async Task<IActionResult> Rss([MaxLength(64)] string? categoryName = null
return Content(xml, "text/xml");
}

[HttpGet("atom/{categoryName?}")]
public async Task<IActionResult> Atom([MaxLength(64)] string? categoryName = null)
private async Task<IActionResult> GenerateRssResponseAsync(GetRssQuery query)
{
var xml = await _mediator.Send(new GetAtomQuery(categoryName));
var xml = await _mediator.Send(query);
if (string.IsNullOrWhiteSpace(xml))
{
return NotFound();
}

return Content(xml, "text/xml");
return Content(xml, "application/rss+xml; charset=utf-8");
}
}

0 comments on commit d19054b

Please sign in to comment.