From de0d0a35266448ebeb956f30af8b6be43e2be24e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 16 Mar 2019 13:56:29 -0700 Subject: [PATCH] New: Discord Notifications Closes #1511 --- .../Notifications/Discord/Discord.cs | 123 ++++++++++++++++++ .../Notifications/Discord/DiscordColors.cs | 9 ++ .../Notifications/Discord/DiscordException.cs | 16 +++ .../Notifications/Discord/DiscordProxy.cs | 46 +++++++ .../Notifications/Discord/DiscordSettings.cs | 35 +++++ .../Discord/Payloads/DiscordPayload.cs | 17 +++ .../Notifications/Discord/Payloads/Embed.cs | 10 ++ .../Notifications/Slack/Slack.cs | 9 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 7 + 9 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/Discord/Discord.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/DiscordException.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordPayload.cs create mode 100644 src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs new file mode 100644 index 000000000..9a8bff070 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Notifications.Discord.Payloads; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Discord +{ + public class Discord : NotificationBase + { + private readonly IDiscordProxy _proxy; + + public Discord(IDiscordProxy proxy) + { + _proxy = proxy; + } + + public override string Name => "Discord"; + public override string Link => "https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks"; + + public override void OnGrab(GrabMessage message) + { + var embeds = new List + { + new Embed + { + Description = message.Message, + Title = message.Series.Title, + Text = message.Message, + Color = (int)DiscordColors.Warning + } + }; + var payload = CreatePayload($"Grabbed: {message.Message}", embeds); + + _proxy.SendPayload(payload, Settings); + } + + public override void OnDownload(DownloadMessage message) + { + var embeds = new List + { + new Embed + { + Description = message.Message, + Title = message.Series.Title, + Text = message.Message, + Color = (int)DiscordColors.Success + } + }; + var payload = CreatePayload($"Imported: {message.Message}", embeds); + + _proxy.SendPayload(payload, Settings); + } + + public override void OnRename(Series series) + { + var attachments = new List + { + new Embed + { + Title = series.Title, + } + }; + + var payload = CreatePayload("Renamed", attachments); + + _proxy.SendPayload(payload, Settings); + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(TestMessage()); + + return new ValidationResult(failures); + } + + public ValidationFailure TestMessage() + { + try + { + var message = $"Test message from Sonarr posted at {DateTime.Now}"; + var payload = CreatePayload(message); + + _proxy.SendPayload(payload, Settings); + + } + catch (DiscordException ex) + { + return new NzbDroneValidationFailure("Unable to post", ex.Message); + } + + return null; + } + + private DiscordPayload CreatePayload(string message, List embeds = null) + { + var avatar = Settings.Avatar; + + var payload = new DiscordPayload + { + Username = Settings.Username, + Content = message, + Embeds = embeds + }; + + if (avatar.IsNotNullOrWhiteSpace()) + { + payload.AvatarUrl = avatar; + } + + if (Settings.Username.IsNotNullOrWhiteSpace()) + { + payload.Username = Settings.Username; + } + + return payload; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs new file mode 100644 index 000000000..16590aade --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordColors.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Notifications.Discord +{ + public enum DiscordColors + { + Danger = 15749200, + Success = 2605644, + Warning = 16753920 + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordException.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordException.cs new file mode 100644 index 000000000..1bc0d6294 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordException.cs @@ -0,0 +1,16 @@ +using System; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Notifications.Discord +{ + class DiscordException : NzbDroneException + { + public DiscordException(string message) : base(message) + { + } + + public DiscordException(string message, Exception innerException, params object[] args) : base(message, innerException, args) + { + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs new file mode 100644 index 000000000..425364253 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordProxy.cs @@ -0,0 +1,46 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Notifications.Discord.Payloads; +using NzbDrone.Core.Rest; + +namespace NzbDrone.Core.Notifications.Discord +{ + public interface IDiscordProxy + { + void SendPayload(DiscordPayload payload, DiscordSettings settings); + } + + public class DiscordProxy : IDiscordProxy + { + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public DiscordProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void SendPayload(DiscordPayload payload, DiscordSettings settings) + { + try + { + var request = new HttpRequestBuilder(settings.WebHookUrl) + .Accept(HttpAccept.Json) + .Build(); + + request.Method = HttpMethod.POST; + request.Headers.ContentType = "application/json"; + request.SetContent(payload.ToJson()); + + _httpClient.Execute(request); + } + catch (RestException ex) + { + _logger.Error(ex, "Unable to post payload {0}", payload); + throw new DiscordException("Unable to post payload", ex); + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs new file mode 100644 index 000000000..e4ba51571 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/DiscordSettings.cs @@ -0,0 +1,35 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Discord +{ + public class DiscordSettingsValidator : AbstractValidator + { + public DiscordSettingsValidator() + { + RuleFor(c => c.WebHookUrl).IsValidUrl(); + } + } + + public class DiscordSettings : IProviderConfig + { + private static readonly DiscordSettingsValidator Validator = new DiscordSettingsValidator(); + + [FieldDefinition(0, Label = "Webhook URL", HelpText = "Discord channel webhook url")] + public string WebHookUrl { get; set; } + + [FieldDefinition(1, Label = "Username", HelpText = "The username to post as, defaults to Discord webhook default")] + public string Username { get; set; } + + [FieldDefinition(2, Label = "Avatar", HelpText = "Change the avatar that is used for messages from this integration", Type = FieldType.Textbox)] + public string Avatar { get; set; } + + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordPayload.cs b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordPayload.cs new file mode 100644 index 000000000..37f1f1c3d --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Payloads/DiscordPayload.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Discord.Payloads +{ + public class DiscordPayload + { + public string Content { get; set; } + + public string Username { get; set; } + + [JsonProperty("avatar_url")] + public string AvatarUrl { get; set; } + + public List Embeds { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs b/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs new file mode 100644 index 000000000..50e27914b --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Discord/Payloads/Embed.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.Notifications.Discord.Payloads +{ + public class Embed + { + public string Description { get; set; } + public string Title { get; set; } + public string Text { get; set; } + public int Color { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Slack/Slack.cs b/src/NzbDrone.Core/Notifications/Slack/Slack.cs index c8afe6e00..431e4d82c 100644 --- a/src/NzbDrone.Core/Notifications/Slack/Slack.cs +++ b/src/NzbDrone.Core/Notifications/Slack/Slack.cs @@ -1,15 +1,10 @@ using System; using System.Collections.Generic; using FluentValidation.Results; -using NLog; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; using NzbDrone.Core.Notifications.Slack.Payloads; -using NzbDrone.Core.Rest; using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; -using RestSharp; namespace NzbDrone.Core.Notifications.Slack @@ -17,12 +12,10 @@ namespace NzbDrone.Core.Notifications.Slack public class Slack : NotificationBase { private readonly ISlackProxy _proxy; - private readonly Logger _logger; - public Slack(ISlackProxy proxy, Logger logger) + public Slack(ISlackProxy proxy) { _proxy = proxy; - _logger = logger; } public override string Name => "Slack"; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 586b15686..05aab29e3 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -779,6 +779,13 @@ + + + + + + +