diff --git a/.vscode/settings.json b/.vscode/settings.json index 8ec43a2..ddd3c39 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,7 @@ "password": "postgres" } ], - "editor.formatOnType": true + "editor.formatOnType": true, + "csharp.inlayHints.parameters.forObjectCreationParameters": true, + "omnisharp.organizeImportsOnFormat": true } \ No newline at end of file diff --git a/DiscordWebhookClient b/DiscordWebhookClient new file mode 160000 index 0000000..d84949a --- /dev/null +++ b/DiscordWebhookClient @@ -0,0 +1 @@ +Subproject commit d84949a6b735b8e5a079d1d298726da6ca0220e9 diff --git a/Newsbot.Collector.Domain/Interfaces/IDiscordNotificationClient.cs b/Newsbot.Collector.Domain/Interfaces/IDiscordNotificationClient.cs new file mode 100644 index 0000000..864d6a3 --- /dev/null +++ b/Newsbot.Collector.Domain/Interfaces/IDiscordNotificationClient.cs @@ -0,0 +1,8 @@ +using Newsbot.Collector.Domain.Models; + +namespace Newsbot.Collector.Domain.Interfaces; + +public interface IDiscordNotificatioClient +{ + void SendMessage(DiscordMessage payload); +} \ No newline at end of file diff --git a/Newsbot.Collector.Domain/Models/DatabaseModel.cs b/Newsbot.Collector.Domain/Models/DatabaseModel.cs index 3fd6c5d..8b48619 100644 --- a/Newsbot.Collector.Domain/Models/DatabaseModel.cs +++ b/Newsbot.Collector.Domain/Models/DatabaseModel.cs @@ -6,7 +6,7 @@ public class ArticlesModel public Guid SourceID { get; set; } public string Tags { get; set; } = ""; public string Title { get; set; } = ""; - public string URL { get; set; } = ""; + public string? URL { get; set; } public DateTime PubDate { get; set; } = DateTime.Now; public string Video { get; set; } = ""; public int VideoHeight { get; set; } = 0; @@ -14,7 +14,7 @@ public class ArticlesModel public string Thumbnail { get; set; } = ""; public string Description { get; set; } = ""; public string AuthorName { get; set; } = ""; - public string AuthorImage { get; set; } = ""; + public string? AuthorImage { get; set; } } public class AuthorModel diff --git a/Newsbot.Collector.Domain/Models/DiscordMessage.cs b/Newsbot.Collector.Domain/Models/DiscordMessage.cs new file mode 100644 index 0000000..d2c2c13 --- /dev/null +++ b/Newsbot.Collector.Domain/Models/DiscordMessage.cs @@ -0,0 +1,65 @@ +using Newtonsoft.Json; + +namespace Newsbot.Collector.Domain.Models; + +public class DiscordMessage +{ + [JsonProperty("content")] + public string? Content { get; set; } + + [JsonProperty("username")] + public string? Username { get; set; } + + [JsonProperty("avatar_url")] + public string? AvatarUrl { get; set; } + //[JsonProperty("thread_name")] + //public string ThreadName { get; set; } = ""; + + [JsonProperty("embeds")] + public DiscordMessageEmbed[]? Embeds { get; set; } +} + +public class DiscordMessageEmbedAuthor +{ + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("url")] + public string? Url { get; set; } + + [JsonProperty("icon_url")] + public string? IconUrl { get; set; } +} + +public class DiscordMessageEmbedField +{ + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("value")] + public string? Value { get; set; } + + [JsonProperty("inline")] + public bool Inline { get; set; } +} + +public class DiscordMessageEmbedImage +{ + [JsonProperty("url")] + public string? Url { get; set; } +} + +public class DiscordMessageEmbedThumbnail +{ + [JsonProperty("url")] + public string? Url { get; set; } +} + +public class DiscordMessageEmbedFooter +{ + [JsonProperty("text")] + public string? Text { get; set; } + + [JsonProperty("icon_url")] + public string? IconUrl { get; set; } +} \ No newline at end of file diff --git a/Newsbot.Collector.Domain/Models/DiscordMessageEmbed.cs b/Newsbot.Collector.Domain/Models/DiscordMessageEmbed.cs new file mode 100644 index 0000000..9a3e1d4 --- /dev/null +++ b/Newsbot.Collector.Domain/Models/DiscordMessageEmbed.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace Newsbot.Collector.Domain.Models; + +public class DiscordMessageEmbed +{ + [JsonProperty("title")] + public string? Title { get; set; } + + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("url")] + public string? Url { get; set; } + + [JsonProperty("color")] + public int? Color { get; set; } + + //2023-03-31T07:00:00.000Z + //[JsonProperty("timestamp")] + //public DateTime TimeStamp { get; set; } + + [JsonProperty("author")] + public DiscordMessageEmbedAuthor? Author { get; set; } + + [JsonProperty("fields")] + public DiscordMessageEmbedField[]? Fields { get; set; } + + [JsonProperty("image")] + public DiscordMessageEmbedImage? Image { get; set; } + + [JsonProperty("thumbnail")] + public DiscordMessageEmbedThumbnail? Thumbnail { get; set; } + + [JsonProperty("footer")] + public DiscordMessageEmbedFooter? Footer { get; set; } +} \ No newline at end of file diff --git a/Newsbot.Collector.Domain/Models/DiscordMessageEmbedColors.cs b/Newsbot.Collector.Domain/Models/DiscordMessageEmbedColors.cs new file mode 100644 index 0000000..20ddbdb --- /dev/null +++ b/Newsbot.Collector.Domain/Models/DiscordMessageEmbedColors.cs @@ -0,0 +1,8 @@ +namespace Newsbot.Collector.Domain.Models; + +public class DiscordMessageEmbedColors +{ + public const int Black = 0; + public const int Red = 16711680; + public const int DarkBlue = 655615; +} \ No newline at end of file diff --git a/Newsbot.Collector.Domain/Newsbot.Collector.Domain.csproj b/Newsbot.Collector.Domain/Newsbot.Collector.Domain.csproj index 5177e08..0f6f10f 100644 --- a/Newsbot.Collector.Domain/Newsbot.Collector.Domain.csproj +++ b/Newsbot.Collector.Domain/Newsbot.Collector.Domain.csproj @@ -8,6 +8,7 @@ + diff --git a/Newsbot.Collector.Services/Jobs/DiscordNotificationJob.cs b/Newsbot.Collector.Services/Jobs/DiscordNotificationJob.cs new file mode 100644 index 0000000..0bded1f --- /dev/null +++ b/Newsbot.Collector.Services/Jobs/DiscordNotificationJob.cs @@ -0,0 +1,109 @@ +using Newsbot.Collector.Database.Repositories; +using Newsbot.Collector.Domain.Interfaces; +using Newsbot.Collector.Domain.Models; +using Newsbot.Collector.Services.Notifications.Discord; + +namespace Newsbot.Collector.Services.Jobs; + +public class DiscordNotificationJobOptions +{ + +} + +public class DiscordNotifificationJob +{ + + private IDiscordQueueRepository _queue; + private IArticlesRepository _article; + private IDiscordWebHooksRepository _webhook; + private ISourcesRepository _sources; + private ISubscriptionRepository _subs; + + private IDiscordNotificatioClient _webhookClient; + + public DiscordNotifificationJob() + { + _queue = new DiscordQueueTable(""); + _article = new ArticlesTable(""); + _webhook = new DiscordWebhooksTable(""); + _sources = new SourcesTable(""); + _subs = new SubscriptionsTable(""); + _webhookClient = new DiscordWebhookClient(""); + } + + public void InitAndExecute() + { + + } + + private void Execute() + { + //collect all the new requests + var requests = _queue.List(25); + + foreach (var request in requests) + { + // Get all details on the article in the queue + var articleDetails = _article.GetById(request.ArticleID); + + // Get the deatils of the source + var sourceDetails = _sources.GetByID(articleDetails.SourceID); + + // Find all the subscriptions for that source + var allSubscriptions = _subs.ListBySourceID(sourceDetails.ID); + + foreach (var sub in allSubscriptions) + { + // find the discord webhooks we need to post to + var discordDetails = _webhook.GetByID(sub.DiscordWebHookID); + + var client = new DiscordWebhookClient(discordDetails.Url); + client.SendMessage(GenerateDiscordMessage(sourceDetails, articleDetails)); + } + } + } + + public DiscordMessage GenerateDiscordMessage(SourceModel source, ArticlesModel article) + { + var embed = new DiscordMessageEmbed + { + Title = article.Title, + Color = DiscordMessageEmbedColors.Red, + Description = article.Description, + Author = new DiscordMessageEmbedAuthor + { + Name = article.AuthorName, + }, + Footer = new DiscordMessageEmbedFooter + { + Text = "Brought to you by Newsbot", + } + }; + + if (article.URL is not null && article.URL != "") + { + embed.Url = article.URL; + } + + if (article.Thumbnail is not null && article.Thumbnail != "") + { + embed.Image = new DiscordMessageEmbedImage + { + Url = article.Thumbnail + }; + } + + if (article.AuthorImage is not null && article.AuthorImage != "") + { + embed.Author.IconUrl = article.AuthorImage; + } + + return new DiscordMessage + { + Embeds = new DiscordMessageEmbed[] + { + embed + } + }; + } +} \ No newline at end of file diff --git a/Newsbot.Collector.Services/Jobs/RssWatcherJob.cs b/Newsbot.Collector.Services/Jobs/RssWatcherJob.cs index 2cb2861..8d6ce4e 100644 --- a/Newsbot.Collector.Services/Jobs/RssWatcherJob.cs +++ b/Newsbot.Collector.Services/Jobs/RssWatcherJob.cs @@ -131,6 +131,12 @@ public class RssWatcherJob : IHangfireJob { foreach (var item in items) { + if (item.URL is null) + { + Log.Warning($"RSS Watcher collected a blank url and was skipped."); + continue; + } + if (IsThisUrlKnown(item.URL) == true) { continue; diff --git a/Newsbot.Collector.Services/Notifications/Discord/DiscordWebhookClient.cs b/Newsbot.Collector.Services/Notifications/Discord/DiscordWebhookClient.cs new file mode 100644 index 0000000..54642e2 --- /dev/null +++ b/Newsbot.Collector.Services/Notifications/Discord/DiscordWebhookClient.cs @@ -0,0 +1,47 @@ +using System.Net; +using System.Text; +using Newsbot.Collector.Domain.Interfaces; +using Newsbot.Collector.Domain.Models; +using Newtonsoft.Json; + +namespace Newsbot.Collector.Services.Notifications.Discord; + +public class DiscordWebhookClient : IDiscordNotificatioClient +{ + + private string[] _webhooks; + + public DiscordWebhookClient(string webhook) + { + _webhooks = new string[] { webhook }; + } + + public DiscordWebhookClient(string[] webhooks) + { + _webhooks = webhooks; + } + + public void SendMessage(DiscordMessage payload) + { + if (payload.Embeds is not null) + { + MessageValidation.IsEmbedFooterValid(payload.Embeds); + } + + foreach (var webhook in _webhooks) + { + var jsonRaw = JsonConvert.SerializeObject(payload, Newtonsoft.Json.Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + using StringContent jsonContent = new(jsonRaw, Encoding.UTF8, "application/json"); + + using var client = new HttpClient(); + var resp = client.PostAsync(webhook, jsonContent); + resp.Wait(); + + if (resp.Result.StatusCode != HttpStatusCode.NoContent) + { + throw new Exception("Message was not accepted by the sever."); + } + } + } + +} \ No newline at end of file diff --git a/Newsbot.Collector.Services/Notifications/Discord/MessageValidation.cs b/Newsbot.Collector.Services/Notifications/Discord/MessageValidation.cs new file mode 100644 index 0000000..4aaa3fa --- /dev/null +++ b/Newsbot.Collector.Services/Notifications/Discord/MessageValidation.cs @@ -0,0 +1,50 @@ +using Newsbot.Collector.Domain.Models; + +namespace Newsbot.Collector.Services.Notifications.Discord; + +public class MessageValidation +{ + public void IsMessageValid(DiscordMessage payload) + { + if (payload.Embeds is null) + { + + } + } + + public static bool IsEmbedFooterValid(DiscordMessageEmbed[] embeds) + { + if (embeds.Count() == 0) + { + return true; + } + + foreach (var embed in embeds) + { + if (embed.Footer is null) + { + return true; + } + + if (embed.Footer.IconUrl is null) + { + return true; + } + + var containsHttp = embed.Footer.IconUrl.Contains("http://"); + var containsHttps = embed.Footer.IconUrl.Contains("https://"); + + if (containsHttp == false || containsHttps == false) + { + throw new Exception("Footer.IconUrl does not contain http:// or https://"); + } + } + + return true; + } + + public static bool IsEmbedAuthorValid(DiscordMessageEmbedAuthor author) + { + return true; + } +} \ No newline at end of file