From 5b33e8181ac2e849cd04236d68b11f6a57f28727 Mon Sep 17 00:00:00 2001 From: Clemens Vasters Date: Wed, 28 Dec 2022 17:57:06 +0100 Subject: [PATCH] Add CloudEvents support #671 --- .../ConfigFile/Interfaces/ISiteConfig.cs | 12 +- .../DasBlog.Services/ConfigFile/SiteConfig.cs | 8 +- source/DasBlog.Services/IDasBlogSettings.cs | 1 + source/DasBlog.Services/Rss/Rss.cs | 5 +- .../UnitTests/DasBlogSettingTest.cs | 5 + .../DasBlog.Tests/UnitTests/SiteConfigTest.cs | 5 +- .../DasBlog.Web.Repositories/BlogManager.cs | 119 +++++- .../DasBlog.Managers.csproj | 4 +- .../EntryCloudEventData.cs | 34 ++ .../Interfaces/ISubscriptionManager.cs | 3 +- .../SubscriptionManager.cs | 352 ++++++++++-------- .../Config/IISUrlRewrite.Development.config | 2 +- .../Config/site.Development.config | 15 +- source/DasBlog.Web.UI/Config/site.config | 15 + .../Controllers/FeedController.cs | 17 +- .../Settings/DasBlogSettings.cs | 11 +- .../CloudEventsTarget.cs | 143 +++++++ .../newtelligence.DasBlog.Runtime.csproj | 2 +- 18 files changed, 574 insertions(+), 179 deletions(-) create mode 100644 source/DasBlog.Web.Repositories/EntryCloudEventData.cs create mode 100644 source/newtelligence.DasBlog.Runtime/CloudEventsTarget.cs diff --git a/source/DasBlog.Services/ConfigFile/Interfaces/ISiteConfig.cs b/source/DasBlog.Services/ConfigFile/Interfaces/ISiteConfig.cs index 713ce45e..ed983044 100644 --- a/source/DasBlog.Services/ConfigFile/Interfaces/ISiteConfig.cs +++ b/source/DasBlog.Services/ConfigFile/Interfaces/ISiteConfig.cs @@ -130,7 +130,9 @@ public interface ISiteConfig bool EnableCrossposts { get; set; } - bool UseUserCulture { get; set; } + bool EnableCloudEvents { get; set; } + + bool UseUserCulture { get; set; } bool ShowItemSummaryInAggregatedViews { get; set; } @@ -199,7 +201,13 @@ public interface ISiteConfig [XmlArray("CrosspostSites")] CrosspostSite[] CrosspostSiteArray { get; set; } - bool Pop3DeleteAllMessages { get; set; } + [XmlIgnore] + CloudEventsTargetCollection CloudEventsTargets { get; set; } + + [XmlArray("CloudEventsTargets")] + CloudEventsTarget[] CloudEventsTargetArray { get; set; } + + bool Pop3DeleteAllMessages { get; set; } bool Pop3LogIgnoredEmails { get; set; } diff --git a/source/DasBlog.Services/ConfigFile/SiteConfig.cs b/source/DasBlog.Services/ConfigFile/SiteConfig.cs index 413247e4..89282fab 100644 --- a/source/DasBlog.Services/ConfigFile/SiteConfig.cs +++ b/source/DasBlog.Services/ConfigFile/SiteConfig.cs @@ -144,8 +144,10 @@ public string Root { public ContentFilterCollection ContentFilters { get; set; } public ContentFilter[] ContentFilterArray { get; set; } public CrosspostSiteCollection CrosspostSites { get; set; } - public CrosspostSite[] CrosspostSiteArray { get; set; } - public bool Pop3DeleteAllMessages { get; set; } + public CloudEventsTargetCollection CloudEventsTargets { get; set; } + public CrosspostSite[] CrosspostSiteArray { get; set; } + public CloudEventsTarget[] CloudEventsTargetArray { get; set; } + public bool Pop3DeleteAllMessages { get; set; } public bool Pop3LogIgnoredEmails { get; set; } public bool EnableReferralUrlBlackList { get; set; } public string ReferralUrlBlackList { get; set; } @@ -242,6 +244,6 @@ public string Root { public string MastodonServerUrl { get; set; } public string MastodonAccount { get; set; } - + public bool EnableCloudEvents { get; set; } } } diff --git a/source/DasBlog.Services/IDasBlogSettings.cs b/source/DasBlog.Services/IDasBlogSettings.cs index 3e6436ca..71c29c9d 100644 --- a/source/DasBlog.Services/IDasBlogSettings.cs +++ b/source/DasBlog.Services/IDasBlogSettings.cs @@ -49,5 +49,6 @@ public interface IDasBlogSettings SendMailInfo GetMailInfo(MailMessage emailmessage); DateTime GetDisplayTime(DateTime datetime); DateTime GetCreateTime(DateTime datetime); + string GetRssEntryUrl(string entryId); } } diff --git a/source/DasBlog.Services/Rss/Rss.cs b/source/DasBlog.Services/Rss/Rss.cs index abd3699e..b9e6d5e1 100644 --- a/source/DasBlog.Services/Rss/Rss.cs +++ b/source/DasBlog.Services/Rss/Rss.cs @@ -125,7 +125,10 @@ public class RssItem [XmlAttribute("xml:lang")] public string Language; - [XmlElement("author")] + [XmlAttribute("id")] + public string Id; + + [XmlElement("author")] public string Author { get; set; } [XmlElement("title")] diff --git a/source/DasBlog.Tests/UnitTests/DasBlogSettingTest.cs b/source/DasBlog.Tests/UnitTests/DasBlogSettingTest.cs index 9b5e391b..a77098f6 100644 --- a/source/DasBlog.Tests/UnitTests/DasBlogSettingTest.cs +++ b/source/DasBlog.Tests/UnitTests/DasBlogSettingTest.cs @@ -272,5 +272,10 @@ public DateTime GetCreateTime(DateTime datetime) { throw new NotImplementedException(); } + + public string GetRssEntryUrl(string entryId) + { + throw new NotImplementedException(); + } } } diff --git a/source/DasBlog.Tests/UnitTests/SiteConfigTest.cs b/source/DasBlog.Tests/UnitTests/SiteConfigTest.cs index 5ac656ad..763d0c14 100644 --- a/source/DasBlog.Tests/UnitTests/SiteConfigTest.cs +++ b/source/DasBlog.Tests/UnitTests/SiteConfigTest.cs @@ -171,5 +171,8 @@ public class SiteConfigTest : ISiteConfig public string MastodonServerUrl { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public string MastodonAccount { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public bool AllowMarkdownInComments { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - } + public bool EnableCloudEvents { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public CloudEventsTargetCollection CloudEventsTargets { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public CloudEventsTarget[] CloudEventsTargetArray { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + } } diff --git a/source/DasBlog.Web.Repositories/BlogManager.cs b/source/DasBlog.Web.Repositories/BlogManager.cs index bdb0a977..7ef0d566 100644 --- a/source/DasBlog.Web.Repositories/BlogManager.cs +++ b/source/DasBlog.Web.Repositories/BlogManager.cs @@ -20,6 +20,11 @@ using System.Net.Mail; using System.Net; using System.IO; +using CloudNative.CloudEvents; +using System.Xml.Linq; +using CloudNative.CloudEvents.Http; +using CloudNative.CloudEvents.SystemTextJson; +using System.Net.Http; namespace DasBlog.Managers { @@ -187,22 +192,134 @@ public EntrySaveState CreateEntry(Entry entry) { var rtn = InternalSaveEntry(entry, null, null); LogEvent(EventCodes.EntryAdded, entry); + RaisePostCreatedCloudEvent(entry); return rtn; } + + private void RaisePostCreatedCloudEvent(Entry entry) + { + var ext = CloudEventAttribute.CreateExtension("tags", CloudEventAttributeType.String); + var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V1_0, new[] { ext }) + { + Type = "dasblog.post.created", + Source = new Uri(dasBlogSettings.GetBaseUrl()), + Subject = entry.Link, + Data = MapEntryToCloudEventData(entry), + Id = Guid.NewGuid().ToString(), + Time = DateTime.UtcNow, + }; + cloudEvent.SetAttributeFromString("tags", entry.Categories); + RaiseCloudEvent(cloudEvent); + + } + + private void RaiseCloudEvent(CloudEvent cloudEvent) + { + if ( dasBlogSettings.SiteConfiguration.EnableCloudEvents && + dasBlogSettings.SiteConfiguration.CloudEventsTargets != null) + { + foreach (var target in dasBlogSettings.SiteConfiguration.CloudEventsTargets) + { + if (!string.IsNullOrEmpty(target.Uri)) + { + try + { + var content = cloudEvent.ToHttpContent(ContentMode.Structured, new JsonEventFormatter()); + var uriBuilder = new UriBuilder(target.Uri); + if (target.Headers != null) + { + foreach (var header in target.Headers) + { + if (!string.IsNullOrEmpty(header.Name)) + { + content.Headers.Add(header.Name, header.Value); + } + } + } + if (target.QueryArgs!= null) + { + foreach (var queryArgs in target.QueryArgs) + { + uriBuilder.Query = (string.IsNullOrEmpty(uriBuilder.Query) ? string.Empty : uriBuilder.Query + "&") + queryArgs.Name + "=" + queryArgs.Value; + } + } + var httpClient = new HttpClient(); + var result = httpClient.PostAsync(uriBuilder.Uri, content).GetAwaiter().GetResult(); + } + catch(Exception ex) + { + logger.LogError(ex, "Failed to post CloudEvent"); + } + } + } + } + } + + private EntryCloudEventData MapEntryToCloudEventData(Entry entry) + { + var data = new EntryCloudEventData(); + data.Id = entry.EntryId; + data.Title = entry.Title; + data.CreatedUtc = entry.CreatedUtc; + data.ModifiedUtc = entry.ModifiedUtc; + data.Tags = entry.Categories; + data.Description = entry.Description; + data.PermaLink = entry.Link; + data.DetailsLink = dasBlogSettings.GetRssEntryUrl(entry.EntryId); + data.IsPublic = entry.IsPublic; + data.Author = entry.Author; + data.Longitude = entry.Longitude; + data.Latitude = entry.Latitude; + return data; + } + public EntrySaveState UpdateEntry(Entry entry) { var rtn = InternalSaveEntry(entry, null, null); LogEvent(EventCodes.EntryChanged, entry); + RaisePostUpdatedCloudEvent(entry); return rtn; } + private void RaisePostUpdatedCloudEvent(Entry entry) + { + var ext = CloudEventAttribute.CreateExtension("tags", CloudEventAttributeType.String); + var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V1_0, new[] { ext }) + { + Type = "dasblog.post.updated", + Source = new Uri(dasBlogSettings.GetBaseUrl()), + Subject = entry.Link, + Data = MapEntryToCloudEventData(entry), + Id = Guid.NewGuid().ToString(), + Time = DateTime.UtcNow, + }; + cloudEvent.SetAttributeFromString("tags", entry.Categories); + RaiseCloudEvent(cloudEvent); + } + public void DeleteEntry(string postid) { var entry = GetEntryForEdit(postid); dataService.DeleteEntry(postid, null); - LogEvent(EventCodes.EntryDeleted, entry); + RaisePostDeletedCloudEvent(entry); + } + + private void RaisePostDeletedCloudEvent(Entry entry) + { + var ext = CloudEventAttribute.CreateExtension("tags", CloudEventAttributeType.String); + var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V1_0, new[] { ext }) + { + Type = "dasblog.post.deleted", + Source = new Uri(dasBlogSettings.GetBaseUrl()), + Subject = entry.Link, + Id = Guid.NewGuid().ToString(), + Time = DateTime.UtcNow, + Data = MapEntryToCloudEventData(entry) + }; + cloudEvent.SetAttributeFromString("tags", entry.Categories); + RaiseCloudEvent(cloudEvent); } private static StringCollection GetSearchWords(string searchString) diff --git a/source/DasBlog.Web.Repositories/DasBlog.Managers.csproj b/source/DasBlog.Web.Repositories/DasBlog.Managers.csproj index 3b6387a5..f05132f2 100644 --- a/source/DasBlog.Web.Repositories/DasBlog.Managers.csproj +++ b/source/DasBlog.Web.Repositories/DasBlog.Managers.csproj @@ -10,8 +10,10 @@ snupkg + + - + diff --git a/source/DasBlog.Web.Repositories/EntryCloudEventData.cs b/source/DasBlog.Web.Repositories/EntryCloudEventData.cs new file mode 100644 index 00000000..45504c01 --- /dev/null +++ b/source/DasBlog.Web.Repositories/EntryCloudEventData.cs @@ -0,0 +1,34 @@ +using System; +using System.Text.Json.Serialization; + +namespace DasBlog.Managers +{ + internal class EntryCloudEventData + { + [JsonPropertyName("id")] + public string Id { get; internal set; } + [JsonPropertyName("title")] + public string Title { get; internal set; } + [JsonPropertyName("createdUtc")] + public DateTime CreatedUtc { get; internal set; } + [JsonPropertyName("modifiedUtc")] + public DateTime ModifiedUtc { get; internal set; } + [JsonPropertyName("tags")] + public string Tags { get; internal set; } + [JsonPropertyName("description")] + public string Description { get; internal set; } + [JsonPropertyName("permaLink")] + public string PermaLink { get; internal set; } + [JsonPropertyName("contentLink")] + public string DetailsLink { get; internal set; } + [JsonPropertyName("isPublic")] + public bool IsPublic { get; internal set; } + [JsonPropertyName("author")] + public string Author { get; internal set; } + [JsonPropertyName("longitude")] + public double? Longitude { get; internal set; } + [JsonPropertyName("latitude")] + public double? Latitude { get; internal set; } + + } +} diff --git a/source/DasBlog.Web.Repositories/Interfaces/ISubscriptionManager.cs b/source/DasBlog.Web.Repositories/Interfaces/ISubscriptionManager.cs index d65e557d..bca20a0b 100644 --- a/source/DasBlog.Web.Repositories/Interfaces/ISubscriptionManager.cs +++ b/source/DasBlog.Web.Repositories/Interfaces/ISubscriptionManager.cs @@ -10,5 +10,6 @@ public interface ISubscriptionManager RssRoot GetAtom(); RssRoot GetAtomCategory(string categoryName); RsdRoot GetRsd(); - } + RssItem GetRssItem(string entryId); + } } diff --git a/source/DasBlog.Web.Repositories/SubscriptionManager.cs b/source/DasBlog.Web.Repositories/SubscriptionManager.cs index ff8a8999..c0e95534 100644 --- a/source/DasBlog.Web.Repositories/SubscriptionManager.cs +++ b/source/DasBlog.Web.Repositories/SubscriptionManager.cs @@ -32,7 +32,17 @@ public RssRoot GetRss() return GetRssCore(null, dasBlogSettings.SiteConfiguration.RssDayCount, dasBlogSettings.SiteConfiguration.RssMainEntryCount); } - public RssRoot GetRssCategory(string categoryName) + public RssItem GetRssItem(string entryId) + { + Entry entry = dataService.GetEntry(entryId); + if (entry != null) + { + return MapEntryToRssItem(entry); + } + return null; + } + + public RssRoot GetRssCategory(string categoryName) { return GetRssCore(categoryName, 0, 0); } @@ -149,170 +159,188 @@ private RssRoot GetRssCore(string category, int maxDayCount, int maxEntryCount) documentRoot.Channels.Add(ch); foreach (var entry in entries) - { - if (entry.IsPublic == false || entry.Syndicated == false) - { - continue; - } - var doc2 = new XmlDocument(); - var anyElements = new List(); - var item = new RssItem(); - item.Title = entry.Title; - item.Guid = new DasBlog.Services.Rss.Rss20.Guid(); - item.Guid.IsPermaLink = false; - item.Guid.Text = dasBlogSettings.GetPermaLinkUrl(entry.EntryId); - item.Link = dasBlogSettings.RelativeToRoot(dasBlogSettings.GeneratePostUrl(entry)); - User user = dasBlogSettings.GetUserByEmail(entry.Author); - - XmlElement trackbackPing = doc2.CreateElement("trackback", "ping", "http://madskills.com/public/xml/rss/module/trackback/"); - trackbackPing.InnerText = dasBlogSettings.GetTrackbackUrl(entry.EntryId); - anyElements.Add(trackbackPing); - - XmlElement pingbackServer = doc2.CreateElement("pingback", "server", "http://madskills.com/public/xml/rss/module/pingback/"); - pingbackServer.InnerText = dasBlogSettings.PingBackUrl; - anyElements.Add(pingbackServer); - - XmlElement pingbackTarget = doc2.CreateElement("pingback", "target", "http://madskills.com/public/xml/rss/module/pingback/"); - pingbackTarget.InnerText = dasBlogSettings.GetPermaLinkUrl(entry.EntryId); - anyElements.Add(pingbackTarget); - - XmlElement dcCreator = doc2.CreateElement("dc", "creator", "http://purl.org/dc/elements/1.1/"); - if (user != null) - { - dcCreator.InnerText = user.DisplayName; - } - anyElements.Add(dcCreator); - - // Add GeoRSS if it exists. - if (dasBlogSettings.SiteConfiguration.EnableGeoRss) - { - var latitude = new Nullable(); - var longitude = new Nullable(); - - if (entry.Latitude.HasValue) - { - latitude = entry.Latitude; - } - else - { - if (dasBlogSettings.SiteConfiguration.EnableDefaultLatLongForNonGeoCodedPosts) - { - latitude = dasBlogSettings.SiteConfiguration.DefaultLatitude; - } - } - - if (entry.Longitude.HasValue) - { - longitude = entry.Longitude; - } - else - { - if (dasBlogSettings.SiteConfiguration.EnableDefaultLatLongForNonGeoCodedPosts) - { - longitude = dasBlogSettings.SiteConfiguration.DefaultLongitude; - } - } - - if (latitude.HasValue && longitude.HasValue) - { - XmlElement geoLoc = doc2.CreateElement("georss", "point", "http://www.georss.org/georss"); - geoLoc.InnerText = String.Format(CultureInfo.InvariantCulture, "{0:R} {1:R}", latitude, longitude); - anyElements.Add(geoLoc); - } - } - - if (dasBlogSettings.SiteConfiguration.EnableComments) - { - if (entry.AllowComments) - { - XmlElement commentApi = doc2.CreateElement("wfw", "comment", "http://wellformedweb.org/CommentAPI/"); - commentApi.InnerText = dasBlogSettings.GetCommentViewUrl(dasBlogSettings.GeneratePostUrl(entry)); - anyElements.Add(commentApi); - } - - XmlElement commentRss = doc2.CreateElement("wfw", "commentRss", "http://wellformedweb.org/CommentAPI/"); - commentRss.InnerText = dasBlogSettings.GetEntryCommentsRssUrl(entry.EntryId); - anyElements.Add(commentRss); - - //for RSS conformance per FeedValidator.org - int commentsCount = dataService.GetPublicCommentsFor(entry.EntryId).Count; - if (commentsCount > 0) - { - XmlElement slashComments = doc2.CreateElement("slash", "comments", "http://purl.org/rss/1.0/modules/slash/"); - slashComments.InnerText = commentsCount.ToString(); - anyElements.Add(slashComments); - } - item.Comments = dasBlogSettings.GetCommentViewUrl(dasBlogSettings.GeneratePostUrl(entry)); - } - item.Language = entry.Language; - - if (entry.Categories != null && entry.Categories.Length > 0) - { - if (item.Categories == null) item.Categories = new RssCategoryCollection(); - - string[] cats = entry.Categories.Split(';'); - foreach (string c in cats) - { - RssCategory cat = new RssCategory(); - string cleanCat = c.Replace('|', '/'); - cat.Text = cleanCat; - item.Categories.Add(cat); - } - } - if (entry.Attachments.Count > 0) - { - // RSS currently supports only a single enclsoure so we return the first one - item.Enclosure = new Enclosure(); - item.Enclosure.Url = entry.Attachments[0].Name; - item.Enclosure.Type = entry.Attachments[0].Type; - item.Enclosure.Length = entry.Attachments[0].Length.ToString(); - } - item.PubDate = entry.CreatedUtc.ToString("R"); - if (ch.LastBuildDate == null || ch.LastBuildDate.Length == 0) - { - ch.LastBuildDate = item.PubDate; - } - - if (!dasBlogSettings.SiteConfiguration.AlwaysIncludeContentInRSS && - entry.Description != null && - entry.Description.Trim().Length > 0) - { - item.Description = PreprocessItemContent(entry.EntryId, entry.Description); - - } - else - { - if (dasBlogSettings.SiteConfiguration.HtmlTidyContent == false) - { - item.Description = "
" + PreprocessItemContent(entry.EntryId, entry.Content) + "
"; - } - else - { - item.Description = ContentFormatter.FormatContentAsHTML(PreprocessItemContent(entry.EntryId, entry.Content)); + { + if (entry.IsPublic == false || entry.Syndicated == false) + { + continue; + } + var item = MapEntryToRssItem(entry, ch, true); + ch.Items.Add(item); + } + + return documentRoot; + } + private RssItem MapEntryToRssItem(Entry entry, RssChannel ch = null, bool feedView = false) + { + var doc2 = new XmlDocument(); + var anyElements = new List(); + var item = new RssItem(); + item.Id = entry.EntryId; + item.Title = entry.Title; + item.Guid = new DasBlog.Services.Rss.Rss20.Guid(); + item.Guid.IsPermaLink = false; + item.Guid.Text = dasBlogSettings.GetPermaLinkUrl(entry.EntryId); + item.Link = dasBlogSettings.RelativeToRoot(dasBlogSettings.GeneratePostUrl(entry)); + User user = dasBlogSettings.GetUserByEmail(entry.Author); + + if (dasBlogSettings.SiteConfiguration.EnableTrackbackService) + { + XmlElement trackbackPing = doc2.CreateElement("trackback", "ping", "http://madskills.com/public/xml/rss/module/trackback/"); + trackbackPing.InnerText = dasBlogSettings.GetTrackbackUrl(entry.EntryId); + anyElements.Add(trackbackPing); + } + + if (dasBlogSettings.SiteConfiguration.EnablePingbackService) + { + XmlElement pingbackServer = doc2.CreateElement("pingback", "server", "http://madskills.com/public/xml/rss/module/pingback/"); + pingbackServer.InnerText = dasBlogSettings.PingBackUrl; + anyElements.Add(pingbackServer); + + XmlElement pingbackTarget = doc2.CreateElement("pingback", "target", "http://madskills.com/public/xml/rss/module/pingback/"); + pingbackTarget.InnerText = dasBlogSettings.GetPermaLinkUrl(entry.EntryId); + anyElements.Add(pingbackTarget); + } + + XmlElement dcCreator = doc2.CreateElement("dc", "creator", "http://purl.org/dc/elements/1.1/"); + if (user != null) + { + dcCreator.InnerText = user.DisplayName; + } + anyElements.Add(dcCreator); + + // Add GeoRSS if it exists. + if (dasBlogSettings.SiteConfiguration.EnableGeoRss) + { + var latitude = new Nullable(); + var longitude = new Nullable(); + + if (entry.Latitude.HasValue) + { + latitude = entry.Latitude; + } + else + { + if (dasBlogSettings.SiteConfiguration.EnableDefaultLatLongForNonGeoCodedPosts) + { + latitude = dasBlogSettings.SiteConfiguration.DefaultLatitude; + } + } + + if (entry.Longitude.HasValue) + { + longitude = entry.Longitude; + } + else + { + if (dasBlogSettings.SiteConfiguration.EnableDefaultLatLongForNonGeoCodedPosts) + { + longitude = dasBlogSettings.SiteConfiguration.DefaultLongitude; + } + } + + if (latitude.HasValue && longitude.HasValue) + { + XmlElement geoLoc = doc2.CreateElement("georss", "point", "http://www.georss.org/georss"); + geoLoc.InnerText = String.Format(CultureInfo.InvariantCulture, "{0:R} {1:R}", latitude, longitude); + anyElements.Add(geoLoc); + } + } - try - { - string xhtml = ContentFormatter.FormatContentAsXHTML(PreprocessItemContent(entry.EntryId, entry.Content)); - doc2.LoadXml(xhtml); - anyElements.Add((XmlElement)doc2.SelectSingleNode("//*[local-name() = 'body'][namespace-uri()='http://www.w3.org/1999/xhtml']")); - } - catch //(Exception ex) - { - //Debug.Write(ex.ToString()); - // absorb - } - } - } - - item.anyElements = anyElements.ToArray(); - ch.Items.Add(item); - } - - return documentRoot; - } + if (dasBlogSettings.SiteConfiguration.EnableComments) + { + if (entry.AllowComments) + { + XmlElement commentApi = doc2.CreateElement("wfw", "comment", "http://wellformedweb.org/CommentAPI/"); + commentApi.InnerText = dasBlogSettings.GetCommentViewUrl(dasBlogSettings.GeneratePostUrl(entry)); + anyElements.Add(commentApi); + } + + XmlElement commentRss = doc2.CreateElement("wfw", "commentRss", "http://wellformedweb.org/CommentAPI/"); + commentRss.InnerText = dasBlogSettings.GetEntryCommentsRssUrl(entry.EntryId); + anyElements.Add(commentRss); + + //for RSS conformance per FeedValidator.org + int commentsCount = dataService.GetPublicCommentsFor(entry.EntryId).Count; + if (commentsCount > 0) + { + XmlElement slashComments = doc2.CreateElement("slash", "comments", "http://purl.org/rss/1.0/modules/slash/"); + slashComments.InnerText = commentsCount.ToString(); + anyElements.Add(slashComments); + } + item.Comments = dasBlogSettings.GetCommentViewUrl(dasBlogSettings.GeneratePostUrl(entry)); + } + item.Language = entry.Language; + + if (entry.Categories != null && entry.Categories.Length > 0) + { + if (item.Categories == null) item.Categories = new RssCategoryCollection(); + + string[] cats = entry.Categories.Split(';'); + foreach (string c in cats) + { + RssCategory cat = new RssCategory(); + string cleanCat = c.Replace('|', '/'); + cat.Text = cleanCat; + item.Categories.Add(cat); + } + } + if (entry.Attachments.Count > 0) + { + // RSS currently supports only a single enclsoure so we return the first one + item.Enclosure = new Enclosure(); + item.Enclosure.Url = entry.Attachments[0].Name; + item.Enclosure.Type = entry.Attachments[0].Type; + item.Enclosure.Length = entry.Attachments[0].Length.ToString(); + } + item.PubDate = entry.CreatedUtc.ToString("R"); + if (ch != null && (ch.LastBuildDate == null || ch.LastBuildDate.Length == 0)) + { + ch.LastBuildDate = item.PubDate; + } + + if (feedView || !dasBlogSettings.SiteConfiguration.AlwaysIncludeContentInRSS && + entry.Description != null && + entry.Description.Trim().Length > 0) + { + if (feedView) + { + item.Description = PreprocessItemContent(entry.EntryId, entry.Description); + } + else + { + item.Description = entry.Description; + } + } + else + { + var content = (feedView ? PreprocessItemContent(entry.EntryId, entry.Content) : entry.Content); + if (!dasBlogSettings.SiteConfiguration.HtmlTidyContent && feedView) + { + item.Description = "
" + content + "
"; + } + else + { + item.Description = ContentFormatter.FormatContentAsHTML(!string.IsNullOrEmpty(entry.Description)?entry.Description:content); + try + { + string xhtml = ContentFormatter.FormatContentAsXHTML(System.Net.WebUtility.HtmlDecode(content)); + doc2.LoadXml(xhtml); + anyElements.Add((XmlElement)doc2.SelectSingleNode("//*[local-name() = 'body'][namespace-uri()='http://www.w3.org/1999/xhtml']")); + } + catch //(Exception ex) + { + //Debug.Write(ex.ToString()); + // absorb + } + } + } + + item.anyElements = anyElements.ToArray(); + return item; + } - protected EntryCollection BuildEntries(string category, int maxDayCount, int maxEntryCount) + protected EntryCollection BuildEntries(string category, int maxDayCount, int maxEntryCount) { var entryList = new EntryCollection(); diff --git a/source/DasBlog.Web.UI/Config/IISUrlRewrite.Development.config b/source/DasBlog.Web.UI/Config/IISUrlRewrite.Development.config index 13384168..88439f0c 100644 --- a/source/DasBlog.Web.UI/Config/IISUrlRewrite.Development.config +++ b/source/DasBlog.Web.UI/Config/IISUrlRewrite.Development.config @@ -57,7 +57,7 @@ - + diff --git a/source/DasBlog.Web.UI/Config/site.Development.config b/source/DasBlog.Web.UI/Config/site.Development.config index 10184180..2483e547 100644 --- a/source/DasBlog.Web.UI/Config/site.Development.config +++ b/source/DasBlog.Web.UI/Config/site.Development.config @@ -3,7 +3,7 @@ - https://localhost:5001/ + http://localhost:5001/ @@ -139,6 +139,7 @@ false false false + false true false false @@ -159,6 +160,18 @@ + + + eventGrid + https://{myns}.westeurope-1.eventgrid.azure.net/api/events + +
+ aeg-sas-key + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +
+
+
+
true true false diff --git a/source/DasBlog.Web.UI/Config/site.config b/source/DasBlog.Web.UI/Config/site.config index 586e84fb..e8e8a76f 100644 --- a/source/DasBlog.Web.UI/Config/site.config +++ b/source/DasBlog.Web.UI/Config/site.config @@ -138,6 +138,7 @@ false false false + false true false false @@ -158,6 +159,20 @@ + + + + eventGrid + https://{myns}.westeurope-1.eventgrid.azure.net/api/events + +
+ aeg-sas-key + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +
+
+
+
+
true true false diff --git a/source/DasBlog.Web.UI/Controllers/FeedController.cs b/source/DasBlog.Web.UI/Controllers/FeedController.cs index 3041a3b4..66da1754 100644 --- a/source/DasBlog.Web.UI/Controllers/FeedController.cs +++ b/source/DasBlog.Web.UI/Controllers/FeedController.cs @@ -45,7 +45,20 @@ public IActionResult Rss() } [Produces("text/xml")] - [HttpGet("feed/rss/{category}"), HttpHead("feed/rss/{category}")] + [HttpGet("feed/rss/{id}"), HttpHead("feed/rss/{id}")] + public IActionResult RssItem(string id) + { + if (!memoryCache.TryGetValue(CACHEKEY_RSS+id, out RssItem rssItem)) + { + rssItem = subscriptionManager.GetRssItem(id); + + memoryCache.Set(CACHEKEY_RSS+id, rssItem, SiteCacheSettings()); + } + return Ok(rssItem); + } + + [Produces("text/xml")] + [HttpGet("feed/tags/{category}/rss"), HttpHead("feed/tags/{category}/rss")] public IActionResult RssByCategory(string category) { if (!memoryCache.TryGetValue(CACHEKEY_RSS + "_" + category, out RssRoot rss)) @@ -120,7 +133,7 @@ public ActionResult PingBack() return Ok(); } - [HttpGet("feed/rss/comments/{entryid}"), HttpHead("feed/rss/comments/{entryid}")] + [HttpGet("feed/rss/{entryid}/comments"), HttpHead("feed/rss/{entryid}/comments")] public ActionResult RssComments(string entryid) { return Ok(); diff --git a/source/DasBlog.Web.UI/Settings/DasBlogSettings.cs b/source/DasBlog.Web.UI/Settings/DasBlogSettings.cs index 06b6cab8..46896cf0 100644 --- a/source/DasBlog.Web.UI/Settings/DasBlogSettings.cs +++ b/source/DasBlog.Web.UI/Settings/DasBlogSettings.cs @@ -101,7 +101,7 @@ public string GetTrackbackUrl(string entryId) public string GetEntryCommentsRssUrl(string entryId) { - return RelativeToRoot(RssUrl + "/comments/" + entryId); + return RelativeToRoot(RssUrl + $"/{entryId}/comments/"); } public string GetCategoryViewUrl(string category) @@ -116,7 +116,12 @@ public string GetCategoryViewUrlName(string category) public string GetRssCategoryUrl(string category) { - return string.Empty; + return RelativeToRoot($"feed/tags/{category}/rss"); + } + + public string GetRssEntryUrl(string entryId) + { + return RelativeToRoot($"feed/rss/{entryId}"); } public User GetUser(string userName) @@ -307,5 +312,7 @@ public DateTime GetCreateTime(DateTime datetime) return datetime; } + + } } diff --git a/source/newtelligence.DasBlog.Runtime/CloudEventsTarget.cs b/source/newtelligence.DasBlog.Runtime/CloudEventsTarget.cs new file mode 100644 index 00000000..2330b35f --- /dev/null +++ b/source/newtelligence.DasBlog.Runtime/CloudEventsTarget.cs @@ -0,0 +1,143 @@ +#region Copyright (c) 2003, newtelligence AG. All rights reserved. +/* +// Copyright (c) 2003, newtelligence AG. (http://www.newtelligence.com) +// Original BlogX Source Code: Copyright (c) 2003, Chris Anderson (http://simplegeek.com) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted +// provided that the following conditions are met: +// +// (1) Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// (2) Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials +// provided with the distribution. +// (3) Neither the name of the newtelligence AG nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS +// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// ------------------------------------------------------------------------- +// +// Original BlogX source code (c) 2003 by Chris Anderson (http://simplegeek.com) +// +// newtelligence is a registered trademark of newtelligence Aktiengesellschaft. +// +// For portions of this software, the some additional copyright notices may apply +// which can either be found in the license.txt file included in the source distribution +// or following this notice. +// +*/ +#endregion + + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Xml; +using System.Xml.Serialization; + + +namespace newtelligence.DasBlog.Runtime +{ + + public class CloudEventsTargetHeader + { + public string Name { get; set; } + public string Value { get; set; } + } + + [Serializable] + public class CloudEventsTarget + { + string profileName; + string uri; + CloudEventsTargetHeader[] headers; + CloudEventsTargetHeader[] queryArgs; + + public CloudEventsTarget() + { + } + + public CloudEventsTarget(string profileName, string uri, CloudEventsTargetHeader[] headers, CloudEventsTargetHeader[] queryArgs) + { + this.profileName = profileName; + this.uri = uri; + this.headers = headers; + this.queryArgs = queryArgs; + } + + [XmlElement("ProfileName")] + public string ProfileName { get { return profileName; } set { profileName = value; } } + [XmlElement("Uri")] + public string Uri { get { return uri; } set { uri = value; } } + [XmlArray("QueryArgs")] + public CloudEventsTargetHeader[] QueryArgs { get { return queryArgs; } set { queryArgs = value; } } + [XmlArray("Headers")] + public CloudEventsTargetHeader[] Headers { get { return headers; } set { headers = value; } } + + [XmlAnyElement] + public XmlElement[] anyElements; + [XmlAnyAttribute] + public XmlAttribute[] anyAttributes; + } + + /// + /// A collection of elements of type CloudEventsSite + /// + [Serializable] + public class CloudEventsTargetCollection: Collection + { + /// + /// Initializes a new empty instance of the CloudEventsSiteCollection class. + /// + public CloudEventsTargetCollection() + :base() + { + // empty + } + + /// + /// Initializes a new instance of the CloudEventsSiteCollection class, containing elements + /// copied from an array. + /// + /// + /// The array whose elements are to be added to the new CloudEventsSiteCollection. + /// + public CloudEventsTargetCollection(IList items) + :base() + { + if (items == null) { + throw new ArgumentNullException("items"); + } + + this.AddRange(items); + } + + /// + /// Adds the elements of an array to the end of this CloudEventsSiteCollection. + /// + /// + /// The array whose elements are to be added to the end of this CloudEventsSiteCollection. + /// + public virtual void AddRange(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + foreach (CloudEventsTarget item in items) + { + this.Add(item); + } + } + } +} diff --git a/source/newtelligence.DasBlog.Runtime/newtelligence.DasBlog.Runtime.csproj b/source/newtelligence.DasBlog.Runtime/newtelligence.DasBlog.Runtime.csproj index d39cd973..b31816f6 100644 --- a/source/newtelligence.DasBlog.Runtime/newtelligence.DasBlog.Runtime.csproj +++ b/source/newtelligence.DasBlog.Runtime/newtelligence.DasBlog.Runtime.csproj @@ -11,7 +11,7 @@ - +