diff --git a/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs b/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs new file mode 100644 index 000000000..4eae3476a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Apprise/Apprise.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Notifications.Apprise +{ + public class Apprise : NotificationBase + { + public override string Name => "Apprise"; + + public override string Link => "https://github.com/caronc/apprise"; + + private readonly IAppriseProxy _proxy; + + public Apprise(IAppriseProxy proxy) + { + _proxy = proxy; + } + + public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) + { + _proxy.SendNotification(Settings, HEALTH_ISSUE_TITLE_BRANDED, $"{healthCheck.Message}"); + } + + public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) + { + _proxy.SendNotification(Settings, APPLICATION_UPDATE_TITLE_BRANDED, $"{updateMessage.Message}"); + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(_proxy.Test(Settings)); + + return new ValidationResult(failures); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Apprise/AppriseProxy.cs b/src/NzbDrone.Core/Notifications/Apprise/AppriseProxy.cs new file mode 100644 index 000000000..c62d8c68a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Apprise/AppriseProxy.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using System.Net; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.Notifications.Apprise +{ + public interface IAppriseProxy + { + void SendNotification(AppriseSettings settings, string title, string message); + + ValidationFailure Test(AppriseSettings settings); + } + + public class AppriseProxy : IAppriseProxy + { + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public AppriseProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void SendNotification(AppriseSettings settings, string title, string body) + { + var requestBuilder = new HttpRequestBuilder(settings.BaseUrl.TrimEnd('/', ' ')).Post() + .AddFormParameter("title", title) + .AddFormParameter("body", body); + + if (settings.ConfigurationKey.IsNotNullOrWhiteSpace()) + { + requestBuilder + .Resource("/notify/{configurationKey}") + .SetSegment("configurationKey", settings.ConfigurationKey); + } + else if (settings.StatelessUrls.IsNotNullOrWhiteSpace()) + { + requestBuilder + .Resource("/notify") + .AddFormParameter("urls", settings.StatelessUrls); + } + + if (settings.Tags.Any()) + { + requestBuilder.AddFormParameter("tag", settings.Tags.Join(",")); + } + + if (settings.AuthUsername.IsNotNullOrWhiteSpace() || settings.AuthPassword.IsNotNullOrWhiteSpace()) + { + requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.AuthUsername, settings.AuthPassword); + } + + _httpClient.Execute(requestBuilder.Build()); + } + + public ValidationFailure Test(AppriseSettings settings) + { + const string title = "Prowlarr - Test Notification"; + const string body = "Success! You have properly configured your apprise notification settings."; + + try + { + SendNotification(settings, title, body); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Error(ex, $"HTTP Auth credentials are invalid: {ex.Message}"); + return new ValidationFailure("AuthUsername", $"HTTP Auth credentials are invalid: {ex.Message}"); + } + + _logger.Error(ex, "Unable to send test message. Server connection failed. Status code: {0}", ex.Message); + return new ValidationFailure("Url", $"Unable to connect to Apprise API. Please try again later. Status code: {ex.Message}"); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to send test message. Status code: {0}", ex.Message); + return new ValidationFailure("Url", $"Unable to send test message. Status code: {ex.Message}"); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs b/src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs new file mode 100644 index 000000000..d7bfd3a04 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Apprise/AppriseSettings.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Apprise +{ + public class AppriseSettingsValidator : AbstractValidator + { + public AppriseSettingsValidator() + { + RuleFor(c => c.BaseUrl).IsValidUrl(); + + RuleFor(c => c.ConfigurationKey).NotEmpty() + .When(c => c.StatelessUrls.IsNullOrWhiteSpace()) + .WithMessage("Use either Configuration Key or Stateless Urls"); + + RuleFor(c => c.ConfigurationKey).Matches("^[a-z0-9-]*$") + .WithMessage("Allowed characters a-z, 0-9 and -"); + + RuleFor(c => c.StatelessUrls).NotEmpty() + .When(c => c.ConfigurationKey.IsNullOrWhiteSpace()) + .WithMessage("Use either Configuration Key or Stateless Urls"); + + RuleFor(c => c.StatelessUrls).Empty() + .When(c => c.ConfigurationKey.IsNotNullOrWhiteSpace()) + .WithMessage("Use either Configuration Key or Stateless Urls"); + + RuleFor(c => c.Tags).Empty() + .When(c => c.StatelessUrls.IsNotNullOrWhiteSpace()) + .WithMessage("Stateless Urls do not support tags"); + } + } + + public class AppriseSettings : IProviderConfig + { + private static readonly AppriseSettingsValidator Validator = new (); + + public AppriseSettings() + { + Tags = Array.Empty(); + } + + [FieldDefinition(1, Label = "Apprise Base URL", Type = FieldType.Url, Placeholder = "http://localhost:8000", HelpText = "Apprise server Base URL, including http(s):// and port if needed", HelpLink = "https://github.com/caronc/apprise-api")] + public string BaseUrl { get; set; } + + [FieldDefinition(2, Label = "Apprise Configuration Key", Type = FieldType.Textbox, HelpText = "Configuration Key for the Persistent Storage Solution. Leave empty if Stateless Urls is used.", HelpLink = "https://github.com/caronc/apprise-api#persistent-storage-solution")] + public string ConfigurationKey { get; set; } + + [FieldDefinition(3, Label = "Apprise Stateless Urls", Type = FieldType.Textbox, HelpText = "One or more URLs separated by commas identifying where the notification should be sent to. Leave empty if Persistent Storage is used.", HelpLink = "https://github.com/caronc/apprise#productivity-based-notifications")] + public string StatelessUrls { get; set; } + + [FieldDefinition(4, Label = "Apprise Tags", Type = FieldType.Tag, HelpText = "Optionally notify only those tagged accordingly.")] + public IEnumerable Tags { get; set; } + + [FieldDefinition(5, Label = "Auth Username", Type = FieldType.Textbox, HelpText = "HTTP Basic Auth Username", Privacy = PrivacyLevel.UserName)] + public string AuthUsername { get; set; } + + [FieldDefinition(6, Label = "Auth Password", Type = FieldType.Password, HelpText = "HTTP Basic Auth Password", Privacy = PrivacyLevel.Password)] + public string AuthPassword { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +}