From feff60968524cd607d887e17640486d9b01db055 Mon Sep 17 00:00:00 2001 From: Denis Gheorghescu Date: Fri, 1 Sep 2023 18:31:16 +0300 Subject: [PATCH] New: Add support for Pushcut notifications --- .../Notifications/Pushcut/Pushcut.cs | 76 ++++++++++++++++ .../Notifications/Pushcut/PushcutException.cs | 28 ++++++ .../Notifications/Pushcut/PushcutPayload.cs | 9 ++ .../Notifications/Pushcut/PushcutProxy.cs | 88 +++++++++++++++++++ .../Notifications/Pushcut/PushcutResponse.cs | 7 ++ .../Notifications/Pushcut/PushcutSettings.cs | 35 ++++++++ src/Radarr.sln.DotSettings | 2 + 7 files changed, 245 insertions(+) create mode 100644 src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushcut/PushcutException.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushcut/PushcutPayload.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushcut/PushcutProxy.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushcut/PushcutResponse.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs diff --git a/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs b/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs new file mode 100644 index 000000000..e36fe7c46 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushcut/Pushcut.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Movies; + +namespace NzbDrone.Core.Notifications.Pushcut +{ + public class Pushcut : NotificationBase + { + private readonly IPushcutProxy _proxy; + + public Pushcut(IPushcutProxy proxy) + { + _proxy = proxy; + } + + public override string Name => "Pushcut"; + + public override string Link => "https://www.pushcut.io"; + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(_proxy.Test(Settings)); + + return new ValidationResult(failures); + } + + public override void OnGrab(GrabMessage grabMessage) + { + _proxy.SendNotification(MOVIE_GRABBED_TITLE, grabMessage?.Message, Settings); + } + + public override void OnDownload(DownloadMessage downloadMessage) + { + _proxy.SendNotification(downloadMessage.OldMovieFiles.Any() ? MOVIE_UPGRADED_TITLE : MOVIE_DOWNLOADED_TITLE, downloadMessage.Message, Settings); + } + + public override void OnMovieAdded(Movie movie) + { + _proxy.SendNotification(MOVIE_ADDED_TITLE, $"{movie.Title} added to library", Settings); + } + + public override void OnMovieFileDelete(MovieFileDeleteMessage deleteMessage) + { + _proxy.SendNotification(MOVIE_FILE_DELETED_TITLE, deleteMessage.Message, Settings); + } + + public override void OnMovieDelete(MovieDeleteMessage deleteMessage) + { + _proxy.SendNotification(MOVIE_DELETED_TITLE, deleteMessage.Message, Settings); + } + + public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) + { + _proxy.SendNotification(HEALTH_ISSUE_TITLE_BRANDED, healthCheck.Message, Settings); + } + + public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) + { + _proxy.SendNotification(HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousCheck.Message}", Settings); + } + + public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) + { + _proxy.SendNotification(APPLICATION_UPDATE_TITLE_BRANDED, updateMessage.Message, Settings); + } + + public override void OnManualInteractionRequired(ManualInteractionRequiredMessage manualInteractionRequiredMessage) + { + _proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED, manualInteractionRequiredMessage.Message, Settings); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutException.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutException.cs new file mode 100644 index 000000000..5783063e2 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutException.cs @@ -0,0 +1,28 @@ +using System; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Notifications.Pushcut +{ + public class PushcutException : NzbDroneException + { + public PushcutException(string message, params object[] args) + : base(message, args) + { + } + + public PushcutException(string message) + : base(message) + { + } + + public PushcutException(string message, Exception innerException, params object[] args) + : base(message, innerException, args) + { + } + + public PushcutException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutPayload.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutPayload.cs new file mode 100644 index 000000000..41a5d3caf --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutPayload.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Notifications.Pushcut +{ + public class PushcutPayload + { + public string Title { get; set; } + public string Text { get; set; } + public bool? IsTimeSensitive { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutProxy.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutProxy.cs new file mode 100644 index 000000000..ce11f2107 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutProxy.cs @@ -0,0 +1,88 @@ +using System.Net; +using System.Net.Http; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; + +namespace NzbDrone.Core.Notifications.Pushcut +{ + public interface IPushcutProxy + { + void SendNotification(string title, string message, PushcutSettings settings); + ValidationFailure Test(PushcutSettings settings); + } + + public class PushcutProxy : IPushcutProxy + { + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public PushcutProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void SendNotification(string title, string message, PushcutSettings settings) + { + var request = new HttpRequestBuilder("https://api.pushcut.io/v1/notifications/{notificationName}") + .SetSegment("notificationName", settings?.NotificationName) + .SetHeader("API-Key", settings?.ApiKey) + .Accept(HttpAccept.Json) + .Build(); + var payload = new PushcutPayload + { + Title = title, + Text = message, + IsTimeSensitive = settings?.TimeSensitive + }; + + request.Method = HttpMethod.Post; + request.Headers.ContentType = "application/json"; + request.SetContent(payload.ToJson()); + + try + { + _httpClient.Execute(request); + } + catch (HttpException exception) + { + _logger.Error(exception, "Unable to send Pushcut notification: {0}", exception.Message); + throw new PushcutException("Unable to send Pushcut notification: {0}", exception.Message, exception); + } + } + + public ValidationFailure Test(PushcutSettings settings) + { + try + { + const string title = "Radarr Test Title"; + const string message = "Success! You have properly configured your Pushcut notification settings."; + SendNotification(title, message, settings); + } + catch (PushcutException pushcutException) when (pushcutException.InnerException is HttpException httpException) + { + if (httpException.Response.StatusCode == HttpStatusCode.Forbidden) + { + _logger.Error(pushcutException, "API Key is invalid: {0}", pushcutException.Message); + return new ValidationFailure("API Key", $"API Key is invalid: {pushcutException.Message}"); + } + + if (httpException.Response.Content.IsNotNullOrWhiteSpace()) + { + var response = Json.Deserialize(httpException.Response.Content); + + _logger.Error(pushcutException, "Unable to send test notification. Response from Pushcut: {0}", response.Error); + return new ValidationFailure("Url", $"Unable to send test notification. Response from Pushcut: {response.Error}"); + } + + _logger.Error(pushcutException, "Unable to connect to Pushcut API. Server connection failed: ({0}) {1}", httpException.Response.StatusCode, pushcutException.Message); + return new ValidationFailure("Host", $"Unable to connect to Pushcut API. Server connection failed: ({httpException.Response.StatusCode}) {pushcutException.Message}"); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutResponse.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutResponse.cs new file mode 100644 index 000000000..280880cd0 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutResponse.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Notifications.Pushcut +{ + public class PushcutResponse + { + public string Error { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs b/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs new file mode 100644 index 000000000..625bca01e --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushcut/PushcutSettings.cs @@ -0,0 +1,35 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Pushcut +{ + public class PushcutSettingsValidator : AbstractValidator + { + public PushcutSettingsValidator() + { + RuleFor(settings => settings.ApiKey).NotEmpty(); + RuleFor(settings => settings.NotificationName).NotEmpty(); + } + } + + public class PushcutSettings : IProviderConfig + { + private static readonly PushcutSettingsValidator Validator = new (); + + [FieldDefinition(0, Label = "Notification name", Type = FieldType.Textbox, HelpText = "Notification name from Notifications tab of the Pushcut app.")] + public string NotificationName { get; set; } + + [FieldDefinition(1, Label = "API Key", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey, HelpText = "API Keys can be managed in the Account view of the Pushcut app.")] + public string ApiKey { get; set; } + + [FieldDefinition(2, Label = "Time sensitive", Type = FieldType.Checkbox, HelpText = "Check to mark the notification as \"Time-Sensitive\"")] + public bool TimeSensitive { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/Radarr.sln.DotSettings b/src/Radarr.sln.DotSettings index c09f7c54f..0e0cba3f2 100644 --- a/src/Radarr.sln.DotSettings +++ b/src/Radarr.sln.DotSettings @@ -1,3 +1,5 @@  + True + True True True \ No newline at end of file