New: Add Apprise notifications

This commit is contained in:
Bogdan 2023-04-22 08:54:04 +03:00 committed by Mark McDowall
parent 720fc2818f
commit ed7d99c160
7 changed files with 315 additions and 0 deletions

View File

@ -0,0 +1,64 @@
using System.Collections.Generic;
using FluentValidation.Results;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Notifications.Apprise
{
public class Apprise : NotificationBase<AppriseSettings>
{
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 OnGrab(GrabMessage grabMessage)
{
_proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings);
}
public override void OnDownload(DownloadMessage message)
{
_proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings);
}
public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage)
{
_proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings);
}
public override void OnSeriesAdd(SeriesAddMessage message)
{
_proxy.SendNotification(SERIES_ADDED_TITLE, message.Message, Settings);
}
public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage)
{
_proxy.SendNotification(SERIES_DELETED_TITLE, deleteMessage.Message, Settings);
}
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
{
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
_proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_proxy.Test(Settings));
return new ValidationResult(failures);
}
}
}

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Notifications.Apprise
{
public class AppriseError
{
public string Error { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Notifications.Apprise
{
public class AppriseException : NzbDroneException
{
public AppriseException(string message)
: base(message)
{
}
public AppriseException(string message, Exception innerException, params object[] args)
: base(message, innerException, args)
{
}
}
}

View File

@ -0,0 +1,19 @@
using System.Runtime.Serialization;
namespace NzbDrone.Core.Notifications.Apprise
{
public enum AppriseNotificationType
{
[EnumMember(Value = "info")]
Info = 0,
[EnumMember(Value = "success")]
Success,
[EnumMember(Value = "warning")]
Warning,
[EnumMember(Value = "failure")]
Failure,
}
}

View File

@ -0,0 +1,15 @@
namespace NzbDrone.Core.Notifications.Apprise
{
public class ApprisePayload
{
public string Urls { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public AppriseNotificationType Type { get; set; }
public string Tag { get; set; }
}
}

View File

@ -0,0 +1,118 @@
using System;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Notifications.Apprise
{
public interface IAppriseProxy
{
void SendNotification(string title, string message, AppriseSettings settings);
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(string title, string message, AppriseSettings settings)
{
var payload = new ApprisePayload
{
Title = title,
Body = message,
Type = (AppriseNotificationType)settings.NotificationType
};
var requestBuilder = new HttpRequestBuilder(settings.ServerUrl.TrimEnd('/', ' '))
.Post()
.Accept(HttpAccept.Json);
if (settings.ConfigurationKey.IsNotNullOrWhiteSpace())
{
requestBuilder
.Resource("/notify/{configurationKey}")
.SetSegment("configurationKey", settings.ConfigurationKey);
}
else if (settings.StatelessUrls.IsNotNullOrWhiteSpace())
{
requestBuilder.Resource("/notify");
payload.Urls = settings.StatelessUrls;
}
if (settings.Tags.Any())
{
payload.Tag = settings.Tags.Join(",");
}
if (settings.AuthUsername.IsNotNullOrWhiteSpace() || settings.AuthPassword.IsNotNullOrWhiteSpace())
{
requestBuilder.NetworkCredential = new BasicNetworkCredential(settings.AuthUsername, settings.AuthPassword);
}
var request = requestBuilder.Build();
request.Headers.ContentType = "application/json";
request.SetContent(payload.ToJson());
try
{
_httpClient.Execute(request);
}
catch (HttpException ex)
{
_logger.Error(ex, "Unable to send message");
throw new AppriseException("Unable to send Apprise notifications: {0}", ex, ex.Message);
}
}
public ValidationFailure Test(AppriseSettings settings)
{
const string title = "Sonarr - Test Notification";
const string body = "Success! You have properly configured your apprise notification settings.";
try
{
SendNotification(title, body, settings);
}
catch (AppriseException ex) when (ex.InnerException is HttpException httpException)
{
if (httpException.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, $"HTTP Auth credentials are invalid: {0}", ex.Message);
return new ValidationFailure("AuthUsername", $"HTTP Auth credentials are invalid: {ex.Message}");
}
if (httpException.Response.Content.IsNotNullOrWhiteSpace())
{
var error = Json.Deserialize<AppriseError>(httpException.Response.Content);
_logger.Error(ex, $"Unable to send test message. Response from API: {0}", error.Error);
return new ValidationFailure(string.Empty, $"Unable to send test message. Response from API: {error.Error}");
}
_logger.Error(ex, "Unable to send test message. Server connection failed: ({0}) {1}", httpException.Response.StatusCode, ex.Message);
return new ValidationFailure("Url", $"Unable to connect to Apprise API. Server connection failed: ({httpException.Response.StatusCode}) {ex.Message}");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message: {0}", ex.Message);
return new ValidationFailure("Url", $"Unable to send test message: {ex.Message}");
}
return null;
}
}
}

View File

@ -0,0 +1,74 @@
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<AppriseSettings>
{
public AppriseSettingsValidator()
{
RuleFor(c => c.ServerUrl).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()
{
NotificationType = (int)AppriseNotificationType.Info;
Tags = Array.Empty<string>();
}
[FieldDefinition(1, Label = "Apprise Server URL", Type = FieldType.Url, Placeholder = "http://localhost:8000", HelpText = "Apprise server URL, including http(s):// and port if needed", HelpLink = "https://github.com/caronc/apprise-api")]
public string ServerUrl { 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 Notification Type", Type = FieldType.Select, SelectOptions = typeof(AppriseNotificationType))]
public int NotificationType { get; set; }
[FieldDefinition(5, Label = "Apprise Tags", Type = FieldType.Tag, HelpText = "Optionally notify only those tagged accordingly.")]
public IEnumerable<string> Tags { get; set; }
[FieldDefinition(6, Label = "Username", Type = FieldType.Textbox, HelpText = "HTTP Basic Auth Username", Privacy = PrivacyLevel.UserName)]
public string AuthUsername { get; set; }
[FieldDefinition(7, Label = "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));
}
}
}