From f38077aac7e2c9f2ffe5a1b318cc0ad7b91c0c3d Mon Sep 17 00:00:00 2001 From: Ricardo Christmann <80476005+ricci2511@users.noreply.github.com> Date: Sat, 15 Jul 2023 04:22:08 +0200 Subject: [PATCH] New: Add pushsafer notification service (#8770) --- .../Notifications/Pushsafer/Pushsafer.cs | 74 ++++++++++++ .../Pushsafer/PushsaferPriority.cs | 11 ++ .../Notifications/Pushsafer/PushsaferProxy.cs | 105 ++++++++++++++++++ .../Pushsafer/PushsaferSettings.cs | 68 ++++++++++++ .../Validation/RuleBuilderExtensions.cs | 13 +++ 5 files changed, 271 insertions(+) create mode 100644 src/NzbDrone.Core/Notifications/Pushsafer/Pushsafer.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushsafer/PushsaferPriority.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushsafer/PushsaferProxy.cs create mode 100644 src/NzbDrone.Core/Notifications/Pushsafer/PushsaferSettings.cs diff --git a/src/NzbDrone.Core/Notifications/Pushsafer/Pushsafer.cs b/src/NzbDrone.Core/Notifications/Pushsafer/Pushsafer.cs new file mode 100644 index 000000000..7a75bd84c --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushsafer/Pushsafer.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Movies; + +namespace NzbDrone.Core.Notifications.Pushsafer +{ + public class Pushsafer : NotificationBase + { + private readonly IPushsaferProxy _proxy; + + public Pushsafer(IPushsaferProxy proxy) + { + _proxy = proxy; + } + + public override string Name => "Pushsafer"; + public override string Link => "https://pushsafer.com/"; + + public override void OnGrab(GrabMessage grabMessage) + { + _proxy.SendNotification(MOVIE_GRABBED_TITLE, grabMessage.Message, Settings); + } + + public override void OnDownload(DownloadMessage message) + { + _proxy.SendNotification(MOVIE_DOWNLOADED_TITLE, message.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, healthCheck.Message, Settings); + } + + public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) + { + _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings); + } + + public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) + { + _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings); + } + + public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message) + { + _proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, Settings); + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(_proxy.Test(Settings)); + + return new ValidationResult(failures); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferPriority.cs b/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferPriority.cs new file mode 100644 index 000000000..d3a654f85 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferPriority.cs @@ -0,0 +1,11 @@ +namespace NzbDrone.Core.Notifications.Pushsafer +{ + public enum PushsaferPriority + { + Silent = -2, + Quiet = -1, + Normal = 0, + High = 1, + Emergency = 2 + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferProxy.cs b/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferProxy.cs new file mode 100644 index 000000000..915391b23 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferProxy.cs @@ -0,0 +1,105 @@ +using System; +using System.Net; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.Notifications.Pushsafer +{ + public interface IPushsaferProxy + { + void SendNotification(string title, string message, PushsaferSettings settings); + ValidationFailure Test(PushsaferSettings settings); + } + + public class PushsaferProxy : IPushsaferProxy + { + private const string URL = "https://pushsafer.com/api"; + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public PushsaferProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void SendNotification(string title, string message, PushsaferSettings settings) + { + var requestBuilder = new HttpRequestBuilder(URL).Post(); + + requestBuilder.AddFormParameter("k", settings.ApiKey) + .AddFormParameter("d", string.Join("|", settings.DeviceIds)) + .AddFormParameter("t", title) + .AddFormParameter("m", message) + .AddFormParameter("pr", settings.Priority); + + if ((PushsaferPriority)settings.Priority == PushsaferPriority.Emergency) + { + requestBuilder.AddFormParameter("re", settings.Retry); + requestBuilder.AddFormParameter("ex", settings.Expire); + } + + if (!settings.Sound.IsNullOrWhiteSpace()) + { + requestBuilder.AddFormParameter("s", settings.Sound); + } + + if (!settings.Vibration.IsNullOrWhiteSpace()) + { + requestBuilder.AddFormParameter("v", settings.Vibration); + } + + if (!settings.Icon.IsNullOrWhiteSpace()) + { + requestBuilder.AddFormParameter("i", settings.Icon); + } + + if (!settings.IconColor.IsNullOrWhiteSpace()) + { + requestBuilder.AddFormParameter("c", settings.IconColor); + } + + var request = requestBuilder.Build(); + + var response = _httpClient.Post(request); + + // https://www.pushsafer.com/en/pushapi#api-message + if (response.StatusCode != HttpStatusCode.OK) + { + throw new HttpException(request, response); + } + } + + public ValidationFailure Test(PushsaferSettings settings) + { + try + { + const string title = "Test Notification"; + const string body = "This is a test message from Radarr"; + + SendNotification(title, body, settings); + } + catch (HttpException ex) + { + _logger.Error(ex, "Unable to send test message"); + + return (int)ex.Response.StatusCode switch + { + 250 => new ValidationFailure("ApiKey", "API key is invalid"), + 270 => new ValidationFailure("DeviceIds", "Device ID is invalid"), + 275 => new ValidationFailure("DeviceIds", "Device Group ID is invalid"), + _ => null, + }; + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to send test message"); + return new ValidationFailure("ApiKey", "Unable to send test message"); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferSettings.cs b/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferSettings.cs new file mode 100644 index 000000000..ea0aeb104 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Pushsafer/PushsaferSettings.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Pushsafer +{ + public class PushsaferSettingsValidator : AbstractValidator + { + public PushsaferSettingsValidator() + { + RuleFor(c => c.ApiKey).NotEmpty(); + RuleFor(c => c.Retry).GreaterThanOrEqualTo(60).LessThanOrEqualTo(10800).When(c => (PushsaferPriority)c.Priority == PushsaferPriority.Emergency); + RuleFor(c => c.Expire).GreaterThanOrEqualTo(60).LessThanOrEqualTo(10800).When(c => (PushsaferPriority)c.Priority == PushsaferPriority.Emergency); + RuleFor(c => c.Sound).ValidParsedStringRange(0, 62).When(c => c.Sound.IsNotNullOrWhiteSpace()); + RuleFor(c => c.Vibration).ValidParsedStringRange(1, 3).When(c => c.Vibration.IsNotNullOrWhiteSpace()); + RuleFor(c => c.Icon).ValidParsedStringRange(1, 181).When(c => c.Icon.IsNotNullOrWhiteSpace()); + RuleFor(c => c.IconColor).Matches(new Regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")).When(c => c.IconColor.IsNotNullOrWhiteSpace()); + } + } + + public class PushsaferSettings : IProviderConfig + { + private static readonly PushsaferSettingsValidator Validator = new (); + + public PushsaferSettings() + { + Priority = (int)PushsaferPriority.Normal; + DeviceIds = Array.Empty(); + } + + [FieldDefinition(0, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://www.pushsafer.com/en/pushapi_ext#API-K")] + public string ApiKey { get; set; } + + [FieldDefinition(1, Label = "Device IDs", HelpText = "Device Group ID or list of Device IDs (leave blank to send to all devices)", Type = FieldType.Tag, Placeholder = "123456789|987654321")] + public IEnumerable DeviceIds { get; set; } + + [FieldDefinition(2, Label = "Priority", Type = FieldType.Select, SelectOptions = typeof(PushsaferPriority))] + public int Priority { get; set; } + + [FieldDefinition(3, Label = "Retry", Type = FieldType.Textbox, HelpText = "Interval to retry Emergency alerts, minimum 60 seconds")] + public int Retry { get; set; } + + [FieldDefinition(4, Label = "Expire", Type = FieldType.Textbox, HelpText = "Maximum time to retry Emergency alerts, maximum 10800 seconds")] + public int Expire { get; set; } + + [FieldDefinition(5, Label = "Sound", Type = FieldType.Textbox, Advanced = true, HelpText = "Notification sound 0-62 (leave blank to use the default)", HelpLink = "https://www.pushsafer.com/en/pushapi_ext#API-S")] + public string Sound { get; set; } + + [FieldDefinition(6, Label = "Vibration", Type = FieldType.Textbox, Advanced = true, HelpText = "Vibration pattern 1-3 (leave blank to use the device default)", HelpLink = "https://www.pushsafer.com/en/pushapi_ext#API-V")] + public string Vibration { get; set; } + + [FieldDefinition(7, Label = "Icon", Type = FieldType.Textbox, Advanced = true, HelpText = "Icon number 1-181 (leave blank to use the default Pushsafer icon)", HelpLink = "https://www.pushsafer.com/en/pushapi_ext#API-I")] + public string Icon { get; set; } + + [FieldDefinition(8, Label = "Icon Color", Type = FieldType.Textbox, Advanced = true, HelpText = "Icon color in hex format (leave blank to use the default Pushsafer icon color)", HelpLink = "https://www.pushsafer.com/en/pushapi_ext#API-C")] + public string IconColor { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index fda6fda86..f7343d542 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -73,5 +73,18 @@ public static IRuleBuilderOptions ContainsRadarr(this IRuleBuilder ruleBuilder.SetValidator(new NotEmptyValidator(null)); return ruleBuilder.SetValidator(new RegularExpressionValidator("radarr", RegexOptions.IgnoreCase)).WithMessage("Must contain Radarr"); } + + public static IRuleBuilderOptions ValidParsedStringRange(this IRuleBuilder ruleBuilder, int minValue, int maxValue) + { + return ruleBuilder.Must(x => + { + if (int.TryParse(x, out var value)) + { + return value >= minValue && value <= maxValue; + } + + return false; + }).WithMessage($"Must be greater than or equal to '{minValue}' and less than or equal to '{maxValue}'"); + } } }