Add Notifiarr support (#52)
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Consumers;
|
||||
|
||||
public sealed class NotificationConsumer<T> : IConsumer<T> where T : Notification
|
||||
{
|
||||
private readonly ILogger<NotificationConsumer<T>> _logger;
|
||||
private readonly NotificationService _notificationService;
|
||||
|
||||
public NotificationConsumer(ILogger<NotificationConsumer<T>> logger, NotificationService notificationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_notificationService = notificationService;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<T> context)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (context.Message)
|
||||
{
|
||||
case FailedImportStrikeNotification failedMessage:
|
||||
await _notificationService.Notify(failedMessage);
|
||||
break;
|
||||
case StalledStrikeNotification stalledMessage:
|
||||
await _notificationService.Notify(stalledMessage);
|
||||
break;
|
||||
case QueueItemDeleteNotification queueItemDeleteMessage:
|
||||
await _notificationService.Notify(queueItemDeleteMessage);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// prevent spamming
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "error while processing notifications");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Infrastructure.Verticals.Notifications;
|
||||
|
||||
public interface INotificationFactory
|
||||
{
|
||||
List<INotificationProvider> OnFailedImportStrikeEnabled();
|
||||
|
||||
List<INotificationProvider> OnStalledStrikeEnabled();
|
||||
|
||||
List<INotificationProvider> OnQueueItemDeleteEnabled();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Common.Configuration.Notification;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications;
|
||||
|
||||
public interface INotificationProvider
|
||||
{
|
||||
NotificationConfig Config { get; }
|
||||
|
||||
string Name { get; }
|
||||
|
||||
Task OnFailedImportStrike(FailedImportStrikeNotification notification);
|
||||
|
||||
Task OnStalledStrike(StalledStrikeNotification notification);
|
||||
|
||||
Task OnQueueItemDelete(QueueItemDeleteNotification notification);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
public sealed record FailedImportStrikeNotification : Notification
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Domain.Enums;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
public record Notification
|
||||
{
|
||||
public required InstanceType InstanceType { get; init; }
|
||||
|
||||
public required Uri InstanceUrl { get; init; }
|
||||
|
||||
public required string Hash { get; init; }
|
||||
|
||||
public required string Title { get; init; }
|
||||
|
||||
public required string Description { get; init; }
|
||||
|
||||
public Uri? Image { get; init; }
|
||||
|
||||
public List<NotificationField>? Fields { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
public sealed record NotificationField
|
||||
{
|
||||
public required string Title { get; init; }
|
||||
|
||||
public required string Text { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
public sealed record QueueItemDeleteNotification : Notification
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Models;
|
||||
|
||||
public sealed record StalledStrikeNotification : Notification
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Notifiarr;
|
||||
|
||||
public interface INotifiarrProxy
|
||||
{
|
||||
Task SendNotification(NotifiarrPayload payload, NotifiarrConfig config);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Common.Configuration.Notification;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Notifiarr;
|
||||
|
||||
public sealed record NotifiarrConfig : NotificationConfig
|
||||
{
|
||||
public const string SectionName = "Notifiarr";
|
||||
|
||||
[ConfigurationKeyName("API_KEY")]
|
||||
public string? ApiKey { get; init; }
|
||||
|
||||
[ConfigurationKeyName("CHANNEL_ID")]
|
||||
public string? ChannelId { get; init; }
|
||||
|
||||
public override bool IsValid()
|
||||
{
|
||||
if (string.IsNullOrEmpty(ApiKey?.Trim()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ChannelId?.Trim()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Notifiarr;
|
||||
|
||||
public class NotifiarrException : Exception
|
||||
{
|
||||
public NotifiarrException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public NotifiarrException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace Infrastructure.Verticals.Notifications.Notifiarr;
|
||||
|
||||
public class NotifiarrPayload
|
||||
{
|
||||
public NotifiarrNotification Notification { get; set; } = new NotifiarrNotification();
|
||||
public Discord Discord { get; set; }
|
||||
}
|
||||
|
||||
public class NotifiarrNotification
|
||||
{
|
||||
public bool Update { get; set; }
|
||||
public string Name => "Cleanuperr";
|
||||
public int? Event { get; set; }
|
||||
}
|
||||
|
||||
public class Discord
|
||||
{
|
||||
public string Color { get; set; } = string.Empty;
|
||||
public Ping Ping { get; set; }
|
||||
public Images Images { get; set; }
|
||||
public Text Text { get; set; }
|
||||
public Ids Ids { get; set; }
|
||||
}
|
||||
|
||||
public class Ping
|
||||
{
|
||||
public string PingUser { get; set; }
|
||||
public string PingRole { get; set; }
|
||||
}
|
||||
|
||||
public class Images
|
||||
{
|
||||
public Uri? Thumbnail { get; set; }
|
||||
public Uri? Image { get; set; }
|
||||
}
|
||||
|
||||
public class Text
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<Field> Fields { get; set; } = new List<Field>();
|
||||
public string Footer { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class Field
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public bool Inline { get; set; }
|
||||
}
|
||||
|
||||
public class Ids
|
||||
{
|
||||
public string Channel { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Mapster;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Notifiarr;
|
||||
|
||||
public class NotifiarrProvider : NotificationProvider
|
||||
{
|
||||
private readonly NotifiarrConfig _config;
|
||||
private readonly INotifiarrProxy _proxy;
|
||||
|
||||
private const string WarningColor = "f0ad4e";
|
||||
private const string ImportantColor = "bb2124";
|
||||
|
||||
public NotifiarrProvider(IOptions<NotifiarrConfig> config, INotifiarrProxy proxy)
|
||||
: base(config)
|
||||
{
|
||||
_config = config.Value;
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
public override string Name => "Notifiarr";
|
||||
|
||||
public override async Task OnFailedImportStrike(FailedImportStrikeNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, WarningColor), _config);
|
||||
}
|
||||
|
||||
public override async Task OnStalledStrike(StalledStrikeNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, WarningColor), _config);
|
||||
}
|
||||
|
||||
public override async Task OnQueueItemDelete(QueueItemDeleteNotification notification)
|
||||
{
|
||||
await _proxy.SendNotification(BuildPayload(notification, ImportantColor), _config);
|
||||
}
|
||||
|
||||
private NotifiarrPayload BuildPayload(Notification notification, string color)
|
||||
{
|
||||
NotifiarrPayload payload = new()
|
||||
{
|
||||
Discord = new()
|
||||
{
|
||||
Color = color,
|
||||
Text = new()
|
||||
{
|
||||
Title = notification.Title,
|
||||
Icon = "https://github.com/flmorg/cleanuperr/blob/main/Logo/48.png?raw=true",
|
||||
Description = notification.Description,
|
||||
Fields = new()
|
||||
{
|
||||
new() { Title = "Instance type", Text = notification.InstanceType.ToString() },
|
||||
new() { Title = "Url", Text = notification.InstanceUrl.ToString() },
|
||||
new() { Title = "Download hash", Text = notification.Hash }
|
||||
}
|
||||
},
|
||||
Ids = new Ids
|
||||
{
|
||||
Channel = _config.ChannelId
|
||||
},
|
||||
Images = new()
|
||||
{
|
||||
Thumbnail = new Uri("https://github.com/flmorg/cleanuperr/blob/main/Logo/48.png?raw=true"),
|
||||
Image = notification.Image
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
payload.Discord.Text.Fields.AddRange(notification.Fields?.Adapt<List<Field>>() ?? []);
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Text;
|
||||
using Common.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications.Notifiarr;
|
||||
|
||||
public class NotifiarrProxy : INotifiarrProxy
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private const string Url = "https://notifiarr.com/api/v1/notification/passthrough/";
|
||||
|
||||
public NotifiarrProxy(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
|
||||
}
|
||||
|
||||
public async Task SendNotification(NotifiarrPayload payload, NotifiarrConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
});
|
||||
|
||||
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"{Url}{config.ApiKey}");
|
||||
request.Method = HttpMethod.Post;
|
||||
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
if (exception.StatusCode is null)
|
||||
{
|
||||
throw new NotifiarrException("unable to send notification", exception);
|
||||
}
|
||||
|
||||
switch ((int)exception.StatusCode)
|
||||
{
|
||||
case 401:
|
||||
throw new NotifiarrException("unable to send notification | API key is invalid");
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
throw new NotifiarrException("unable to send notification | service unavailable", exception);
|
||||
default:
|
||||
throw new NotifiarrException("unable to send notification", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Infrastructure.Verticals.Notifications;
|
||||
|
||||
public class NotificationFactory : INotificationFactory
|
||||
{
|
||||
private readonly IEnumerable<INotificationProvider> _providers;
|
||||
|
||||
public NotificationFactory(IEnumerable<INotificationProvider> providers)
|
||||
{
|
||||
_providers = providers;
|
||||
}
|
||||
|
||||
protected List<INotificationProvider> ActiveProviders() =>
|
||||
_providers
|
||||
.Where(x => x.Config.IsValid())
|
||||
.Where(provider => provider.Config.IsEnabled)
|
||||
.ToList();
|
||||
|
||||
public List<INotificationProvider> OnFailedImportStrikeEnabled() =>
|
||||
ActiveProviders()
|
||||
.Where(n => n.Config.OnImportFailedStrike)
|
||||
.ToList();
|
||||
|
||||
public List<INotificationProvider> OnStalledStrikeEnabled() =>
|
||||
ActiveProviders()
|
||||
.Where(n => n.Config.OnStalledStrike)
|
||||
.ToList();
|
||||
|
||||
public List<INotificationProvider> OnQueueItemDeleteEnabled() =>
|
||||
ActiveProviders()
|
||||
.Where(n => n.Config.OnQueueItemDelete)
|
||||
.ToList();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Common.Configuration.Notification;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications;
|
||||
|
||||
public abstract class NotificationProvider : INotificationProvider
|
||||
{
|
||||
protected NotificationProvider(IOptions<NotificationConfig> config)
|
||||
{
|
||||
Config = config.Value;
|
||||
}
|
||||
|
||||
public abstract string Name { get; }
|
||||
|
||||
public NotificationConfig Config { get; }
|
||||
|
||||
public abstract Task OnFailedImportStrike(FailedImportStrikeNotification notification);
|
||||
|
||||
public abstract Task OnStalledStrike(StalledStrikeNotification notification);
|
||||
|
||||
public abstract Task OnQueueItemDelete(QueueItemDeleteNotification notification);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using Common.Configuration.Arr;
|
||||
using Domain.Enums;
|
||||
using Domain.Models.Arr.Queue;
|
||||
using Infrastructure.Verticals.Context;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Mapster;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications;
|
||||
|
||||
public sealed class NotificationPublisher
|
||||
{
|
||||
private readonly ILogger<NotificationPublisher> _logger;
|
||||
private readonly IBus _messageBus;
|
||||
|
||||
public NotificationPublisher(ILogger<NotificationPublisher> logger, IBus messageBus)
|
||||
{
|
||||
_logger = logger;
|
||||
_messageBus = messageBus;
|
||||
}
|
||||
|
||||
public async Task NotifyStrike(StrikeType strikeType, int strikeCount)
|
||||
{
|
||||
try
|
||||
{
|
||||
QueueRecord record = GetRecordFromContext();
|
||||
InstanceType instanceType = GetInstanceTypeFromContext();
|
||||
Uri instanceUrl = GetInstanceUrlFromContext();
|
||||
Uri? imageUrl = GetImageFromContext(record, instanceType);
|
||||
|
||||
Notification notification = new()
|
||||
{
|
||||
InstanceType = instanceType,
|
||||
InstanceUrl = instanceUrl,
|
||||
Hash = record.DownloadId.ToLowerInvariant(),
|
||||
Title = $"Strike received with reason: {strikeType}",
|
||||
Description = record.Title,
|
||||
Image = imageUrl,
|
||||
Fields = [new() { Title = "Strike count", Text = strikeCount.ToString() }]
|
||||
};
|
||||
|
||||
switch (strikeType)
|
||||
{
|
||||
case StrikeType.Stalled:
|
||||
await _messageBus.Publish(notification.Adapt<StalledStrikeNotification>());
|
||||
break;
|
||||
case StrikeType.ImportFailed:
|
||||
await _messageBus.Publish(notification.Adapt<FailedImportStrikeNotification>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "failed to notify strike");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task NotifyQueueItemDelete(bool removeFromClient, DeleteReason reason)
|
||||
{
|
||||
QueueRecord record = GetRecordFromContext();
|
||||
InstanceType instanceType = GetInstanceTypeFromContext();
|
||||
Uri instanceUrl = GetInstanceUrlFromContext();
|
||||
Uri? imageUrl = GetImageFromContext(record, instanceType);
|
||||
|
||||
Notification notification = new()
|
||||
{
|
||||
InstanceType = instanceType,
|
||||
InstanceUrl = instanceUrl,
|
||||
Hash = record.DownloadId.ToLowerInvariant(),
|
||||
Title = $"Deleting item from queue with reason: {reason}",
|
||||
Description = record.Title,
|
||||
Image = imageUrl,
|
||||
Fields = [new() { Title = "Removed from download client?", Text = removeFromClient ? "Yes" : "No" }]
|
||||
};
|
||||
|
||||
await _messageBus.Publish(notification.Adapt<QueueItemDeleteNotification>());
|
||||
}
|
||||
|
||||
private static QueueRecord GetRecordFromContext() =>
|
||||
ContextProvider.Get<QueueRecord>(nameof(QueueRecord)) ?? throw new Exception("failed to get record from context");
|
||||
|
||||
private static InstanceType GetInstanceTypeFromContext() =>
|
||||
(InstanceType)(ContextProvider.Get<object>(nameof(InstanceType)) ??
|
||||
throw new Exception("failed to get instance type from context"));
|
||||
|
||||
private static Uri GetInstanceUrlFromContext() =>
|
||||
ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url)) ??
|
||||
throw new Exception("failed to get instance url from context");
|
||||
|
||||
private static Uri GetImageFromContext(QueueRecord record, InstanceType instanceType) =>
|
||||
instanceType switch
|
||||
{
|
||||
InstanceType.Sonarr => record.Series!.Images.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Radarr => record.Movie!.Images.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Lidarr => record.Album!.Images.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(instanceType))
|
||||
} ?? throw new Exception("failed to get image url from context");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.Notifications;
|
||||
|
||||
public class NotificationService
|
||||
{
|
||||
private readonly ILogger<NotificationService> _logger;
|
||||
private readonly INotificationFactory _notificationFactory;
|
||||
|
||||
public NotificationService(ILogger<NotificationService> logger, INotificationFactory notificationFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_notificationFactory = notificationFactory;
|
||||
}
|
||||
|
||||
public async Task Notify(FailedImportStrikeNotification notification)
|
||||
{
|
||||
foreach (INotificationProvider provider in _notificationFactory.OnFailedImportStrikeEnabled())
|
||||
{
|
||||
try
|
||||
{
|
||||
await provider.OnFailedImportStrike(notification);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "failed to send notification | provider {provider}", provider.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Notify(StalledStrikeNotification notification)
|
||||
{
|
||||
foreach (INotificationProvider provider in _notificationFactory.OnStalledStrikeEnabled())
|
||||
{
|
||||
try
|
||||
{
|
||||
await provider.OnStalledStrike(notification);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "failed to send notification | provider {provider}", provider.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Notify(QueueItemDeleteNotification notification)
|
||||
{
|
||||
foreach (INotificationProvider provider in _notificationFactory.OnQueueItemDeleteEnabled())
|
||||
{
|
||||
try
|
||||
{
|
||||
await provider.OnQueueItemDelete(notification);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "failed to send notification | provider {provider}", provider.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user