mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-23 22:18:39 -05:00
Add ntfy support (#300)
This commit is contained in:
@@ -405,6 +405,7 @@ public class ConfigurationController : ControllerBase
|
||||
var providers = await _dataContext.NotificationConfigs
|
||||
.Include(p => p.NotifiarrConfiguration)
|
||||
.Include(p => p.AppriseConfiguration)
|
||||
.Include(p => p.NtfyConfiguration)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
@@ -428,6 +429,7 @@ public class ConfigurationController : ControllerBase
|
||||
{
|
||||
NotificationProviderType.Notifiarr => p.NotifiarrConfiguration ?? new object(),
|
||||
NotificationProviderType.Apprise => p.AppriseConfiguration ?? new object(),
|
||||
NotificationProviderType.Ntfy => p.NtfyConfiguration ?? new object(),
|
||||
_ => new object()
|
||||
}
|
||||
})
|
||||
@@ -846,6 +848,7 @@ public class ConfigurationController : ControllerBase
|
||||
var existingProvider = await _dataContext.NotificationConfigs
|
||||
.Include(p => p.NotifiarrConfiguration)
|
||||
.Include(p => p.AppriseConfiguration)
|
||||
.Include(p => p.NtfyConfiguration)
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
if (existingProvider == null)
|
||||
@@ -970,6 +973,266 @@ public class ConfigurationController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("notification_providers/ntfy")]
|
||||
public async Task<IActionResult> CreateNtfyProvider([FromBody] CreateNtfyProviderDto newProvider)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Validate required fields
|
||||
if (string.IsNullOrWhiteSpace(newProvider.Name))
|
||||
{
|
||||
return BadRequest("Provider name is required");
|
||||
}
|
||||
|
||||
var duplicateConfig = await _dataContext.NotificationConfigs.CountAsync(x => x.Name == newProvider.Name);
|
||||
|
||||
if (duplicateConfig > 0)
|
||||
{
|
||||
return BadRequest("A provider with this name already exists");
|
||||
}
|
||||
|
||||
// Create provider-specific configuration with validation
|
||||
var ntfyConfig = new NtfyConfig
|
||||
{
|
||||
ServerUrl = newProvider.ServerUrl,
|
||||
Topics = newProvider.Topics,
|
||||
AuthenticationType = newProvider.AuthenticationType,
|
||||
Username = newProvider.Username,
|
||||
Password = newProvider.Password,
|
||||
AccessToken = newProvider.AccessToken,
|
||||
Priority = newProvider.Priority,
|
||||
Tags = newProvider.Tags
|
||||
};
|
||||
|
||||
// Validate the configuration
|
||||
ntfyConfig.Validate();
|
||||
|
||||
// Create the provider entity
|
||||
var provider = new NotificationConfig
|
||||
{
|
||||
Name = newProvider.Name,
|
||||
Type = NotificationProviderType.Ntfy,
|
||||
IsEnabled = newProvider.IsEnabled,
|
||||
OnFailedImportStrike = newProvider.OnFailedImportStrike,
|
||||
OnStalledStrike = newProvider.OnStalledStrike,
|
||||
OnSlowStrike = newProvider.OnSlowStrike,
|
||||
OnQueueItemDeleted = newProvider.OnQueueItemDeleted,
|
||||
OnDownloadCleaned = newProvider.OnDownloadCleaned,
|
||||
OnCategoryChanged = newProvider.OnCategoryChanged,
|
||||
NtfyConfiguration = ntfyConfig
|
||||
};
|
||||
|
||||
// Add the new provider to the database
|
||||
_dataContext.NotificationConfigs.Add(provider);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
// Clear cache to ensure fresh data on next request
|
||||
await _notificationConfigurationService.InvalidateCacheAsync();
|
||||
|
||||
// Return the provider in DTO format to match frontend expectations
|
||||
var providerDto = new NotificationProviderDto
|
||||
{
|
||||
Id = provider.Id,
|
||||
Name = provider.Name,
|
||||
Type = provider.Type,
|
||||
IsEnabled = provider.IsEnabled,
|
||||
Events = new NotificationEventFlags
|
||||
{
|
||||
OnFailedImportStrike = provider.OnFailedImportStrike,
|
||||
OnStalledStrike = provider.OnStalledStrike,
|
||||
OnSlowStrike = provider.OnSlowStrike,
|
||||
OnQueueItemDeleted = provider.OnQueueItemDeleted,
|
||||
OnDownloadCleaned = provider.OnDownloadCleaned,
|
||||
OnCategoryChanged = provider.OnCategoryChanged
|
||||
},
|
||||
Configuration = provider.NtfyConfiguration ?? new object()
|
||||
};
|
||||
|
||||
return CreatedAtAction(nameof(GetNotificationProviders), new { id = provider.Id }, providerDto);
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create Ntfy provider");
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("notification_providers/ntfy/{id}")]
|
||||
public async Task<IActionResult> UpdateNtfyProvider(Guid id, [FromBody] UpdateNtfyProviderDto updatedProvider)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Find the existing notification provider
|
||||
var existingProvider = await _dataContext.NotificationConfigs
|
||||
.Include(p => p.NtfyConfiguration)
|
||||
.FirstOrDefaultAsync(p => p.Id == id && p.Type == NotificationProviderType.Ntfy);
|
||||
|
||||
if (existingProvider == null)
|
||||
{
|
||||
return NotFound($"Ntfy provider with ID {id} not found");
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (string.IsNullOrWhiteSpace(updatedProvider.Name))
|
||||
{
|
||||
return BadRequest("Provider name is required");
|
||||
}
|
||||
|
||||
var duplicateConfig = await _dataContext.NotificationConfigs
|
||||
.Where(x => x.Id != id)
|
||||
.Where(x => x.Name == updatedProvider.Name)
|
||||
.CountAsync();
|
||||
|
||||
if (duplicateConfig > 0)
|
||||
{
|
||||
return BadRequest("A provider with this name already exists");
|
||||
}
|
||||
|
||||
// Create provider-specific configuration with validation
|
||||
var ntfyConfig = new NtfyConfig
|
||||
{
|
||||
ServerUrl = updatedProvider.ServerUrl,
|
||||
Topics = updatedProvider.Topics,
|
||||
AuthenticationType = updatedProvider.AuthenticationType,
|
||||
Username = updatedProvider.Username,
|
||||
Password = updatedProvider.Password,
|
||||
AccessToken = updatedProvider.AccessToken,
|
||||
Priority = updatedProvider.Priority,
|
||||
Tags = updatedProvider.Tags
|
||||
};
|
||||
|
||||
// Preserve the existing ID if updating
|
||||
if (existingProvider.NtfyConfiguration != null)
|
||||
{
|
||||
ntfyConfig = ntfyConfig with { Id = existingProvider.NtfyConfiguration.Id };
|
||||
}
|
||||
|
||||
// Validate the configuration
|
||||
ntfyConfig.Validate();
|
||||
|
||||
// Create a new provider entity with updated values (records are immutable)
|
||||
var newProvider = existingProvider with
|
||||
{
|
||||
Name = updatedProvider.Name,
|
||||
IsEnabled = updatedProvider.IsEnabled,
|
||||
OnFailedImportStrike = updatedProvider.OnFailedImportStrike,
|
||||
OnStalledStrike = updatedProvider.OnStalledStrike,
|
||||
OnSlowStrike = updatedProvider.OnSlowStrike,
|
||||
OnQueueItemDeleted = updatedProvider.OnQueueItemDeleted,
|
||||
OnDownloadCleaned = updatedProvider.OnDownloadCleaned,
|
||||
OnCategoryChanged = updatedProvider.OnCategoryChanged,
|
||||
NtfyConfiguration = ntfyConfig,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Remove old and add new (EF handles this as an update)
|
||||
_dataContext.NotificationConfigs.Remove(existingProvider);
|
||||
_dataContext.NotificationConfigs.Add(newProvider);
|
||||
|
||||
// Persist the configuration
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
// Clear cache to ensure fresh data on next request
|
||||
await _notificationConfigurationService.InvalidateCacheAsync();
|
||||
|
||||
// Return the provider in DTO format to match frontend expectations
|
||||
var providerDto = new NotificationProviderDto
|
||||
{
|
||||
Id = newProvider.Id,
|
||||
Name = newProvider.Name,
|
||||
Type = newProvider.Type,
|
||||
IsEnabled = newProvider.IsEnabled,
|
||||
Events = new NotificationEventFlags
|
||||
{
|
||||
OnFailedImportStrike = newProvider.OnFailedImportStrike,
|
||||
OnStalledStrike = newProvider.OnStalledStrike,
|
||||
OnSlowStrike = newProvider.OnSlowStrike,
|
||||
OnQueueItemDeleted = newProvider.OnQueueItemDeleted,
|
||||
OnDownloadCleaned = newProvider.OnDownloadCleaned,
|
||||
OnCategoryChanged = newProvider.OnCategoryChanged
|
||||
},
|
||||
Configuration = newProvider.NtfyConfiguration ?? new object()
|
||||
};
|
||||
|
||||
return Ok(providerDto);
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update Ntfy provider with ID {Id}", id);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("notification_providers/ntfy/test")]
|
||||
public async Task<IActionResult> TestNtfyProvider([FromBody] TestNtfyProviderDto testRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create configuration for testing with validation
|
||||
var ntfyConfig = new NtfyConfig
|
||||
{
|
||||
ServerUrl = testRequest.ServerUrl,
|
||||
Topics = testRequest.Topics,
|
||||
AuthenticationType = testRequest.AuthenticationType,
|
||||
Username = testRequest.Username,
|
||||
Password = testRequest.Password,
|
||||
AccessToken = testRequest.AccessToken,
|
||||
Priority = testRequest.Priority,
|
||||
Tags = testRequest.Tags
|
||||
};
|
||||
|
||||
// Validate the configuration
|
||||
ntfyConfig.Validate();
|
||||
|
||||
// Create a temporary provider DTO for the test service
|
||||
var providerDto = new NotificationProviderDto
|
||||
{
|
||||
Id = Guid.NewGuid(), // Temporary ID for testing
|
||||
Name = "Test Provider",
|
||||
Type = NotificationProviderType.Ntfy,
|
||||
IsEnabled = true,
|
||||
Events = new NotificationEventFlags
|
||||
{
|
||||
OnFailedImportStrike = true, // Enable for test
|
||||
OnStalledStrike = false,
|
||||
OnSlowStrike = false,
|
||||
OnQueueItemDeleted = false,
|
||||
OnDownloadCleaned = false,
|
||||
OnCategoryChanged = false
|
||||
},
|
||||
Configuration = ntfyConfig
|
||||
};
|
||||
|
||||
// Test the notification provider
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Ntfy provider");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("queue_cleaner")]
|
||||
public async Task<IActionResult> UpdateQueueCleanerConfig([FromBody] QueueCleanerConfig newConfig)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Notifiarr;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
|
||||
|
||||
namespace Cleanuparr.Api.DependencyInjection;
|
||||
|
||||
@@ -10,6 +11,7 @@ public static class NotificationsDI
|
||||
services
|
||||
.AddScoped<INotifiarrProxy, NotifiarrProxy>()
|
||||
.AddScoped<IAppriseProxy, AppriseProxy>()
|
||||
.AddScoped<INtfyProxy, NtfyProxy>()
|
||||
.AddScoped<INotificationConfigurationService, NotificationConfigurationService>()
|
||||
.AddScoped<INotificationProviderFactory, NotificationProviderFactory>()
|
||||
.AddScoped<NotificationProviderFactory>()
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||
|
||||
public sealed record CreateNtfyProviderDto : CreateNotificationProviderBaseDto
|
||||
{
|
||||
public string ServerUrl { get; init; } = string.Empty;
|
||||
|
||||
public List<string> Topics { get; init; } = [];
|
||||
|
||||
public NtfyAuthenticationType AuthenticationType { get; init; } = NtfyAuthenticationType.None;
|
||||
|
||||
public string Username { get; init; } = string.Empty;
|
||||
|
||||
public string Password { get; init; } = string.Empty;
|
||||
|
||||
public string AccessToken { get; init; } = string.Empty;
|
||||
|
||||
public NtfyPriority Priority { get; init; } = NtfyPriority.Default;
|
||||
|
||||
public List<string> Tags { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||
|
||||
public sealed record TestNtfyProviderDto
|
||||
{
|
||||
public string ServerUrl { get; init; } = string.Empty;
|
||||
|
||||
public List<string> Topics { get; init; } = [];
|
||||
|
||||
public NtfyAuthenticationType AuthenticationType { get; init; } = NtfyAuthenticationType.None;
|
||||
|
||||
public string Username { get; init; } = string.Empty;
|
||||
|
||||
public string Password { get; init; } = string.Empty;
|
||||
|
||||
public string AccessToken { get; init; } = string.Empty;
|
||||
|
||||
public NtfyPriority Priority { get; init; } = NtfyPriority.Default;
|
||||
|
||||
public List<string> Tags { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Api.Models.NotificationProviders;
|
||||
|
||||
public sealed record UpdateNtfyProviderDto : CreateNotificationProviderBaseDto
|
||||
{
|
||||
public string ServerUrl { get; init; } = string.Empty;
|
||||
|
||||
public List<string> Topics { get; init; } = [];
|
||||
|
||||
public NtfyAuthenticationType AuthenticationType { get; init; } = NtfyAuthenticationType.None;
|
||||
|
||||
public string Username { get; init; } = string.Empty;
|
||||
|
||||
public string Password { get; init; } = string.Empty;
|
||||
|
||||
public string AccessToken { get; init; } = string.Empty;
|
||||
|
||||
public NtfyPriority Priority { get; init; } = NtfyPriority.Default;
|
||||
|
||||
public List<string> Tags { get; init; } = [];
|
||||
}
|
||||
@@ -3,5 +3,6 @@ namespace Cleanuparr.Domain.Enums;
|
||||
public enum NotificationProviderType
|
||||
{
|
||||
Notifiarr,
|
||||
Apprise
|
||||
Apprise,
|
||||
Ntfy
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Cleanuparr.Domain.Enums;
|
||||
|
||||
public enum NtfyAuthenticationType
|
||||
{
|
||||
None,
|
||||
BasicAuth,
|
||||
AccessToken
|
||||
}
|
||||
10
code/backend/Cleanuparr.Domain/Enums/NtfyPriority.cs
Normal file
10
code/backend/Cleanuparr.Domain/Enums/NtfyPriority.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Cleanuparr.Domain.Enums;
|
||||
|
||||
public enum NtfyPriority
|
||||
{
|
||||
Min = 1,
|
||||
Low = 2,
|
||||
Default = 3,
|
||||
High = 4,
|
||||
Max = 5
|
||||
}
|
||||
@@ -85,6 +85,7 @@ public sealed class NotificationConfigurationService : INotificationConfiguratio
|
||||
var providers = await _dataContext.Set<NotificationConfig>()
|
||||
.Include(p => p.NotifiarrConfiguration)
|
||||
.Include(p => p.AppriseConfiguration)
|
||||
.Include(p => p.NtfyConfiguration)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
@@ -133,6 +134,7 @@ public sealed class NotificationConfigurationService : INotificationConfiguratio
|
||||
{
|
||||
NotificationProviderType.Notifiarr => config.NotifiarrConfiguration,
|
||||
NotificationProviderType.Apprise => config.AppriseConfiguration,
|
||||
NotificationProviderType.Ntfy => config.NtfyConfiguration,
|
||||
_ => new object()
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Notifiarr;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -22,6 +23,7 @@ public sealed class NotificationProviderFactory : INotificationProviderFactory
|
||||
{
|
||||
NotificationProviderType.Notifiarr => CreateNotifiarrProvider(config),
|
||||
NotificationProviderType.Apprise => CreateAppriseProvider(config),
|
||||
NotificationProviderType.Ntfy => CreateNtfyProvider(config),
|
||||
_ => throw new NotSupportedException($"Provider type {config.Type} is not supported")
|
||||
};
|
||||
}
|
||||
@@ -41,4 +43,12 @@ public sealed class NotificationProviderFactory : INotificationProviderFactory
|
||||
|
||||
return new AppriseProvider(config.Name, config.Type, appriseConfig, proxy);
|
||||
}
|
||||
|
||||
private INotificationProvider CreateNtfyProvider(NotificationProviderDto config)
|
||||
{
|
||||
var ntfyConfig = (NtfyConfig)config.Configuration;
|
||||
var proxy = _serviceProvider.GetRequiredService<INtfyProxy>();
|
||||
|
||||
return new NtfyProvider(config.Name, config.Type, ntfyConfig, proxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
|
||||
|
||||
public interface INtfyProxy
|
||||
{
|
||||
Task SendNotification(NtfyPayload payload, NtfyConfig config);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
|
||||
|
||||
public sealed class NtfyException : Exception
|
||||
{
|
||||
public NtfyException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public NtfyException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
|
||||
|
||||
public sealed class NtfyPayload
|
||||
{
|
||||
[JsonProperty("topic")]
|
||||
public string Topic { get; init; } = string.Empty;
|
||||
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string? Title { get; init; }
|
||||
|
||||
[JsonProperty("priority")]
|
||||
public int? Priority { get; init; }
|
||||
|
||||
[JsonProperty("tags")]
|
||||
public string[]? Tags { get; init; }
|
||||
|
||||
[JsonProperty("click")]
|
||||
public string? Click { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Text;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
|
||||
|
||||
public sealed class NtfyProvider : NotificationProviderBase<NtfyConfig>
|
||||
{
|
||||
private readonly INtfyProxy _proxy;
|
||||
|
||||
public NtfyProvider(
|
||||
string name,
|
||||
NotificationProviderType type,
|
||||
NtfyConfig config,
|
||||
INtfyProxy proxy
|
||||
) : base(name, type, config)
|
||||
{
|
||||
_proxy = proxy;
|
||||
}
|
||||
|
||||
public override async Task SendNotificationAsync(NotificationContext context)
|
||||
{
|
||||
var topics = GetTopics();
|
||||
var tasks = topics.Select(topic => SendToTopic(topic, context));
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task SendToTopic(string topic, NotificationContext context)
|
||||
{
|
||||
NtfyPayload payload = BuildPayload(topic, context);
|
||||
await _proxy.SendNotification(payload, Config);
|
||||
}
|
||||
|
||||
private NtfyPayload BuildPayload(string topic, NotificationContext context)
|
||||
{
|
||||
int priority = MapSeverityToPriority(context.Severity);
|
||||
string message = BuildMessage(context);
|
||||
|
||||
return new NtfyPayload
|
||||
{
|
||||
Topic = topic.Trim(),
|
||||
Title = context.Title,
|
||||
Message = message,
|
||||
Priority = priority,
|
||||
Tags = Config.Tags.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildMessage(NotificationContext context)
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.AppendLine(context.Description);
|
||||
|
||||
if (context.Data.Any())
|
||||
{
|
||||
message.AppendLine();
|
||||
foreach ((string key, string value) in context.Data)
|
||||
{
|
||||
message.AppendLine($"{key}: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
return message.ToString().Trim();
|
||||
}
|
||||
|
||||
private int MapSeverityToPriority(EventSeverity severity)
|
||||
{
|
||||
return severity switch
|
||||
{
|
||||
EventSeverity.Information => (int)Config.Priority,
|
||||
EventSeverity.Warning => Math.Max((int)Config.Priority, (int)NtfyPriority.High),
|
||||
EventSeverity.Important => (int)NtfyPriority.Max,
|
||||
_ => (int)Config.Priority
|
||||
};
|
||||
}
|
||||
|
||||
private string[] GetTopics()
|
||||
{
|
||||
return Config.Topics
|
||||
.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||
.Select(t => t.Trim())
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
|
||||
|
||||
public sealed class NtfyProxy : INtfyProxy
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public NtfyProxy(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
|
||||
}
|
||||
|
||||
public async Task SendNotification(NtfyPayload payload, NtfyConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
|
||||
var parsedUrl = config.Uri!;
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, parsedUrl);
|
||||
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
|
||||
|
||||
// Set authentication headers based on configuration
|
||||
SetAuthenticationHeaders(request, config);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
if (exception.StatusCode is null)
|
||||
{
|
||||
throw new NtfyException("Unable to send notification", exception);
|
||||
}
|
||||
|
||||
switch ((int)exception.StatusCode)
|
||||
{
|
||||
case 400:
|
||||
throw new NtfyException("Bad request - invalid topic or payload", exception);
|
||||
case 401:
|
||||
throw new NtfyException("Unauthorized - invalid credentials", exception);
|
||||
case 413:
|
||||
throw new NtfyException("Payload too large", exception);
|
||||
case 429:
|
||||
throw new NtfyException("Rate limited - too many requests", exception);
|
||||
case 507:
|
||||
throw new NtfyException("Insufficient storage on server", exception);
|
||||
default:
|
||||
throw new NtfyException("Unable to send notification", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetAuthenticationHeaders(HttpRequestMessage request, NtfyConfig config)
|
||||
{
|
||||
switch (config.AuthenticationType)
|
||||
{
|
||||
case NtfyAuthenticationType.BasicAuth:
|
||||
if (!string.IsNullOrWhiteSpace(config.Username) && !string.IsNullOrWhiteSpace(config.Password))
|
||||
{
|
||||
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{config.Username}:{config.Password}"));
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
|
||||
}
|
||||
break;
|
||||
|
||||
case NtfyAuthenticationType.AccessToken:
|
||||
if (!string.IsNullOrWhiteSpace(config.AccessToken))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", config.AccessToken);
|
||||
}
|
||||
break;
|
||||
|
||||
case NtfyAuthenticationType.None:
|
||||
default:
|
||||
// No authentication required
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,8 @@ public class DataContext : DbContext
|
||||
public DbSet<NotifiarrConfig> NotifiarrConfigs { get; set; }
|
||||
|
||||
public DbSet<AppriseConfig> AppriseConfigs { get; set; }
|
||||
|
||||
public DbSet<NtfyConfig> NtfyConfigs { get; set; }
|
||||
|
||||
public DbSet<BlacklistSyncHistory> BlacklistSyncHistory { get; set; }
|
||||
|
||||
@@ -129,6 +131,11 @@ public class DataContext : DbContext
|
||||
.HasForeignKey<AppriseConfig>(c => c.NotificationConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(p => p.NtfyConfiguration)
|
||||
.WithOne(c => c.NotificationConfig)
|
||||
.HasForeignKey<NtfyConfig>(c => c.NotificationConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasIndex(p => p.Name).IsUnique();
|
||||
});
|
||||
|
||||
|
||||
822
code/backend/Cleanuparr.Persistence/Migrations/Data/20250912234118_AddNtfy.Designer.cs
generated
Normal file
822
code/backend/Cleanuparr.Persistence/Migrations/Data/20250912234118_AddNtfy.Designer.cs
generated
Normal file
@@ -0,0 +1,822 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20250912234118_AddNtfy")]
|
||||
partial class AddNtfy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<short>("FailedImportMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_configs");
|
||||
|
||||
b.ToTable("arr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<Guid>("ArrConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_config_id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_instances");
|
||||
|
||||
b.HasIndex("ArrConfigId")
|
||||
.HasDatabaseName("ix_arr_instances_arr_config_id");
|
||||
|
||||
b.ToTable("arr_instances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("DownloadCleanerConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_cleaner_config_id");
|
||||
|
||||
b.Property<double>("MaxRatio")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_ratio");
|
||||
|
||||
b.Property<double>("MaxSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_seed_time");
|
||||
|
||||
b.Property<double>("MinSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("min_seed_time");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_clean_categories");
|
||||
|
||||
b.HasIndex("DownloadCleanerConfigId")
|
||||
.HasDatabaseName("ix_clean_categories_download_cleaner_config_id");
|
||||
|
||||
b.ToTable("clean_categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.PrimitiveCollection<string>("UnlinkedCategories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_categories");
|
||||
|
||||
b.Property<bool>("UnlinkedEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_enabled");
|
||||
|
||||
b.Property<string>("UnlinkedIgnoredRootDir")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_ignored_root_dir");
|
||||
|
||||
b.Property<string>("UnlinkedTargetCategory")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_target_category");
|
||||
|
||||
b.Property<bool>("UnlinkedUseTag")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_use_tag");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_cleaner_configs");
|
||||
|
||||
b.ToTable("download_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("host");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<string>("TypeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type_name");
|
||||
|
||||
b.Property<string>("UrlBase")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url_base");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_clients");
|
||||
|
||||
b.ToTable("download_clients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("DisplaySupportBanner")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("display_support_banner");
|
||||
|
||||
b.Property<bool>("DryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("dry_run");
|
||||
|
||||
b.Property<string>("EncryptionKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("encryption_key");
|
||||
|
||||
b.Property<string>("HttpCertificateValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("http_certificate_validation");
|
||||
|
||||
b.Property<ushort>("HttpMaxRetries")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_max_retries");
|
||||
|
||||
b.Property<ushort>("HttpTimeout")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_timeout");
|
||||
|
||||
b.PrimitiveCollection<string>("IgnoredDownloads")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ignored_downloads");
|
||||
|
||||
b.Property<ushort>("SearchDelay")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_delay");
|
||||
|
||||
b.Property<bool>("SearchEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_enabled");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Log", "Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig.Log#LoggingConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("ArchiveEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("log_archive_enabled");
|
||||
|
||||
b1.Property<ushort>("ArchiveRetainedCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("log_archive_retained_count");
|
||||
|
||||
b1.Property<ushort>("ArchiveTimeLimitHours")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("log_archive_time_limit_hours");
|
||||
|
||||
b1.Property<string>("Level")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_level");
|
||||
|
||||
b1.Property<ushort>("RetainedFileCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("log_retained_file_count");
|
||||
|
||||
b1.Property<ushort>("RollingSizeMB")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("log_rolling_size_mb");
|
||||
|
||||
b1.Property<ushort>("TimeLimitHours")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("log_time_limit_hours");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_general_configs");
|
||||
|
||||
b.ToTable("general_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeleteKnownMalware")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_known_malware");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ignore_private");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Lidarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Lidarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("lidarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Radarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Radarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("radarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Readarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Readarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("readarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Sonarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Sonarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sonarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Whisparr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Whisparr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("whisparr_blocklist_path");
|
||||
|
||||
b1.Property<int>("BlocklistType")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("whisparr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("whisparr_enabled");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_content_blocker_configs");
|
||||
|
||||
b.ToTable("content_blocker_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<Guid>("NotificationConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notification_config_id");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_apprise_configs");
|
||||
|
||||
b.HasIndex("NotificationConfigId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_apprise_configs_notification_config_id");
|
||||
|
||||
b.ToTable("apprise_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<string>("ChannelId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("channel_id");
|
||||
|
||||
b.Property<Guid>("NotificationConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notification_config_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifiarr_configs");
|
||||
|
||||
b.HasIndex("NotificationConfigId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_notifiarr_configs_notification_config_id");
|
||||
|
||||
b.ToTable("notifiarr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_enabled");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notification_configs");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_notification_configs_name");
|
||||
|
||||
b.ToTable("notification_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("access_token");
|
||||
|
||||
b.Property<string>("AuthenticationType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("authentication_type");
|
||||
|
||||
b.Property<Guid>("NotificationConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notification_config_id");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<string>("ServerUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("server_url");
|
||||
|
||||
b.PrimitiveCollection<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags");
|
||||
|
||||
b.PrimitiveCollection<string>("Topics")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("topics");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_ntfy_configs");
|
||||
|
||||
b.HasIndex("NotificationConfigId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_ntfy_configs_notification_config_id");
|
||||
|
||||
b.ToTable("ntfy_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("FailedImport", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.FailedImport#FailedImportConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_delete_private");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_ignore_private");
|
||||
|
||||
b1.PrimitiveCollection<string>("IgnoredPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("failed_import_ignored_patterns");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Slow", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Slow#SlowConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_delete_private");
|
||||
|
||||
b1.Property<string>("IgnoreAboveSize")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_ignore_above_size");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_max_strikes");
|
||||
|
||||
b1.Property<double>("MaxTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("slow_max_time");
|
||||
|
||||
b1.Property<string>("MinSpeed")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_min_speed");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Stalled", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Stalled#StalledConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_delete_private");
|
||||
|
||||
b1.Property<ushort>("DownloadingMetadataMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_downloading_metadata_max_strikes");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_max_strikes");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_queue_cleaner_configs");
|
||||
|
||||
b.ToTable("queue_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", "ArrConfig")
|
||||
.WithMany("Instances")
|
||||
.HasForeignKey("ArrConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_arr_instances_arr_configs_arr_config_id");
|
||||
|
||||
b.Navigation("ArrConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", "DownloadCleanerConfig")
|
||||
.WithMany("Categories")
|
||||
.HasForeignKey("DownloadCleanerConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_clean_categories_download_cleaner_configs_download_cleaner_config_id");
|
||||
|
||||
b.Navigation("DownloadCleanerConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig")
|
||||
.WithOne("AppriseConfiguration")
|
||||
.HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", "NotificationConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_apprise_configs_notification_configs_notification_config_id");
|
||||
|
||||
b.Navigation("NotificationConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig")
|
||||
.WithOne("NotifiarrConfiguration")
|
||||
.HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", "NotificationConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notifiarr_configs_notification_configs_notification_config_id");
|
||||
|
||||
b.Navigation("NotificationConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig")
|
||||
.WithOne("NtfyConfiguration")
|
||||
.HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", "NotificationConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_ntfy_configs_notification_configs_notification_config_id");
|
||||
|
||||
b.Navigation("NotificationConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Navigation("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", b =>
|
||||
{
|
||||
b.Navigation("AppriseConfiguration");
|
||||
|
||||
b.Navigation("NotifiarrConfiguration");
|
||||
|
||||
b.Navigation("NtfyConfiguration");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNtfy : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ntfy_configs",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
notification_config_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
server_url = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
topics = table.Column<string>(type: "TEXT", nullable: false),
|
||||
authentication_type = table.Column<string>(type: "TEXT", nullable: false),
|
||||
username = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
password = table.Column<string>(type: "TEXT", maxLength: 255, nullable: true),
|
||||
access_token = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
priority = table.Column<string>(type: "TEXT", nullable: false),
|
||||
tags = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_ntfy_configs", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_ntfy_configs_notification_configs_notification_config_id",
|
||||
column: x => x.notification_config_id,
|
||||
principalTable: "notification_configs",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_ntfy_configs_notification_config_id",
|
||||
table: "ntfy_configs",
|
||||
column: "notification_config_id",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ntfy_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cleanuparr.Persistence;
|
||||
@@ -601,6 +601,68 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
b.ToTable("notification_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("access_token");
|
||||
|
||||
b.Property<string>("AuthenticationType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("authentication_type");
|
||||
|
||||
b.Property<Guid>("NotificationConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notification_config_id");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<string>("ServerUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("server_url");
|
||||
|
||||
b.PrimitiveCollection<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags");
|
||||
|
||||
b.PrimitiveCollection<string>("Topics")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("topics");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_ntfy_configs");
|
||||
|
||||
b.HasIndex("NotificationConfigId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_ntfy_configs_notification_config_id");
|
||||
|
||||
b.ToTable("ntfy_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -806,6 +868,18 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
b.Navigation("DownloadClient");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig")
|
||||
.WithOne("NtfyConfiguration")
|
||||
.HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", "NotificationConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_ntfy_configs_notification_configs_notification_config_id");
|
||||
|
||||
b.Navigation("NotificationConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
@@ -821,6 +895,8 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
b.Navigation("AppriseConfiguration");
|
||||
|
||||
b.Navigation("NotifiarrConfiguration");
|
||||
|
||||
b.Navigation("NtfyConfiguration");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
||||
@@ -39,11 +39,14 @@ public sealed record NotificationConfig
|
||||
|
||||
public AppriseConfig? AppriseConfiguration { get; init; }
|
||||
|
||||
public NtfyConfig? NtfyConfiguration { get; init; }
|
||||
|
||||
[NotMapped]
|
||||
public bool IsConfigured => Type switch
|
||||
{
|
||||
NotificationProviderType.Notifiarr => NotifiarrConfiguration?.IsValid() == true,
|
||||
NotificationProviderType.Apprise => AppriseConfiguration?.IsValid() == true,
|
||||
NotificationProviderType.Ntfy => NtfyConfiguration?.IsValid() == true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
|
||||
public sealed record NtfyConfig : IConfig
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
public Guid NotificationConfigId { get; init; }
|
||||
|
||||
public NotificationConfig NotificationConfig { get; init; } = null!;
|
||||
|
||||
[Required]
|
||||
[MaxLength(500)]
|
||||
public string ServerUrl { get; init; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public List<string> Topics { get; init; } = new();
|
||||
|
||||
[Required]
|
||||
public NtfyAuthenticationType AuthenticationType { get; init; } = NtfyAuthenticationType.None;
|
||||
|
||||
[MaxLength(255)]
|
||||
public string? Username { get; init; }
|
||||
|
||||
[MaxLength(255)]
|
||||
public string? Password { get; init; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? AccessToken { get; init; }
|
||||
|
||||
[Required]
|
||||
public NtfyPriority Priority { get; init; } = NtfyPriority.Default;
|
||||
|
||||
public List<string> Tags { get; init; } = new();
|
||||
|
||||
[NotMapped]
|
||||
public Uri? Uri
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(ServerUrl) ? null : new Uri(ServerUrl, UriKind.Absolute);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Uri != null &&
|
||||
Topics.Any(t => !string.IsNullOrWhiteSpace(t)) &&
|
||||
IsAuthenticationValid();
|
||||
}
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ServerUrl))
|
||||
{
|
||||
throw new ValidationException("ntfy server URL is required");
|
||||
}
|
||||
|
||||
if (Uri == null)
|
||||
{
|
||||
throw new ValidationException("ntfy server URL must be a valid HTTP or HTTPS URL");
|
||||
}
|
||||
|
||||
if (!Topics.Any(t => !string.IsNullOrWhiteSpace(t)))
|
||||
{
|
||||
throw new ValidationException("At least one ntfy topic is required");
|
||||
}
|
||||
|
||||
ValidateAuthentication();
|
||||
}
|
||||
|
||||
private bool IsAuthenticationValid()
|
||||
{
|
||||
return AuthenticationType switch
|
||||
{
|
||||
NtfyAuthenticationType.None => true,
|
||||
NtfyAuthenticationType.BasicAuth => !string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password),
|
||||
NtfyAuthenticationType.AccessToken => !string.IsNullOrWhiteSpace(AccessToken),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private void ValidateAuthentication()
|
||||
{
|
||||
switch (AuthenticationType)
|
||||
{
|
||||
case NtfyAuthenticationType.BasicAuth:
|
||||
if (string.IsNullOrWhiteSpace(Username))
|
||||
{
|
||||
throw new ValidationException("Username is required for Basic Auth");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
throw new ValidationException("Password is required for Basic Auth");
|
||||
}
|
||||
break;
|
||||
|
||||
case NtfyAuthenticationType.AccessToken:
|
||||
if (string.IsNullOrWhiteSpace(AccessToken))
|
||||
{
|
||||
throw new ValidationException("Access token is required for Token authentication");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
code/frontend/public/icons/ext/apprise-light.svg
Normal file
2
code/frontend/public/icons/ext/apprise-light.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M433.8 75.1C386.3 26.7 323.2 0 256 0S125.7 26.7 78.2 75.1C30.8 123.4 4.7 187.7 4.7 256s26.1 132.6 73.5 180.9C125.7 485.3 188.8 512 256 512s130.3-26.7 177.8-75.1c47.4-48.3 73.5-112.6 73.5-180.9s-26.1-132.6-73.5-180.9m-76.9 98.1c-7.4-13.4-21.5-19.8-37.8-17-5.6.9-10.9-2.8-11.9-8.4-.9-5.6 2.8-10.9 8.4-11.9 25-4.2 47.7 6.3 59.3 27.4 11.4 20.8 8.4 45.8-7.7 63.7-2 2.3-4.8 3.4-7.7 3.4-2.5 0-4.9-.9-6.9-2.6-4.2-3.8-4.6-10.3-.8-14.5 10.3-11.3 12.2-27 5.1-40.1M113.1 365.1c-28.2 0-51.1-22.2-51.1-49.6 0-14.7 6.6-27.9 17.1-37 16.2 27.7 33.8 56.4 50.1 84.1-5.2 1.6-10.5 2.5-16.1 2.5m121.8 54-21.2 12.2c-7.7 4.5-17.6 1.8-22.1-5.9l-30.1-52.1 49.2-28.4 30.1 52.1c4.5 7.8 1.8 17.7-5.9 22.1M150 352.4c-16.8-29-35.8-59.7-52.6-88.7 28.8-15.5 85.8-44.5 118.5-108.4 7.2-14.1 10.9-28.8 15.7-40.9 1.5-3.9 4.7-6.9 8.7-8 1.4-.4 3.1-.6 4.9-.4 4.4.4 8.6 2.8 11 7l103.3 177.7c.2.3.3.6.5.9 4.8 9.8-3.9 21-14.7 19.3-15.3-2.4-31.9-6.7-51.3-6.8-69.4-.5-116.4 34.5-144 48.3m284.4-164.2c-1.9 22.8-9.9 38-22.3 55.2-2.1 2.9-5.4 4.5-8.8 4.5-2.2 0-4.4-.7-6.3-2-4.8-3.5-6-10.2-2.5-15.1 10.4-14.6 16.8-26.5 18.3-44.4 1.8-22.3-5.8-42.6-21.4-57-17-15.7-41.8-22.8-66.2-18.8-5.9 1-11.4-3.1-12.4-8.9-1-5.9 3.1-11.4 8.9-12.4 31-5 62.5 4.1 84.4 24.3 20.7 19 30.7 45.6 28.3 74.6" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
code/frontend/public/icons/ext/apprise.svg
Normal file
1
code/frontend/public/icons/ext/apprise.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><linearGradient id="apprise_svg__a" x1="88.655" x2="423.345" y1="423.345" y2="88.656" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#144042"/><stop offset="1" style="stop-color:#216666"/></linearGradient><ellipse cx="256" cy="256" rx="234.3" ry="239" style="fill:url(#apprise_svg__a)"/><path d="M433.8 75.1C386.3 26.7 323.2 0 256 0S125.7 26.7 78.2 75.1C30.8 123.4 4.7 187.7 4.7 256s26.1 132.6 73.5 180.9C125.7 485.3 188.8 512 256 512s130.3-26.7 177.8-75.1c47.4-48.3 73.5-112.6 73.5-180.9s-26.1-132.6-73.5-180.9M256 495C126.6 495 21.7 388 21.7 256S126.6 17 256 17s234.3 107 234.3 239S385.4 495 256 495"/><path d="M217.4 341.1c-2.1-3.7-6.9-5-10.5-2.8l-49.2 28.4c-2.6 1.5-4.2 4.4-3.8 7.6.1 1.1.6 2.2 1.1 3.2l29.7 51.5c3.3 5.6 8.5 9.7 14.8 11.3 2.1.6 4.2.8 6.3.8 4.2 0 8.4-1.1 12.1-3.3l19.9-11.5c5.3-3.1 9.5-7.8 11.4-13.6 2.2-6.7 1.5-13.9-2-19.9zm17.1 78.3-20.4 11.8c-8 4.6-18.1 1.9-22.7-6.1l-29.9-51.7 49.2-28.4 29.9 51.7c4.6 7.9 1.9 18.1-6.1 22.7m132.4-131.2c-.2-.5-.5-.9-.7-1.4L262.9 109.2c-3.6-6.1-9.9-10.2-17-10.8-2.6-.2-5.1 0-7.6.6-6.2 1.7-11.4 6.4-13.9 12.6-1.7 4.4-3.3 9-4.9 13.8-2.9 8.7-6 17.7-10.5 26.4-30.6 59.6-83.8 88.2-112.4 103.6l-2.9 1.5c-3.8 2.1-5.2 6.9-3 10.7 8.4 14.6 17.5 29.8 26.4 44.5 8.8 14.7 17.9 29.8 26.2 44.3 2 3.5 6.5 4.9 10.1 3 5-2.5 10.7-5.7 16.7-9.1 27.1-15.3 68-38.4 122.5-38.4h1.3c13.7.1 25.9 2.4 37.8 4.6 4.3.8 8.3 1.5 12.4 2.2 8 1.3 16-2 20.8-8.6 4.7-6.5 5.5-14.8 2-21.9 0 .1 0 0 0 0m-21.6 22.7c-15.3-2.4-31.9-6.7-51.3-6.8-69.4-.5-116.4 34.5-144 48.3-16.8-29-35.8-59.7-52.6-88.7 28.8-15.5 85.8-44.5 118.5-108.4 7.2-14.1 10.9-28.8 15.7-40.9 1.5-3.9 4.7-6.9 8.7-8 1.4-.4 3.1-.6 4.9-.4 4.4.4 8.6 2.8 11 7l103.3 177.7c.2.3.3.6.5.9 4.8 9.9-3.9 21-14.7 19.3M111 317.1c-8.4-14-17.1-28.5-25.3-42.6-1.2-2-3.1-3.3-5.4-3.7s-4.6.3-6.3 1.8c-12.6 10.9-19.8 26.5-19.8 42.9 0 31.6 26.4 57.3 58.9 57.3 6.3 0 12.4-1 18.4-2.9 2.2-.7 4-2.4 4.9-4.6.8-2.2.6-4.6-.6-6.7-8.1-13.6-16.6-27.8-24.8-41.5m2.1 48c-28.2 0-51.1-22.2-51.1-49.6 0-14.7 6.6-27.9 17.1-37 16.2 27.7 33.8 56.4 50.1 84.1-5.2 1.6-10.5 2.5-16.1 2.5m268.6-205.5c-13.2-24.1-39-36.1-67.3-31.3-4.7.8-8.9 3.4-11.7 7.3s-3.9 8.7-3.1 13.4c1.6 9.8 10.9 16.4 20.8 14.8 13.1-2.2 24 2.6 29.8 13.2 5.6 10.1 4 22.3-3.9 31.2-3.2 3.6-4.8 8.2-4.6 13 .3 4.8 2.4 9.2 5.9 12.4 3.3 3 7.6 4.6 12 4.6 5.1 0 10-2.2 13.4-6 18.3-20.4 21.7-48.9 8.7-72.6M367.3 227c-2 2.3-4.8 3.4-7.7 3.4-2.5 0-4.9-.9-6.9-2.6-4.2-3.8-4.6-10.3-.8-14.5 10.2-11.3 12.1-27 4.9-40-7.4-13.4-21.5-19.8-37.8-17-5.6.9-10.9-2.8-11.9-8.4-.9-5.6 2.8-10.9 8.4-11.9 25-4.2 47.7 6.3 59.3 27.4 11.6 20.7 8.6 45.7-7.5 63.6m44.1-119.1c-23.6-21.8-57.5-31.6-90.9-26.2-4.9.8-9.2 3.4-12.1 7.5-2.9 4-4.1 8.9-3.3 13.8 1.6 10.1 11.2 17 21.3 15.3 22.2-3.6 44.5 2.7 59.8 16.8 13.9 12.8 20.6 30.9 19 50.7-1.3 16.2-7 26.8-16.9 40.5-2.9 4-4 8.9-3.2 13.8s3.5 9.2 7.5 12c3.2 2.3 6.9 3.5 10.8 3.5 6 0 11.6-2.9 15.1-7.7 13.1-18.3 21.6-34.5 23.7-59.1 2.5-31.4-8.4-60.2-30.8-80.9m23 80.3c-1.9 22.8-9.9 38-22.3 55.2-2.1 2.9-5.4 4.5-8.8 4.5-2.2 0-4.4-.7-6.3-2-4.8-3.5-6-10.2-2.5-15.1 10.4-14.6 16.8-26.5 18.3-44.4 1.8-22.3-5.8-42.6-21.4-57-17-15.7-41.8-22.8-66.2-18.8-5.9 1-11.4-3.1-12.4-8.9-1-5.9 3.1-11.4 8.9-12.4 31-5 62.5 4.1 84.4 24.3 20.7 19 30.7 45.6 28.3 74.6" style="fill:#99b4b4"/><path d="M360 291.6c4.8 9.8-3.9 21-14.7 19.3-15.3-2.4-31.9-6.7-51.3-6.8-69.4-.5-116.4 34.5-144 48.3-16.8-29-35.8-59.7-52.6-88.7 28.8-15.5 85.8-44.5 118.5-108.4 7.2-14.1 10.9-28.8 15.7-40.9 1.5-3.9 4.7-6.9 8.7-8 1.4-.4 3.1-.6 4.9-.4 4.4.4 8.6 2.8 11 7l103.3 177.7c.2.3.4.6.5.9M79 278.4c-10.5 9.1-17.1 22.3-17.1 37 0 27.4 22.9 49.6 51.1 49.6 5.6 0 11-.9 16-2.5-16.2-27.7-33.7-56.4-50-84.1m161.8 118.7L210.7 345l-49.2 28.4 30.1 52.1c4.5 7.7 14.3 10.4 22.1 5.9l21.2-12.2c7.7-4.5 10.4-14.4 5.9-22.1m171.3-153.6c12.3-17.2 20.4-32.4 22.3-55.2 2.4-29.1-7.6-55.6-28.3-74.7-21.8-20.2-53.4-29.3-84.4-24.3-5.9 1-9.9 6.5-8.9 12.4s6.5 9.9 12.4 8.9c24.5-4 49.2 3.1 66.2 18.8 15.7 14.5 23.3 34.7 21.4 57-1.5 17.9-7.8 29.9-18.3 44.4-3.5 4.8-2.4 11.6 2.5 15.1 1.9 1.4 4.1 2 6.3 2 3.4.1 6.7-1.5 8.8-4.4M367.3 227c16.1-17.9 19.1-42.9 7.7-63.7-11.6-21.1-34.3-31.6-59.3-27.4-5.6.9-9.4 6.3-8.4 11.9.9 5.6 6.3 9.4 11.9 8.4 16.3-2.7 30.4 3.6 37.8 17 7.2 13 5.2 28.7-4.9 40-3.8 4.2-3.5 10.7.8 14.5 2 1.8 4.4 2.6 6.9 2.6 2.6.1 5.4-1 7.5-3.3"/></svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
2
code/frontend/public/icons/ext/notifiarr-light.svg
Normal file
2
code/frontend/public/icons/ext/notifiarr-light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 4.9 KiB |
1
code/frontend/public/icons/ext/notifiarr.svg
Normal file
1
code/frontend/public/icons/ext/notifiarr.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.8 KiB |
2
code/frontend/public/icons/ext/ntfy-light.svg
Normal file
2
code/frontend/public/icons/ext/ntfy-light.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M443.1 32.7h-365C40.9 32.7 9 62 9 99.2l.4 311.2-9.4 69 127.1-33.8H443c37.2 0 69.1-29.3 69.1-66.5V99.2c0-37.2-31.9-66.5-69-66.5m22 346.3c0 10-9 19.8-22.1 19.6H120.2l-64.6 19.5.7-3.8-.4-315.1c0-10.1 9.1-19.6 22.2-19.6H443c13.1 0 22.1 9.5 22.1 19.6zM110.5 139.7l124.6 67.9V254l-116.4 63.3-8.2 4.5v-50.1l76.6-40.6.5-.2-.5-.2-76.6-40.6zm158.2 152.4h132.4v46H268.7z" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 479 B |
1
code/frontend/public/icons/ext/ntfy.svg
Normal file
1
code/frontend/public/icons/ext/ntfy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><linearGradient id="ntfy_svg__a" x1="21.759" x2="445.247" y1="74.359" y2="393.379" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#348878"/><stop offset="1" style="stop-color:#56bda8"/></linearGradient><path d="M120.2 398.6H443c13.1.2 22.1-9.6 22.1-19.6V99.2c0-10.1-9-19.6-22.1-19.6H78.1c-13.1 0-22.2 9.5-22.2 19.6l.4 315.1-.7 3.8z" style="fill:url(#ntfy_svg__a)"/><path d="M443.1 32.7h-365C40.9 32.7 9 62 9 99.2l.4 311.2-9.4 69 127.1-33.8H443c37.2 0 69.1-29.3 69.1-66.5V99.2c0-37.2-31.9-66.5-69-66.5m22 346.3c0 10-9 19.8-22.1 19.6H120.2l-64.6 19.5.7-3.8-.4-315.1c0-10.1 9.1-19.6 22.2-19.6H443c13.1 0 22.1 9.5 22.1 19.6zM110.5 139.7l124.6 67.9V254l-116.4 63.3-8.2 4.5v-50.1l76.6-40.6.5-.2-.5-.2-76.6-40.6zm158.2 152.4h132.4v46H268.7z" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 862 B |
@@ -105,11 +105,28 @@ export class DocumentationService {
|
||||
'notifications': {
|
||||
'enabled': 'enabled',
|
||||
'name': 'provider-name',
|
||||
'eventTriggers': 'event-triggers'
|
||||
},
|
||||
'notifications/notifiarr': {
|
||||
'notifiarr.apiKey': 'notifiarr-api-key',
|
||||
'notifiarr.channelId': 'notifiarr-channel-id',
|
||||
'notifiarr.channelId': 'notifiarr-channel-id'
|
||||
},
|
||||
'notifications/apprise': {
|
||||
'apprise.url': 'apprise-url',
|
||||
'apprise.key': 'apprise-key',
|
||||
'apprise.tags': 'apprise-tags',
|
||||
'apprise.tags': 'apprise-tags'
|
||||
},
|
||||
'notifications/ntfy': {
|
||||
'ntfy.serverUrl': 'ntfy-server-url',
|
||||
'ntfy.topics': 'ntfy-topics',
|
||||
'ntfy.authenticationType': 'ntfy-authentication-type',
|
||||
'ntfy.username': 'ntfy-username',
|
||||
'ntfy.password': 'ntfy-password',
|
||||
'ntfy.accessToken': 'ntfy-access-token',
|
||||
'ntfy.priority': 'ntfy-priority',
|
||||
'ntfy.tags': 'ntfy-tags'
|
||||
},
|
||||
'notifications/events': {
|
||||
'eventTriggers': 'event-triggers'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
TestNotificationResult
|
||||
} from '../../shared/models/notification-provider.model';
|
||||
import { NotificationProviderType } from '../../shared/models/enums';
|
||||
import { NtfyAuthenticationType } from '../../shared/models/ntfy-authentication-type.enum';
|
||||
import { NtfyPriority } from '../../shared/models/ntfy-priority.enum';
|
||||
|
||||
// Provider-specific interfaces
|
||||
export interface CreateNotifiarrProviderDto {
|
||||
@@ -75,6 +77,55 @@ export interface TestAppriseProviderDto {
|
||||
tags: string;
|
||||
}
|
||||
|
||||
export interface CreateNtfyProviderDto {
|
||||
name: string;
|
||||
isEnabled: boolean;
|
||||
onFailedImportStrike: boolean;
|
||||
onStalledStrike: boolean;
|
||||
onSlowStrike: boolean;
|
||||
onQueueItemDeleted: boolean;
|
||||
onDownloadCleaned: boolean;
|
||||
onCategoryChanged: boolean;
|
||||
serverUrl: string;
|
||||
topics: string[];
|
||||
authenticationType: NtfyAuthenticationType;
|
||||
username: string;
|
||||
password: string;
|
||||
accessToken: string;
|
||||
priority: NtfyPriority;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface UpdateNtfyProviderDto {
|
||||
name: string;
|
||||
isEnabled: boolean;
|
||||
onFailedImportStrike: boolean;
|
||||
onStalledStrike: boolean;
|
||||
onSlowStrike: boolean;
|
||||
onQueueItemDeleted: boolean;
|
||||
onDownloadCleaned: boolean;
|
||||
onCategoryChanged: boolean;
|
||||
serverUrl: string;
|
||||
topics: string[];
|
||||
authenticationType: NtfyAuthenticationType;
|
||||
username: string;
|
||||
password: string;
|
||||
accessToken: string;
|
||||
priority: NtfyPriority;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface TestNtfyProviderDto {
|
||||
serverUrl: string;
|
||||
topics: string[];
|
||||
authenticationType: NtfyAuthenticationType;
|
||||
username: string;
|
||||
password: string;
|
||||
accessToken: string;
|
||||
priority: NtfyPriority;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@@ -104,6 +155,13 @@ export class NotificationProviderService {
|
||||
return this.http.post<NotificationProviderDto>(`${this.baseUrl}/notification_providers/apprise`, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Ntfy provider
|
||||
*/
|
||||
createNtfyProvider(provider: CreateNtfyProviderDto): Observable<NotificationProviderDto> {
|
||||
return this.http.post<NotificationProviderDto>(`${this.baseUrl}/notification_providers/ntfy`, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing Notifiarr provider
|
||||
*/
|
||||
@@ -118,6 +176,13 @@ export class NotificationProviderService {
|
||||
return this.http.put<NotificationProviderDto>(`${this.baseUrl}/notification_providers/apprise/${id}`, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing Ntfy provider
|
||||
*/
|
||||
updateNtfyProvider(id: string, provider: UpdateNtfyProviderDto): Observable<NotificationProviderDto> {
|
||||
return this.http.put<NotificationProviderDto>(`${this.baseUrl}/notification_providers/ntfy/${id}`, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a notification provider
|
||||
*/
|
||||
@@ -139,6 +204,13 @@ export class NotificationProviderService {
|
||||
return this.http.post<TestNotificationResult>(`${this.baseUrl}/notification_providers/apprise/test`, testRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test an Ntfy provider (without ID - for testing configuration before saving)
|
||||
*/
|
||||
testNtfyProvider(testRequest: TestNtfyProviderDto): Observable<TestNotificationResult> {
|
||||
return this.http.post<TestNotificationResult>(`${this.baseUrl}/notification_providers/ntfy/test`, testRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic create method that delegates to provider-specific methods
|
||||
*/
|
||||
@@ -148,6 +220,8 @@ export class NotificationProviderService {
|
||||
return this.createNotifiarrProvider(provider as CreateNotifiarrProviderDto);
|
||||
case NotificationProviderType.Apprise:
|
||||
return this.createAppriseProvider(provider as CreateAppriseProviderDto);
|
||||
case NotificationProviderType.Ntfy:
|
||||
return this.createNtfyProvider(provider as CreateNtfyProviderDto);
|
||||
default:
|
||||
throw new Error(`Unsupported provider type: ${type}`);
|
||||
}
|
||||
@@ -162,6 +236,8 @@ export class NotificationProviderService {
|
||||
return this.updateNotifiarrProvider(id, provider as UpdateNotifiarrProviderDto);
|
||||
case NotificationProviderType.Apprise:
|
||||
return this.updateAppriseProvider(id, provider as UpdateAppriseProviderDto);
|
||||
case NotificationProviderType.Ntfy:
|
||||
return this.updateNtfyProvider(id, provider as UpdateNtfyProviderDto);
|
||||
default:
|
||||
throw new Error(`Unsupported provider type: ${type}`);
|
||||
}
|
||||
@@ -176,6 +252,8 @@ export class NotificationProviderService {
|
||||
return this.testNotifiarrProvider(testRequest as TestNotifiarrProviderDto);
|
||||
case NotificationProviderType.Apprise:
|
||||
return this.testAppriseProvider(testRequest as TestAppriseProviderDto);
|
||||
case NotificationProviderType.Ntfy:
|
||||
return this.testNtfyProvider(testRequest as TestNtfyProviderDto);
|
||||
default:
|
||||
throw new Error(`Unsupported provider type: ${type}`);
|
||||
}
|
||||
|
||||
@@ -363,8 +363,6 @@
|
||||
multiple
|
||||
fluid
|
||||
[typeahead]="false"
|
||||
[suggestions]="unlinkedCategoriesSuggestions"
|
||||
(completeMethod)="onUnlinkedCategoriesComplete($event)"
|
||||
placeholder="Add category and press Enter"
|
||||
class="desktop-only"
|
||||
>
|
||||
|
||||
@@ -91,9 +91,6 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
// Flag to track if form has been initially loaded to avoid showing dialog on page load
|
||||
private formInitialized = false;
|
||||
|
||||
// Minimal autocomplete support - empty suggestions to allow manual input
|
||||
unlinkedCategoriesSuggestions: string[] = [];
|
||||
|
||||
// Get the categories form array for easier access in the template
|
||||
get categoriesFormArray(): FormArray {
|
||||
return this.downloadCleanerForm.get('categories') as FormArray;
|
||||
@@ -751,15 +748,6 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal complete method for autocomplete - just returns empty array to allow manual input
|
||||
*/
|
||||
onUnlinkedCategoriesComplete(event: any): void {
|
||||
// Return empty array - this allows users to type any value manually
|
||||
// PrimeNG requires this method even when we don't want suggestions
|
||||
this.unlinkedCategoriesSuggestions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show confirmation dialog when enabling the download cleaner
|
||||
*/
|
||||
|
||||
@@ -40,7 +40,7 @@ export class AppriseProviderComponent implements OnInit, OnChanges {
|
||||
* Exposed for template to open documentation for apprise fields
|
||||
*/
|
||||
openFieldDocs(fieldName: string): void {
|
||||
this.documentationService.openFieldDocumentation('notifications', fieldName);
|
||||
this.documentationService.openFieldDocumentation('notifications/apprise', fieldName);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -38,7 +38,7 @@ export class NotifiarrProviderComponent implements OnInit, OnChanges {
|
||||
|
||||
/** Exposed for template to open documentation for notifiarr fields */
|
||||
openFieldDocs(fieldName: string): void {
|
||||
this.documentationService.openFieldDocumentation('notifications', fieldName);
|
||||
this.documentationService.openFieldDocumentation('notifications/notifiarr', fieldName);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<app-notification-provider-base
|
||||
[visible]="visible"
|
||||
modalTitle="Configure ntfy Provider"
|
||||
[saving]="saving"
|
||||
[testing]="testing"
|
||||
[editingProvider]="editingProvider"
|
||||
(save)="onSave($event)"
|
||||
(cancel)="onCancel()"
|
||||
(test)="onTest($event)"
|
||||
>
|
||||
<!-- Provider-specific configuration goes here -->
|
||||
<div slot="provider-config">
|
||||
<!-- Server URL -->
|
||||
<div class="field">
|
||||
<label for="server-url">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('ntfy.serverUrl')"
|
||||
></i>
|
||||
Server URL *
|
||||
</label>
|
||||
<input
|
||||
id="server-url"
|
||||
type="url"
|
||||
pInputText
|
||||
[formControl]="serverUrlControl"
|
||||
placeholder="https://ntfy.sh"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasFieldError(serverUrlControl, 'required')" class="p-error">Server URL is required</small>
|
||||
<small *ngIf="hasFieldError(serverUrlControl, 'invalidUri')" class="p-error">Must be a valid URL</small>
|
||||
<small *ngIf="hasFieldError(serverUrlControl, 'invalidProtocol')" class="p-error">Must use http or https protocol</small>
|
||||
<small class="form-helper-text">The URL of your ntfy server. Use https://ntfy.sh for the public service or your self-hosted instance.</small>
|
||||
</div>
|
||||
|
||||
<!-- Topics -->
|
||||
<div class="field">
|
||||
<label for="topics">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('ntfy.topics')"
|
||||
></i>
|
||||
Topics *
|
||||
</label>
|
||||
<!-- Mobile-friendly autocomplete (chips UI) -->
|
||||
<app-mobile-autocomplete
|
||||
[formControl]="topicsControl"
|
||||
placeholder="Enter topic names"
|
||||
></app-mobile-autocomplete>
|
||||
|
||||
<!-- Desktop autocomplete (allows multiple entries) -->
|
||||
<p-autocomplete
|
||||
id="topics"
|
||||
[formControl]="topicsControl"
|
||||
multiple
|
||||
fluid
|
||||
[typeahead]="false"
|
||||
placeholder="Add a topic and press Enter"
|
||||
class="desktop-only w-full"
|
||||
></p-autocomplete>
|
||||
|
||||
<small *ngIf="hasFieldError(topicsControl, 'required')" class="p-error">At least one topic is required</small>
|
||||
<small *ngIf="hasFieldError(topicsControl, 'minlength')" class="p-error">At least one topic is required</small>
|
||||
<small class="form-helper-text">Enter the ntfy topics you want to publish to. Press Enter or comma to add each topic.</small>
|
||||
</div>
|
||||
|
||||
<!-- Authentication Type -->
|
||||
<div class="field">
|
||||
<label for="auth-type">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('ntfy.authenticationType')"
|
||||
></i>
|
||||
Authentication Type *
|
||||
</label>
|
||||
<p-select
|
||||
id="auth-type"
|
||||
[formControl]="authenticationTypeControl"
|
||||
[options]="authenticationOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
[showClear]="false"
|
||||
></p-select>
|
||||
<small *ngIf="hasFieldError(authenticationTypeControl, 'required')" class="p-error">Authentication type is required</small>
|
||||
<small class="form-helper-text">Choose how to authenticate with the ntfy server.</small>
|
||||
</div>
|
||||
|
||||
<!-- Username (conditional on Basic Auth) -->
|
||||
<div class="field" *ngIf="currentAuthType === NtfyAuthenticationType.BasicAuth">
|
||||
<label for="username">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('ntfy.username')"
|
||||
></i>
|
||||
Username *
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
pInputText
|
||||
[formControl]="usernameControl"
|
||||
placeholder="Enter username"
|
||||
class="w-full"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<small *ngIf="hasFieldError(usernameControl, 'required')" class="p-error">Username is required for Basic Auth</small>
|
||||
<small class="form-helper-text">Your username for basic authentication.</small>
|
||||
</div>
|
||||
|
||||
<!-- Password (conditional on Basic Auth) -->
|
||||
<div class="field" *ngIf="currentAuthType === NtfyAuthenticationType.BasicAuth">
|
||||
<label for="password">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('ntfy.password')"
|
||||
></i>
|
||||
Password *
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
pInputText
|
||||
[formControl]="passwordControl"
|
||||
placeholder="Enter password"
|
||||
class="w-full"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<small *ngIf="hasFieldError(passwordControl, 'required')" class="p-error">Password is required for Basic Auth</small>
|
||||
<small class="form-helper-text">Your password for basic authentication.</small>
|
||||
</div>
|
||||
|
||||
<!-- Access Token (conditional on Access Token) -->
|
||||
<div class="field" *ngIf="currentAuthType === NtfyAuthenticationType.AccessToken">
|
||||
<label for="access-token">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('ntfy.accessToken')"
|
||||
></i>
|
||||
Access Token *
|
||||
</label>
|
||||
<input
|
||||
id="access-token"
|
||||
type="password"
|
||||
pInputText
|
||||
[formControl]="accessTokenControl"
|
||||
placeholder="Enter access token"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasFieldError(accessTokenControl, 'required')" class="p-error">Access token is required</small>
|
||||
<small class="form-helper-text">Your access token for bearer token authentication.</small>
|
||||
</div>
|
||||
|
||||
<!-- Priority -->
|
||||
<div class="field">
|
||||
<label for="priority">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('ntfy.priority')"
|
||||
></i>
|
||||
Priority *
|
||||
</label>
|
||||
<p-select
|
||||
id="priority"
|
||||
[formControl]="priorityControl"
|
||||
[options]="priorityOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
[showClear]="false"
|
||||
></p-select>
|
||||
<small *ngIf="hasFieldError(priorityControl, 'required')" class="p-error">Priority is required</small>
|
||||
<small class="form-helper-text">The priority level for notifications (1=min, 5=max).</small>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="field">
|
||||
<label for="tags">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('ntfy.tags')"
|
||||
></i>
|
||||
Tags (Optional)
|
||||
</label>
|
||||
<!-- Mobile-friendly autocomplete (chips UI) -->
|
||||
<app-mobile-autocomplete
|
||||
[formControl]="tagsControl"
|
||||
placeholder="Enter tag names"
|
||||
></app-mobile-autocomplete>
|
||||
|
||||
<!-- Desktop autocomplete (allows multiple entries) -->
|
||||
<p-autocomplete
|
||||
id="tags"
|
||||
[formControl]="tagsControl"
|
||||
multiple
|
||||
fluid
|
||||
[typeahead]="false"
|
||||
placeholder="Add a tag and press Enter"
|
||||
class="desktop-only w-full"
|
||||
></p-autocomplete>
|
||||
|
||||
<small class="form-helper-text">Optional tags to add to notifications (e.g., warning, alert). Press Enter or comma to add each tag.</small>
|
||||
</div>
|
||||
</div>
|
||||
</app-notification-provider-base>
|
||||
@@ -0,0 +1 @@
|
||||
@use '../../../styles/settings-shared.scss';
|
||||
@@ -0,0 +1,216 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges, inject } from '@angular/core';
|
||||
import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { AutoCompleteModule } from 'primeng/autocomplete';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { MobileAutocompleteComponent } from '../../../../shared/components/mobile-autocomplete/mobile-autocomplete.component';
|
||||
import { NtfyFormData, BaseProviderFormData } from '../../models/provider-modal.model';
|
||||
import { DocumentationService } from '../../../../core/services/documentation.service';
|
||||
import { NotificationProviderDto } from '../../../../shared/models/notification-provider.model';
|
||||
import { NotificationProviderBaseComponent } from '../base/notification-provider-base.component';
|
||||
import { UrlValidators } from '../../../../core/validators/url.validator';
|
||||
import { NtfyAuthenticationType } from '../../../../shared/models/ntfy-authentication-type.enum';
|
||||
import { NtfyPriority } from '../../../../shared/models/ntfy-priority.enum';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ntfy-provider',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
InputTextModule,
|
||||
AutoCompleteModule,
|
||||
SelectModule,
|
||||
MobileAutocompleteComponent,
|
||||
NotificationProviderBaseComponent
|
||||
],
|
||||
templateUrl: './ntfy-provider.component.html',
|
||||
styleUrls: ['./ntfy-provider.component.scss']
|
||||
})
|
||||
export class NtfyProviderComponent implements OnInit, OnChanges {
|
||||
@Input() visible = false;
|
||||
@Input() editingProvider: NotificationProviderDto | null = null;
|
||||
@Input() saving = false;
|
||||
@Input() testing = false;
|
||||
|
||||
@Output() save = new EventEmitter<NtfyFormData>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
@Output() test = new EventEmitter<NtfyFormData>();
|
||||
|
||||
// Provider-specific form controls
|
||||
serverUrlControl = new FormControl('', [Validators.required, UrlValidators.httpUrl]);
|
||||
topicsControl = new FormControl<string[]>([], [Validators.required, Validators.minLength(1)]);
|
||||
authenticationTypeControl = new FormControl(NtfyAuthenticationType.None, [Validators.required]);
|
||||
usernameControl = new FormControl('');
|
||||
passwordControl = new FormControl('');
|
||||
accessTokenControl = new FormControl('');
|
||||
priorityControl = new FormControl(NtfyPriority.Default, [Validators.required]);
|
||||
tagsControl = new FormControl<string[]>([]);
|
||||
|
||||
private documentationService = inject(DocumentationService);
|
||||
|
||||
// Enum references for template
|
||||
readonly NtfyAuthenticationType = NtfyAuthenticationType;
|
||||
readonly NtfyPriority = NtfyPriority;
|
||||
|
||||
// Dropdown options
|
||||
authenticationOptions = [
|
||||
{ label: 'None', value: NtfyAuthenticationType.None },
|
||||
{ label: 'Basic Auth', value: NtfyAuthenticationType.BasicAuth },
|
||||
{ label: 'Access Token', value: NtfyAuthenticationType.AccessToken }
|
||||
];
|
||||
|
||||
priorityOptions = [
|
||||
{ label: 'Min', value: NtfyPriority.Min },
|
||||
{ label: 'Low', value: NtfyPriority.Low },
|
||||
{ label: 'Default', value: NtfyPriority.Default },
|
||||
{ label: 'High', value: NtfyPriority.High },
|
||||
{ label: 'Max', value: NtfyPriority.Max }
|
||||
];
|
||||
|
||||
/**
|
||||
* Exposed for template to open documentation for ntfy fields
|
||||
*/
|
||||
openFieldDocs(fieldName: string): void {
|
||||
this.documentationService.openFieldDocumentation('notifications/ntfy', fieldName);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize component but don't populate yet - wait for ngOnChanges
|
||||
|
||||
// Set up conditional validation for authentication fields
|
||||
this.authenticationTypeControl.valueChanges.subscribe(type => {
|
||||
this.updateAuthFieldValidation(type);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Populate provider-specific fields when editingProvider input changes
|
||||
if (changes['editingProvider']) {
|
||||
if (this.editingProvider) {
|
||||
this.populateProviderFields();
|
||||
} else {
|
||||
// Reset fields when editingProvider is cleared
|
||||
this.resetProviderFields();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private populateProviderFields(): void {
|
||||
if (this.editingProvider) {
|
||||
const config = this.editingProvider.configuration as any;
|
||||
|
||||
this.serverUrlControl.setValue(config?.serverUrl || 'https://ntfy.sh');
|
||||
this.topicsControl.setValue(config?.topics || []);
|
||||
this.authenticationTypeControl.setValue(config?.authenticationType || NtfyAuthenticationType.None);
|
||||
this.usernameControl.setValue(config?.username || '');
|
||||
this.passwordControl.setValue(config?.password || '');
|
||||
this.accessTokenControl.setValue(config?.accessToken || '');
|
||||
this.priorityControl.setValue(config?.priority || NtfyPriority.Default);
|
||||
this.tagsControl.setValue(config?.tags || []);
|
||||
}
|
||||
}
|
||||
|
||||
private resetProviderFields(): void {
|
||||
this.serverUrlControl.setValue('https://ntfy.sh');
|
||||
this.topicsControl.setValue([]);
|
||||
this.authenticationTypeControl.setValue(NtfyAuthenticationType.None);
|
||||
this.usernameControl.setValue('');
|
||||
this.passwordControl.setValue('');
|
||||
this.accessTokenControl.setValue('');
|
||||
this.priorityControl.setValue(NtfyPriority.Default);
|
||||
this.tagsControl.setValue([]);
|
||||
}
|
||||
|
||||
private updateAuthFieldValidation(authType: NtfyAuthenticationType | null): void {
|
||||
// Clear previous validators
|
||||
this.usernameControl.clearValidators();
|
||||
this.passwordControl.clearValidators();
|
||||
this.accessTokenControl.clearValidators();
|
||||
|
||||
// Set validators based on auth type
|
||||
if (authType === NtfyAuthenticationType.BasicAuth) {
|
||||
this.usernameControl.setValidators([Validators.required]);
|
||||
this.passwordControl.setValidators([Validators.required]);
|
||||
} else if (authType === NtfyAuthenticationType.AccessToken) {
|
||||
this.accessTokenControl.setValidators([Validators.required]);
|
||||
}
|
||||
|
||||
// Update validation status
|
||||
this.usernameControl.updateValueAndValidity();
|
||||
this.passwordControl.updateValueAndValidity();
|
||||
this.accessTokenControl.updateValueAndValidity();
|
||||
}
|
||||
|
||||
protected hasFieldError(control: FormControl, errorType: string): boolean {
|
||||
return !!(control && control.errors?.[errorType] && (control.dirty || control.touched));
|
||||
}
|
||||
|
||||
private isFormValid(): boolean {
|
||||
return this.serverUrlControl.valid &&
|
||||
this.topicsControl.valid &&
|
||||
this.authenticationTypeControl.valid &&
|
||||
this.usernameControl.valid &&
|
||||
this.passwordControl.valid &&
|
||||
this.accessTokenControl.valid &&
|
||||
this.priorityControl.valid;
|
||||
}
|
||||
|
||||
private buildNtfyData(baseData: BaseProviderFormData): NtfyFormData {
|
||||
return {
|
||||
...baseData,
|
||||
serverUrl: this.serverUrlControl.value || '',
|
||||
topics: this.topicsControl.value || [],
|
||||
authenticationType: this.authenticationTypeControl.value || NtfyAuthenticationType.None,
|
||||
username: this.usernameControl.value || '',
|
||||
password: this.passwordControl.value || '',
|
||||
accessToken: this.accessTokenControl.value || '',
|
||||
priority: this.priorityControl.value || NtfyPriority.Default,
|
||||
tags: this.tagsControl.value || []
|
||||
};
|
||||
}
|
||||
|
||||
onSave(baseData: BaseProviderFormData): void {
|
||||
if (this.isFormValid()) {
|
||||
const ntfyData = this.buildNtfyData(baseData);
|
||||
this.save.emit(ntfyData);
|
||||
} else {
|
||||
// Mark provider-specific fields as touched to show validation errors
|
||||
this.serverUrlControl.markAsTouched();
|
||||
this.topicsControl.markAsTouched();
|
||||
this.authenticationTypeControl.markAsTouched();
|
||||
this.usernameControl.markAsTouched();
|
||||
this.passwordControl.markAsTouched();
|
||||
this.accessTokenControl.markAsTouched();
|
||||
this.priorityControl.markAsTouched();
|
||||
}
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.cancel.emit();
|
||||
}
|
||||
|
||||
onTest(baseData: BaseProviderFormData): void {
|
||||
if (this.isFormValid()) {
|
||||
const ntfyData = this.buildNtfyData(baseData);
|
||||
this.test.emit(ntfyData);
|
||||
} else {
|
||||
// Mark provider-specific fields as touched to show validation errors
|
||||
this.serverUrlControl.markAsTouched();
|
||||
this.topicsControl.markAsTouched();
|
||||
this.authenticationTypeControl.markAsTouched();
|
||||
this.usernameControl.markAsTouched();
|
||||
this.passwordControl.markAsTouched();
|
||||
this.accessTokenControl.markAsTouched();
|
||||
this.priorityControl.markAsTouched();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current authentication type for template conditionals
|
||||
*/
|
||||
get currentAuthType(): NtfyAuthenticationType | null {
|
||||
return this.authenticationTypeControl.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<p-dialog
|
||||
[(visible)]="visible"
|
||||
[modal]="true"
|
||||
[closable]="true"
|
||||
[draggable]="false"
|
||||
[resizable]="false"
|
||||
styleClass="instance-modal provider-selection-modal"
|
||||
header="Add Notification Provider"
|
||||
(onHide)="onCancel()">
|
||||
|
||||
<div class="provider-selection-content">
|
||||
<p class="selection-description">
|
||||
Choose a notification provider type to configure:
|
||||
</p>
|
||||
|
||||
<div class="provider-selection-grid">
|
||||
<div
|
||||
class="provider-card"
|
||||
*ngFor="let provider of availableProviders"
|
||||
(click)="selectProvider(provider.type)"
|
||||
(mouseenter)="onProviderEnter(provider.type)"
|
||||
(mouseleave)="onProviderLeave()"
|
||||
[attr.data-provider]="provider.type">
|
||||
|
||||
<div class="provider-icon">
|
||||
<img [src]="hoveredProvider === provider.type && provider.iconUrlHover ? provider.iconUrlHover : provider.iconUrl" [alt]="provider.name + ' icon'" class="provider-icon-image">
|
||||
</div>
|
||||
<div class="provider-name">
|
||||
{{ provider.name }}
|
||||
</div>
|
||||
<div class="provider-description" *ngIf="provider.description">
|
||||
{{ provider.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="onCancel()">
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
@@ -1,12 +1,15 @@
|
||||
.provider-selection-modal {
|
||||
.p-dialog {
|
||||
width: 90vw;
|
||||
max-width: 600px;
|
||||
max-width: 900px; // Increased to accommodate multiple columns
|
||||
min-width: 320px; // Ensure minimum usable width
|
||||
}
|
||||
}
|
||||
|
||||
.provider-selection-content {
|
||||
padding: 1rem;
|
||||
max-height: 70vh; // Limit height to prevent excessive tall modals
|
||||
overflow-y: auto; // Enable scrolling if needed
|
||||
}
|
||||
|
||||
.selection-description {
|
||||
@@ -18,9 +21,22 @@
|
||||
|
||||
.provider-selection-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
padding: 1rem 0;
|
||||
|
||||
// Default: 3 columns for desktop (large screens)
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
// Medium screens (tablets): 2 columns
|
||||
@media (max-width: 992px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
// Small screens (mobile): 1 column
|
||||
@media (max-width: 576px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
@@ -45,7 +61,10 @@
|
||||
|
||||
.provider-icon {
|
||||
transform: scale(1.1);
|
||||
color: var(--primary-color);
|
||||
|
||||
.provider-icon-image {
|
||||
filter: opacity(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,13 +86,15 @@
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
font-size: 3rem;
|
||||
color: var(--text-color-secondary);
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
i {
|
||||
.provider-icon-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: block;
|
||||
filter: opacity(0.7);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,28 +113,34 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--surface-border);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.provider-selection-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
// Responsive design for card content
|
||||
@media (max-width: 576px) {
|
||||
.provider-card {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
.provider-icon-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.provider-description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure proper spacing on medium screens
|
||||
@media (max-width: 992px) and (min-width: 577px) {
|
||||
.provider-card {
|
||||
padding: 1.75rem 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,10 +162,17 @@
|
||||
animation: fadeInUp 0.4s ease-out;
|
||||
animation-fill-mode: both;
|
||||
|
||||
// Staggered animation delays for up to 12 providers (4 rows x 3 columns)
|
||||
&:nth-child(1) { animation-delay: 0.1s; }
|
||||
&:nth-child(2) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.3s; }
|
||||
&:nth-child(4) { animation-delay: 0.4s; }
|
||||
&:nth-child(2) { animation-delay: 0.15s; }
|
||||
&:nth-child(3) { animation-delay: 0.2s; }
|
||||
&:nth-child(4) { animation-delay: 0.25s; }
|
||||
&:nth-child(5) { animation-delay: 0.3s; }
|
||||
&:nth-child(6) { animation-delay: 0.35s; }
|
||||
&:nth-child(7) { animation-delay: 0.4s; }
|
||||
&:nth-child(8) { animation-delay: 0.45s; }
|
||||
&:nth-child(9) { animation-delay: 0.5s; }
|
||||
&:nth-child(n+10) { animation-delay: 0.55s; } // All remaining items
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
@@ -151,3 +185,40 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Add subtle scroll indicators when content overflows
|
||||
.provider-selection-content {
|
||||
position: relative;
|
||||
|
||||
// Gradient fade at bottom when scrolling is needed
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 20px;
|
||||
background: linear-gradient(to top, var(--surface-card), transparent);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--surface-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--text-color-secondary);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,83 +13,51 @@ import { ProviderTypeInfo } from '../../models/provider-modal.model';
|
||||
DialogModule,
|
||||
ButtonModule
|
||||
],
|
||||
template: `
|
||||
<p-dialog
|
||||
[(visible)]="visible"
|
||||
[modal]="true"
|
||||
[closable]="true"
|
||||
[draggable]="false"
|
||||
[resizable]="false"
|
||||
styleClass="instance-modal provider-selection-modal"
|
||||
header="Add Notification Provider"
|
||||
(onHide)="onCancel()">
|
||||
|
||||
<div class="provider-selection-content">
|
||||
<p class="selection-description">
|
||||
Choose a notification provider type to configure:
|
||||
</p>
|
||||
|
||||
<div class="provider-selection-grid">
|
||||
<div
|
||||
class="provider-card"
|
||||
*ngFor="let provider of availableProviders"
|
||||
(click)="selectProvider(provider.type)"
|
||||
[attr.data-provider]="provider.type">
|
||||
|
||||
<div class="provider-icon">
|
||||
<i [class]="provider.iconClass"></i>
|
||||
</div>
|
||||
<div class="provider-name">
|
||||
{{ provider.name }}
|
||||
</div>
|
||||
<div class="provider-description" *ngIf="provider.description">
|
||||
{{ provider.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="onCancel()">
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-dialog>
|
||||
`,
|
||||
templateUrl: './provider-type-selection.component.html',
|
||||
styleUrls: ['./provider-type-selection.component.scss']
|
||||
})
|
||||
export class ProviderTypeSelectionComponent {
|
||||
@Input() visible = false;
|
||||
@Output() providerSelected = new EventEmitter<NotificationProviderType>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
hoveredProvider: NotificationProviderType | null = null;
|
||||
|
||||
// Available providers - only show implemented ones
|
||||
availableProviders: ProviderTypeInfo[] = [
|
||||
{
|
||||
type: NotificationProviderType.Notifiarr,
|
||||
name: 'Notifiarr',
|
||||
iconClass: 'pi pi-bell',
|
||||
description: 'Discord integration via Notifiarr service'
|
||||
},
|
||||
{
|
||||
type: NotificationProviderType.Apprise,
|
||||
name: 'Apprise',
|
||||
iconClass: 'pi pi-send',
|
||||
description: 'Universal notification library supporting many services'
|
||||
iconUrl: '/icons/ext/apprise-light.svg',
|
||||
iconUrlHover: '/icons/ext/apprise.svg',
|
||||
description: 'https://github.com/caronc/apprise'
|
||||
},
|
||||
{
|
||||
type: NotificationProviderType.Notifiarr,
|
||||
name: 'Notifiarr',
|
||||
iconUrl: '/icons/ext/notifiarr-light.svg',
|
||||
iconUrlHover: '/icons/ext/notifiarr.svg',
|
||||
description: 'https://notifiarr.com'
|
||||
},
|
||||
{
|
||||
type: NotificationProviderType.Ntfy,
|
||||
name: 'ntfy',
|
||||
iconUrl: '/icons/ext/ntfy-light.svg',
|
||||
iconUrlHover: '/icons/ext/ntfy.svg',
|
||||
description: 'https://ntfy.sh/'
|
||||
}
|
||||
// Add more providers as they are implemented
|
||||
];
|
||||
|
||||
selectProvider(type: NotificationProviderType) {
|
||||
this.providerSelected.emit(type);
|
||||
}
|
||||
|
||||
onProviderEnter(type: NotificationProviderType) {
|
||||
this.hoveredProvider = type;
|
||||
}
|
||||
|
||||
onProviderLeave() {
|
||||
this.hoveredProvider = null;
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.cancel.emit();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { NotificationProviderType } from '../../../shared/models/enums';
|
||||
import { NtfyAuthenticationType } from '../../../shared/models/ntfy-authentication-type.enum';
|
||||
import { NtfyPriority } from '../../../shared/models/ntfy-priority.enum';
|
||||
|
||||
export interface ProviderTypeInfo {
|
||||
type: NotificationProviderType;
|
||||
name: string;
|
||||
iconClass: string;
|
||||
iconUrl: string;
|
||||
iconUrlHover?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -35,6 +38,17 @@ export interface AppriseFormData extends BaseProviderFormData {
|
||||
tags: string;
|
||||
}
|
||||
|
||||
export interface NtfyFormData extends BaseProviderFormData {
|
||||
serverUrl: string;
|
||||
topics: string[];
|
||||
authenticationType: NtfyAuthenticationType;
|
||||
username: string;
|
||||
password: string;
|
||||
accessToken: string;
|
||||
priority: NtfyPriority;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
// Events for modal communication
|
||||
export interface ProviderModalEvents {
|
||||
save: (data: any) => void;
|
||||
|
||||
@@ -176,6 +176,17 @@
|
||||
(test)="onAppriseTest($event)"
|
||||
></app-apprise-provider>
|
||||
|
||||
<!-- Ntfy Provider Modal -->
|
||||
<app-ntfy-provider
|
||||
[visible]="showNtfyModal"
|
||||
[editingProvider]="editingProvider"
|
||||
[saving]="saving()"
|
||||
[testing]="testing()"
|
||||
(save)="onNtfySave($event)"
|
||||
(cancel)="onProviderCancel()"
|
||||
(test)="onNtfyTest($event)"
|
||||
></app-ntfy-provider>
|
||||
|
||||
<!-- Confirmation Dialog -->
|
||||
<p-confirmDialog></p-confirmDialog>
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
} from "../../shared/models/notification-provider.model";
|
||||
import { NotificationProviderType } from "../../shared/models/enums";
|
||||
import { DocumentationService } from "../../core/services/documentation.service";
|
||||
import { NotifiarrFormData, AppriseFormData } from "./models/provider-modal.model";
|
||||
import { NotifiarrFormData, AppriseFormData, NtfyFormData } from "./models/provider-modal.model";
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
|
||||
// New modal components
|
||||
import { ProviderTypeSelectionComponent } from "./modals/provider-type-selection/provider-type-selection.component";
|
||||
import { NotifiarrProviderComponent } from "./modals/notifiarr-provider/notifiarr-provider.component";
|
||||
import { AppriseProviderComponent } from "./modals/apprise-provider/apprise-provider.component";
|
||||
import { NtfyProviderComponent } from "./modals/ntfy-provider/ntfy-provider.component";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -49,6 +50,7 @@ import { NotificationService } from "../../core/services/notification.service";
|
||||
ProviderTypeSelectionComponent,
|
||||
NotifiarrProviderComponent,
|
||||
AppriseProviderComponent,
|
||||
NtfyProviderComponent,
|
||||
],
|
||||
providers: [NotificationProviderConfigStore, ConfirmationService, MessageService],
|
||||
templateUrl: "./notification-settings.component.html",
|
||||
@@ -63,6 +65,7 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
showTypeSelectionModal = false; // New: Provider type selection modal
|
||||
showNotifiarrModal = false; // New: Notifiarr provider modal
|
||||
showAppriseModal = false; // New: Apprise provider modal
|
||||
showNtfyModal = false; // New: Ntfy provider modal
|
||||
modalMode: 'add' | 'edit' = 'add';
|
||||
editingProvider: NotificationProviderDto | null = null;
|
||||
|
||||
@@ -173,6 +176,9 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
case NotificationProviderType.Apprise:
|
||||
this.showAppriseModal = true;
|
||||
break;
|
||||
case NotificationProviderType.Ntfy:
|
||||
this.showNtfyModal = true;
|
||||
break;
|
||||
default:
|
||||
// For unsupported types, show the legacy modal with info message
|
||||
this.showProviderModal = true;
|
||||
@@ -220,6 +226,9 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
case NotificationProviderType.Apprise:
|
||||
this.showAppriseModal = true;
|
||||
break;
|
||||
case NotificationProviderType.Ntfy:
|
||||
this.showNtfyModal = true;
|
||||
break;
|
||||
default:
|
||||
// For unsupported types, show the legacy modal with info message
|
||||
this.showProviderModal = true;
|
||||
@@ -268,6 +277,19 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
tags: appriseConfig.tags || "",
|
||||
};
|
||||
break;
|
||||
case NotificationProviderType.Ntfy:
|
||||
const ntfyConfig = provider.configuration as any;
|
||||
testRequest = {
|
||||
serverUrl: ntfyConfig.serverUrl,
|
||||
topics: ntfyConfig.topics,
|
||||
authenticationType: ntfyConfig.authenticationType,
|
||||
username: ntfyConfig.username || "",
|
||||
password: ntfyConfig.password || "",
|
||||
accessToken: ntfyConfig.accessToken || "",
|
||||
priority: ntfyConfig.priority,
|
||||
tags: ntfyConfig.tags || "",
|
||||
};
|
||||
break;
|
||||
default:
|
||||
this.notificationService.showError("Testing not supported for this provider type");
|
||||
return;
|
||||
@@ -304,6 +326,8 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
return "Notifiarr";
|
||||
case NotificationProviderType.Apprise:
|
||||
return "Apprise";
|
||||
case NotificationProviderType.Ntfy:
|
||||
return "ntfy";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -378,6 +402,38 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Ntfy provider save
|
||||
*/
|
||||
onNtfySave(data: NtfyFormData): void {
|
||||
if (this.modalMode === "edit" && this.editingProvider) {
|
||||
this.updateNtfyProvider(data);
|
||||
} else {
|
||||
this.createNtfyProvider(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Ntfy provider test
|
||||
*/
|
||||
onNtfyTest(data: NtfyFormData): void {
|
||||
const testRequest = {
|
||||
serverUrl: data.serverUrl,
|
||||
topics: data.topics,
|
||||
authenticationType: data.authenticationType,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
accessToken: data.accessToken,
|
||||
priority: data.priority,
|
||||
tags: data.tags,
|
||||
};
|
||||
|
||||
this.notificationProviderStore.testProvider({
|
||||
testRequest,
|
||||
type: NotificationProviderType.Ntfy,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle provider modal cancel
|
||||
*/
|
||||
@@ -392,6 +448,7 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
this.showTypeSelectionModal = false;
|
||||
this.showNotifiarrModal = false;
|
||||
this.showAppriseModal = false;
|
||||
this.showNtfyModal = false;
|
||||
this.showProviderModal = false;
|
||||
this.editingProvider = null;
|
||||
this.notificationProviderStore.clearTestResult();
|
||||
@@ -501,6 +558,69 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
this.monitorProviderOperation("updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new Ntfy provider
|
||||
*/
|
||||
private createNtfyProvider(data: NtfyFormData): void {
|
||||
const createDto = {
|
||||
name: data.name,
|
||||
isEnabled: data.enabled,
|
||||
onFailedImportStrike: data.onFailedImportStrike,
|
||||
onStalledStrike: data.onStalledStrike,
|
||||
onSlowStrike: data.onSlowStrike,
|
||||
onQueueItemDeleted: data.onQueueItemDeleted,
|
||||
onDownloadCleaned: data.onDownloadCleaned,
|
||||
onCategoryChanged: data.onCategoryChanged,
|
||||
serverUrl: data.serverUrl,
|
||||
topics: data.topics,
|
||||
authenticationType: data.authenticationType,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
accessToken: data.accessToken,
|
||||
priority: data.priority,
|
||||
tags: data.tags,
|
||||
};
|
||||
|
||||
this.notificationProviderStore.createProvider({
|
||||
provider: createDto,
|
||||
type: NotificationProviderType.Ntfy,
|
||||
});
|
||||
this.monitorProviderOperation("created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing Ntfy provider
|
||||
*/
|
||||
private updateNtfyProvider(data: NtfyFormData): void {
|
||||
if (!this.editingProvider) return;
|
||||
|
||||
const updateDto = {
|
||||
name: data.name,
|
||||
isEnabled: data.enabled,
|
||||
onFailedImportStrike: data.onFailedImportStrike,
|
||||
onStalledStrike: data.onStalledStrike,
|
||||
onSlowStrike: data.onSlowStrike,
|
||||
onQueueItemDeleted: data.onQueueItemDeleted,
|
||||
onDownloadCleaned: data.onDownloadCleaned,
|
||||
onCategoryChanged: data.onCategoryChanged,
|
||||
serverUrl: data.serverUrl,
|
||||
topics: data.topics,
|
||||
authenticationType: data.authenticationType,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
accessToken: data.accessToken,
|
||||
priority: data.priority,
|
||||
tags: data.tags,
|
||||
};
|
||||
|
||||
this.notificationProviderStore.updateProvider({
|
||||
id: this.editingProvider.id,
|
||||
provider: updateDto,
|
||||
type: NotificationProviderType.Ntfy,
|
||||
});
|
||||
this.monitorProviderOperation("updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor provider operation completion and close modals
|
||||
*/
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
input, .p-dropdown, .p-inputnumber {
|
||||
input, .p-select, .p-autocomplete, .p-inputnumber {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@
|
||||
}
|
||||
|
||||
/* Input styling */
|
||||
.p-inputtext, .p-dropdown, .p-inputnumber {
|
||||
.p-inputtext, .p-select, .p-autocomplete, .p-inputnumber {
|
||||
&:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
@@ -92,8 +92,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.p-dropdown {
|
||||
.p-dropdown-label {
|
||||
.p-select, .p-autocomplete {
|
||||
.p-select-label, .p-autocomplete-input {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ export enum DownloadClientTypeName {
|
||||
export enum NotificationProviderType {
|
||||
Notifiarr = "Notifiarr",
|
||||
Apprise = "Apprise",
|
||||
Ntfy = "Ntfy",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum NtfyAuthenticationType {
|
||||
None = 'None',
|
||||
BasicAuth = 'BasicAuth',
|
||||
AccessToken = 'AccessToken'
|
||||
}
|
||||
14
code/frontend/src/app/shared/models/ntfy-config.model.ts
Normal file
14
code/frontend/src/app/shared/models/ntfy-config.model.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { NotificationConfig } from './notification-config.model';
|
||||
import { NtfyAuthenticationType } from './ntfy-authentication-type.enum';
|
||||
import { NtfyPriority } from './ntfy-priority.enum';
|
||||
|
||||
export interface NtfyConfig extends NotificationConfig {
|
||||
serverUrl?: string;
|
||||
topics?: string[];
|
||||
authenticationType?: NtfyAuthenticationType;
|
||||
username?: string;
|
||||
password?: string;
|
||||
accessToken?: string;
|
||||
priority?: NtfyPriority;
|
||||
tags?: string[];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum NtfyPriority {
|
||||
Min = 'Min',
|
||||
Low = 'Low',
|
||||
Default = 'Default',
|
||||
High = 'High',
|
||||
Max = 'Max'
|
||||
}
|
||||
@@ -253,7 +253,7 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.p-dropdown {
|
||||
.p-select, .p-autocomplete {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -430,10 +430,10 @@ a {
|
||||
}
|
||||
|
||||
/* Accent color for various components */
|
||||
.p-dropdown-panel .p-dropdown-items .p-dropdown-item.p-highlight,
|
||||
.p-select-panel .p-select-items .p-select-item.p-highlight,
|
||||
.p-multiselect-panel .p-multiselect-items .p-multiselect-item.p-highlight,
|
||||
.p-listbox .p-listbox-list .p-listbox-item.p-highlight,
|
||||
.p-dropdown-panel .p-dropdown-items .p-dropdown-item:focus,
|
||||
.p-select-panel .p-select-items .p-select-item:focus,
|
||||
.p-multiselect-panel .p-multiselect-items .p-multiselect-item:focus,
|
||||
.p-listbox .p-listbox-list .p-listbox-item:focus {
|
||||
background-color: rgba(126, 87, 194, 0.16);
|
||||
@@ -448,7 +448,9 @@ a {
|
||||
|
||||
/* Focus and selection ring */
|
||||
.p-component:focus,
|
||||
.p-inputtext:focus {
|
||||
.p-inputtext:focus,
|
||||
.p-select:focus,
|
||||
.p-autocomplete:focus {
|
||||
box-shadow: 0 0 0 1px var(--primary-light);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
59
docs/docs/configuration/notifications/apprise.mdx
Normal file
59
docs/docs/configuration/notifications/apprise.mdx
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
import {
|
||||
ConfigSection,
|
||||
styles
|
||||
} from '@site/src/components/documentation';
|
||||
|
||||
# Apprise
|
||||
|
||||
Apprise is a universal notification library that supports over 80 different notification services.
|
||||
|
||||
<div className={styles.documentationPage}>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
<h2 className={styles.sectionTitle}>
|
||||
<span className={styles.sectionIcon}>📡</span>
|
||||
Configuration
|
||||
</h2>
|
||||
|
||||
<p className={styles.sectionDescription}>
|
||||
Configure Apprise to send notifications through any of its supported services.
|
||||
</p>
|
||||
|
||||
<ConfigSection
|
||||
id="apprise-url"
|
||||
title="Apprise URL"
|
||||
icon="🌐"
|
||||
>
|
||||
|
||||
The Apprise server URL where notification requests will be sent.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="apprise-key"
|
||||
title="Apprise Key"
|
||||
icon="🔐"
|
||||
>
|
||||
|
||||
The key that identifies your Apprise configuration. This corresponds to a configuration defined in your Apprise server.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="apprise-tags"
|
||||
title="Apprise Tags"
|
||||
icon="🏷️"
|
||||
>
|
||||
|
||||
Optionally notify only those tagged accordingly. Use a comma (,) to OR your tags and a space ( ) to AND them. More details on this can be seen in the [Apprise documentation](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging).
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -2,10 +2,8 @@
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
import { Important } from '@site/src/components/Admonition';
|
||||
import {
|
||||
ConfigSection,
|
||||
EnhancedImportant,
|
||||
styles
|
||||
} from '@site/src/components/documentation';
|
||||
|
||||
@@ -17,6 +15,11 @@ Configure notification services to receive alerts about Cleanuparr operations.
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
<h2 className={styles.sectionTitle}>
|
||||
<span className={styles.sectionIcon}>⚙️</span>
|
||||
General Settings
|
||||
</h2>
|
||||
|
||||
<ConfigSection
|
||||
id="enabled"
|
||||
title="Enabled"
|
||||
@@ -37,89 +40,13 @@ A unique name to identify this provider. This name will be displayed in the user
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<h2 className={styles.sectionTitle}>
|
||||
<span className={styles.sectionIcon}>🚀</span>
|
||||
Notifiarr
|
||||
</h2>
|
||||
|
||||
<p className={styles.sectionDescription}>
|
||||
Notifiarr is a notification service that can send alerts to Discord.
|
||||
</p>
|
||||
|
||||
<ConfigSection
|
||||
id="notifiarr-api-key"
|
||||
title="Notifiarr API Key"
|
||||
icon="🔑"
|
||||
>
|
||||
|
||||
Your Notifiarr API key for authentication. This key is obtained from your Notifiarr dashboard.
|
||||
|
||||
<EnhancedImportant>
|
||||
Requires Notifiarr's [Passthrough](https://notifiarr.wiki/pages/integrations/passthrough/) integration to work.
|
||||
</EnhancedImportant>
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="notifiarr-channel-id"
|
||||
title="Notifiarr Channel ID"
|
||||
icon="💬"
|
||||
>
|
||||
|
||||
The Discord channel ID where notifications will be sent. This determines the destination for your alerts.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
<h2 className={styles.sectionTitle}>
|
||||
<span className={styles.sectionIcon}>📡</span>
|
||||
Apprise Configuration
|
||||
</h2>
|
||||
|
||||
<p className={styles.sectionDescription}>
|
||||
Apprise is a universal notification library that supports over 80 different notification services.
|
||||
</p>
|
||||
|
||||
<ConfigSection
|
||||
id="apprise-url"
|
||||
title="Apprise URL"
|
||||
icon="🌐"
|
||||
>
|
||||
|
||||
The Apprise server URL where notification requests will be sent.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="apprise-key"
|
||||
title="Apprise Key"
|
||||
icon="🔐"
|
||||
>
|
||||
|
||||
The key that identifies your Apprise configuration. This corresponds to a configuration defined in your Apprise server.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="apprise-tags"
|
||||
title="Apprise Tags"
|
||||
icon="🏷️"
|
||||
>
|
||||
|
||||
Optionally notify only those tagged accordingly. Use a comma (,) to OR your tags and a space ( ) to AND them. More details on this can be seen in the [Apprise documentation](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging).
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
<h2 id="event-triggers" className={styles.sectionTitle}>
|
||||
<span className={styles.sectionIcon}>⚡</span>
|
||||
Event Triggers
|
||||
Event Configuration
|
||||
</h2>
|
||||
|
||||
<ConfigSection
|
||||
@@ -158,7 +85,7 @@ Optionally notify only those tagged accordingly. Use a comma (,) to OR your tags
|
||||
icon="🗑️"
|
||||
>
|
||||
|
||||
**Triggered When**: A download is removed from the queue.
|
||||
**Triggered When**: A download is removed from an arr queue.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
|
||||
54
docs/docs/configuration/notifications/notifiarr.mdx
Normal file
54
docs/docs/configuration/notifications/notifiarr.mdx
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
import {
|
||||
ConfigSection,
|
||||
EnhancedImportant,
|
||||
styles
|
||||
} from '@site/src/components/documentation';
|
||||
|
||||
# Notifiarr
|
||||
|
||||
Notifiarr is a notification service that can send alerts to Discord.
|
||||
|
||||
<div className={styles.documentationPage}>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
<h2 className={styles.sectionTitle}>
|
||||
<span className={styles.sectionIcon}>🚀</span>
|
||||
Configuration
|
||||
</h2>
|
||||
|
||||
<p className={styles.sectionDescription}>
|
||||
Configure Notifiarr to send notifications to your Discord server.
|
||||
</p>
|
||||
|
||||
<ConfigSection
|
||||
id="notifiarr-api-key"
|
||||
title="Notifiarr API Key"
|
||||
icon="🔑"
|
||||
>
|
||||
|
||||
Your Notifiarr API key for authentication. This key is obtained from your Notifiarr dashboard.
|
||||
|
||||
<EnhancedImportant>
|
||||
Requires Notifiarr's [Passthrough](https://notifiarr.wiki/pages/integrations/passthrough/) integration to work.
|
||||
</EnhancedImportant>
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="notifiarr-channel-id"
|
||||
title="Notifiarr Channel ID"
|
||||
icon="💬"
|
||||
>
|
||||
|
||||
The Discord channel ID where notifications will be sent. This determines the destination for your alerts.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
121
docs/docs/configuration/notifications/ntfy.mdx
Normal file
121
docs/docs/configuration/notifications/ntfy.mdx
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
import {
|
||||
ConfigSection,
|
||||
styles
|
||||
} from '@site/src/components/documentation';
|
||||
|
||||
# ntfy
|
||||
|
||||
ntfy is a simple HTTP-based pub-sub notification service. You can use the public service at ntfy.sh or run your own server.
|
||||
|
||||
<div className={styles.documentationPage}>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
<h2 className={styles.sectionTitle}>
|
||||
<span className={styles.sectionIcon}>🔔</span>
|
||||
Configuration
|
||||
</h2>
|
||||
|
||||
<p className={styles.sectionDescription}>
|
||||
Configure ntfy to send push notifications to your devices.
|
||||
</p>
|
||||
|
||||
<ConfigSection
|
||||
id="ntfy-server-url"
|
||||
title="Server URL"
|
||||
icon="🌐"
|
||||
>
|
||||
|
||||
The URL of your ntfy server. Use `https://ntfy.sh` for the public service or your self-hosted instance URL.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="ntfy-topics"
|
||||
title="Topics"
|
||||
icon="📢"
|
||||
>
|
||||
|
||||
The ntfy topics you want to publish to. You can specify multiple topics to send notifications to different channels or devices.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="ntfy-authentication-type"
|
||||
title="Authentication Type"
|
||||
icon="🔐"
|
||||
>
|
||||
|
||||
Choose how to authenticate with the ntfy server:
|
||||
- **None**: No authentication required (public topics)
|
||||
- **Basic Auth**: Username and password authentication
|
||||
- **Access Token**: Bearer token authentication
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="ntfy-username"
|
||||
title="Username"
|
||||
icon="👤"
|
||||
>
|
||||
|
||||
Your username for basic authentication. Required when using Basic Auth authentication type.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="ntfy-password"
|
||||
title="Password"
|
||||
icon="🔑"
|
||||
>
|
||||
|
||||
Your password for basic authentication. Required when using Basic Auth authentication type.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="ntfy-access-token"
|
||||
title="Access Token"
|
||||
icon="🎫"
|
||||
>
|
||||
|
||||
Your access token for bearer token authentication. Required when using Access Token authentication type.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="ntfy-priority"
|
||||
title="Priority"
|
||||
icon="🔥"
|
||||
>
|
||||
|
||||
The priority level for notifications:
|
||||
- **Min**: No vibration or sound. The notification will be under the fold in "Other notifications".
|
||||
- **Low**: No vibration or sound. Notification will not visibly show up until notification drawer is pulled down.
|
||||
- **Default**: Short default vibration and sound. Default notification behavior.
|
||||
- **High**: Long vibration burst, default notification sound with a pop-over notification.
|
||||
- **Max**: Really long vibration bursts, default notification sound with a pop-over notification.
|
||||
|
||||
Reference: https://docs.ntfy.sh/publish/#message-priority
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="ntfy-tags"
|
||||
title="Tags"
|
||||
icon="🏷️"
|
||||
>
|
||||
|
||||
Tags to add to notifications.
|
||||
|
||||
Reference: https://docs.ntfy.sh/publish/#tags-emojis
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user